5 Commits

Author SHA1 Message Date
Mohsen Taba 354a64c742 feat(hadis): add single-file sqlite export and optimize sync serializers 4 weeks ago
Mohsen Taba 315fc26cad merged with recent changes 4 weeks ago
Mohsen Taba b2e6469b13 sync request increment support enhancement 4 weeks ago
Mohsen Taba 6ac32e11a2 perf(db): resolve N+1 queries across endpoints and optimize gunicorn concurrency 1 month ago
Mohsen Taba a90852f8c7 feat(video): temporarily disable youtube stream url resolution 1 month ago
  1. 2
      apps/article/serializers.py
  2. 15
      apps/article/views.py
  3. 19
      apps/bookmark/serializers/bookmark.py
  4. 492
      apps/hadis/HADIS_SYNC_SAMPLE.md
  5. 21
      apps/hadis/admin/version.py
  6. 689
      apps/hadis/hadis_sample.json
  7. 415
      apps/hadis/management/commands/export_hadis_sqlite.py
  8. 34
      apps/hadis/migrations/0040_hadisdatabaseversion.py
  9. 19
      apps/hadis/models/reference.py
  10. 28
      apps/hadis/models/version.py
  11. 35
      apps/hadis/serializers/hadis.py
  12. 6
      apps/hadis/serializers/reference.py
  13. 42
      apps/hadis/serializers/version.py
  14. 3
      apps/hadis/urls.py
  15. 25
      apps/hadis/views/hadis.py
  16. 2
      apps/hadis/views/reference.py
  17. 25
      apps/hadis/views/version.py
  18. 25
      apps/hadis/views_admin.py
  19. 2
      apps/library/views.py
  20. 10
      apps/podcast/models.py
  21. 2
      apps/podcast/serializers.py
  22. 10
      apps/podcast/views.py
  23. 10
      apps/video/models.py
  24. 2
      apps/video/views.py
  25. 2
      docker-compose.prod.yml
  26. 93
      utils/youtube.py

2
apps/article/serializers.py

@ -12,6 +12,8 @@ class ArticleCategoryListSerializer(serializers.ModelSerializer):
fields = ['id', 'title', 'slug', 'acticle_count']
def get_acticle_count(self, obj):
if hasattr(obj, 'article_count_annotated'):
return obj.article_count_annotated
return obj.articles.filter(status=True).count()
class PinnedArticleCollectionSerializer(serializers.ModelSerializer):

15
apps/article/views.py

@ -1,4 +1,4 @@
from django.db.models import Q
from django.db.models import Q, Count, Prefetch
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.decorators import action
@ -39,7 +39,9 @@ class ArticleCategoryListAPIView(generics.ListAPIView):
return super().get(request, *args, **kwargs)
def get_queryset(self):
return ArticleCategory.objects.filter(status=True).order_by('order')
return ArticleCategory.objects.filter(status=True).annotate(
article_count_annotated=Count('articles', filter=Q(articles__status=True), distinct=True)
).order_by('order')
class PinnedArticleCollectionListView(generics.ListAPIView):
@ -170,7 +172,14 @@ class ArticleListAPIView(generics.ListAPIView):
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = Article.objects.filter(status=True)
queryset = Article.objects.filter(status=True).prefetch_related(
Prefetch(
'categories',
queryset=ArticleCategory.objects.annotate(
article_count_annotated=Count('articles', filter=Q(articles__status=True), distinct=True)
)
)
)
# Search by title if search parameter is provided
search_query = self.request.query_params.get('search', None)

19
apps/bookmark/serializers/bookmark.py

@ -64,11 +64,20 @@ class BookmarkStatusSerializer(serializers.Serializer):
'content_id': None
}
is_bookmarked = Bookmark.is_bookmarked(
user=user,
service=service,
content_id=content_id
)
# Cache active bookmarks on the user object to eliminate N+1 queries across list views
if not hasattr(user, '_cached_bookmarks'):
user._cached_bookmarks = {}
if service not in user._cached_bookmarks:
user._cached_bookmarks[service] = set(
Bookmark.objects.filter(
user=user,
service=service,
status=True
).values_list('content_id', flat=True)
)
is_bookmarked = content_id in user._cached_bookmarks[service]
return {
'is_bookmarked': is_bookmarked,

492
apps/hadis/HADIS_SYNC_SAMPLE.md

@ -0,0 +1,492 @@
# نمونه کامل خروجی یک آبجکت حدیث در اندپوینت سینک (`/api/hadis/sync/hadis/`)
این سند ساختار دقیق، واقعی و تودرتوی **فقط یک رکورد حدیث** (حدیث شماره ۱) را نشان می‌دهد که مستقیماً از دیتابیس فعلی و از طریق سریالایزر `HadisSyncSerializer` استخراج شده است.
---
## 📊 مشخصات این آبجکت:
- **تعداد خطوط JSON برای یک حدیث:** ۶۸۹ خط!
- **حجم خالص متنی برای یک حدیث:** حدود **۳۲ کیلوبایت**
- **حجم کل برای ۱۰۵۳ حدیث:** حدود **۳۳ مگابایت** (به دلیل تکرار داده‌های تودرتو)
---
## 🔍 نقاط بحرانی تکرار و تودرتو بودن (Redundancy & Bloat):
1. **تکرار تفاسیر دسته‌بندی در هر حدیث (`interpretations`):**
- تفاسیر به دسته‌بندی تعلق دارند، اما در خروجی **داخل تک‌تک احادیث آن دسته تکرار می‌شوند**. متون تفاسیر شامل کدهای طولانی HTML عربی و روسی هستند.
2. **تکرار مشخصات راویان و لایه‌ها (`transmitters`):**
- برای هر راوی در زنجیره سند (مثلاً ۱۰ راوی)، آبجکت‌های کامل `reliability` و `layer_name` و `layer_description` به همراه لیستی از `uncertain_transmitters` تکرار می‌شوند.
3. **تکرار اطلاعات کتاب‌ها و رفرنس‌ها در تصحیحات (`corrections -> references`):**
- داخل هر تصحیح، آبجکت‌های رفرنس، تصاویر، مشخصات چاپ (`edition_info`) و لینک‌ها به صورت تودرتو دوباره تکرار شده‌اند.
4. **ارجاع به خودِ حدیث در بخش تصحیحات (`hadis_info`):**
- داخل آرایه تصحیحاتِ همین حدیث، فیلد `hadis_info` دوباره `id` و `slug` خودِ همین حدیث را تکرار می‌کند!
---
## 📄 ساختار کامل JSON (نمونه واقعی استخراج‌شده از دیتابیس):
```json
{
"id": 1891,
"number": 1,
"slug": "the-merit-of-seeking-knowledge",
"category_id": 1036,
"category_slug": "reflections",
"title": "The Merit of Seeking Knowledge",
"title_narrator": "Jabir bin Abdullah",
"text": "اتَّقِ اللَّهَ حَيْثُمَا كُنْتَ، وَأَتْبِعِ السَّيِّئَةَ الْحَسَنَةَ تَمْحُهَا.",
"translation": [
{
"title": "Передается от Пророка (мир ему): Правда ведет к благочестию. Это путь праведных.",
"language_code": "ru"
},
{
"title": "The best among you are those who have the best manners and character.",
"language_code": "en"
}
],
"detail": {
"address": [
"Sahih al-Bukhari",
"Book of Revelation",
"Chapter 1: How Revelation Began"
],
"address_details": [],
"hadis_status": {
"id": 133,
"title": "Authentic / Accepted",
"description": "A Sahih (Authentic) Hadith represents the highest level of reliability. It possesses a continuous chain of narrators (Isnad) made up of upright (Adl) and retentive (Dabit) transmitters, free from any hidden defects (Illah) or irregularities (Shudhudh). It is universally accepted for deriving Islamic laws.",
"color": "green",
"main_color_code": "#1DAC43"
},
"status_text": null,
"share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773",
"links": {
"Dorar": "https://dorar.net/hadith/1804",
"Al-Islam": "https://www.al-islam.org/6596",
"IslamWeb": "https://www.islamweb.net/ar/fatwa/6463",
"Sunnah.com": "https://sunnah.com/bukhari/3147",
"IslamicFinder": "https://www.islamicfinder.org/1779"
},
"tags": [
{
"id": 510,
"title": "Хадж"
},
{
"id": 516,
"title": "Доверие"
},
{
"id": 561,
"title": "Kindness"
},
{
"id": 565,
"title": "Hellfire (Jahannam)"
},
{
"id": 567,
"title": "Marriage (Nikah)"
}
],
"references": [
{
"id": 2538,
"title": "Man La Yahduruhu al-Faqih",
"authors": [
{
"id": 9,
"name": "sheykh hamid"
}
],
"description": [
{
"title": "For him who is not in the presence of a jurisprudent - Shia collection.",
"language_code": "en"
},
{
"title": "توضیحات درباره من لا یحضره الفقیه",
"language_code": "fa"
}
],
"share_link": "https://dovodi.newhorizonco.uk/arguments/sources/man-la-yahduruhu-al-faqih"
}
],
"reference_images": [
{
"id": 2541,
"thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref1.png",
"priority": 1
},
{
"id": 2542,
"thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref2.png",
"priority": 2
},
{
"id": 2543,
"thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref3.png",
"priority": 3
},
{
"id": 2544,
"thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref4.png",
"priority": 4
}
]
},
"narrators": {
"description": "Focuses on the relationship between the Creator and the creation.",
"transmitters": [
{
"id": 850,
"name": "Ali ibn Yaqtin",
"slug": "ali-ibn-yaqtin",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 14,
"title": "Reliable",
"slug": "reliable-1-190858903395",
"color": "blue",
"main_color_code": "#5172E1"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": true,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "40",
"order": 0
},
{
"id": 859,
"name": "Al-Muhaqqiq al-Hilli (Ja'far ibn al-Hasan)",
"slug": "al-muhaqqiq-al-hilli-jafar-ibn-al-hasan",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 17,
"title": "Very Weak",
"slug": "very-weak-1-190858905701",
"color": "red",
"main_color_code": "#D33A3A"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": false,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "36",
"order": 1
},
{
"id": 867,
"name": "Al-Miqdad ibn al-Aswad",
"slug": "al-miqdad-ibn-al-aswad",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 12,
"title": "Unknown",
"slug": "unknown",
"color": "gray",
"main_color_code": "#374151"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": true,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "117",
"order": 2
},
{
"id": 1016,
"name": "Muhammad ibn Yahya al-Attar al-Qummi",
"slug": "muhammad-ibn-yahya-al-attar-al-qummi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 58,
"title": "сподвижник",
"slug": "сподвижник",
"color": "gray",
"main_color_code": "#374151"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": false,
"is_uncertain": true,
"uncertain_transmitters": [
{
"id": 871,
"name": "Malik al-Ashtar al-Nakha'i",
"slug": "malik-al-ashtar-al-nakhai",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "161",
"reliability": {
"id": 7,
"title": "Very Reliable",
"slug": "very-reliable",
"color": "green",
"main_color_code": "#1DAC43"
}
},
{
"id": 1016,
"name": "Muhammad ibn Yahya al-Attar al-Qummi",
"slug": "muhammad-ibn-yahya-al-attar-al-qummi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": "",
"death_year_hijri": "57",
"reliability": {
"id": 58,
"title": "сподвижник",
"slug": "сподвижник",
"color": "gray",
"main_color_code": "#374151"
}
}
],
"birth_year_hijri": "",
"death_year_hijri": "57",
"order": 3
},
{
"id": 886,
"name": "Ali ibn Mahziyar al-Ahwazi",
"slug": "ali-ibn-mahziyar-al-ahwazi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 16,
"title": "Weak",
"slug": "weak-1-190858898727",
"color": "orange",
"main_color_code": "#E67E22"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": false,
"is_uncertain": true,
"uncertain_transmitters": [
{
"id": 884,
"name": "Abd Allah ibn Mughirah",
"slug": "abd-allah-ibn-mughirah",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "385",
"reliability": {
"id": 13,
"title": "Very Reliable",
"slug": "very-reliable-1-190858893632",
"color": "green",
"main_color_code": "#1DAC43"
}
},
{
"id": 886,
"name": "Ali ibn Mahziyar al-Ahwazi",
"slug": "ali-ibn-mahziyar-al-ahwazi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "405",
"reliability": {
"id": 16,
"title": "Weak",
"slug": "weak-1-190858898727",
"color": "orange",
"main_color_code": "#E67E22"
}
}
],
"birth_year_hijri": null,
"death_year_hijri": "405",
"order": 4
},
{
"id": 1033,
"name": "Muhammad Hasan al-Shirazi",
"slug": "muhammad-hasan-al-shirazi",
"known_as": "زراره",
"nickname": "زراره شیبانی",
"reliability": {
"id": 7,
"title": "Very Reliable",
"slug": "very-reliable",
"color": "green",
"main_color_code": "#1DAC43"
},
"layer_id": 5700,
"narrator_layer_id": 5700,
"layer_level": 2,
"layer_name": "Prominent Hadith Compilers",
"layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.",
"is_gap": false,
"is_uncertain": true,
"uncertain_transmitters": [
{
"id": 1033,
"name": "Muhammad Hasan al-Shirazi",
"slug": "muhammad-hasan-al-shirazi",
"known_as": "زراره",
"nickname": "زراره شیبانی",
"birth_year_hijri": "80",
"death_year_hijri": "150",
"reliability": {
"id": 7,
"title": "Very Reliable",
"slug": "very-reliable",
"color": "green",
"main_color_code": "#1DAC43"
}
},
{
"id": 853,
"name": "Muhammad ibn Ya'qub al-Kulayni",
"slug": "muhammad-ibn-yaqub-al-kulayni",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "35",
"reliability": {
"id": 15,
"title": "Acceptable",
"slug": "acceptable-1-190858896271",
"color": "yellow",
"main_color_code": "#EDC130"
}
}
],
"birth_year_hijri": "80",
"death_year_hijri": "150",
"order": 5
}
]
},
"explanations": [
{
"title": "Vocabulary & Linguistic Analysis",
"detail": "The term 'Niyyah' (Intention) is pivotal in Islamic lexicon, derived from the root 'nawa'..."
},
{
"title": "Context of Revelation (Asbab al-Wurud)",
"detail": "This tradition is famously known as the 'Hadith of the Migrant to Umm Qais'..."
}
],
"corrections": [
{
"id": 26,
"title": "Scholar Note #4",
"slug": "scholar-note-4",
"narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ",
"description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ...",
"translation": "Revised text segment for correction 4...",
"share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/scholar-note-4",
"hadis_info": {
"id": 1891,
"number": 1,
"slug": "the-merit-of-seeking-knowledge"
},
"address": [
"Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\"..."
],
"images": [],
"references": [
{
"id": 4034,
"book": null,
"author": null,
"edition": null,
"volume_info": null,
"hadith_number": null,
"volume": null,
"pages": null,
"address": "Tirmidhi, Abu Isa Muhammad ibn Isa...",
"url": null,
"open_book_url": null,
"citation_summary": "",
"images": [],
"images_count": 0,
"book_title": null,
"book_authors": [],
"edition_info": null
}
],
"links": [
{
"link": "google.com",
"title": "Первая ссылка"
}
]
}
],
"interpretations": [
{
"id": 12,
"title": null,
"slug": "cat-l3-25-7f5bcb",
"narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ",
"text": "<p dir=\"rtl\">الْمَسْأَلَةُ الثَّامِنَةُ : قَوْلُهُ : { أَهْلَ الْبَيْتِ }...</p>",
"translation": "<p>Вопрос восьмой: (что означают) слова: Ахл аль-бейт...</p>",
"category_info": {
"id": 1036,
"slug": "reflections",
"title": "Reflections"
},
"address": [
"Tirmidhi, Abu Isa Muhammad ibn Isa..."
],
"images": [],
"links": [],
"references": [
{
"id": 76,
"book_title": "last book",
"book_authors": [
{
"id": 9,
"name": "sheykh hamid"
}
],
"edition_info": null,
"volume_info": null,
"hadith_number": null,
"volume": "",
"pages": "",
"address": "Tirmidhi, Abu Isa Muhammad ibn Isa...",
"url": null,
"images": []
}
]
}
]
}
```

21
apps/hadis/admin/version.py

@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin
from utils.admin import dovoodi_admin_site
from ..models import ContentRelease
from ..models import ContentRelease, HadisDatabaseVersion
class ContentReleaseAdmin(ModelAdmin):
@ -25,5 +25,22 @@ class ContentReleaseAdmin(ModelAdmin):
)
# Register model with the custom admin site
class HadisDatabaseVersionAdmin(ModelAdmin):
"""Admin for HadisDatabaseVersion SQLite files"""
list_display = ('version', 'file_size_display', 'hadis_count', 'categories_count', 'is_active', 'created_at')
list_filter = ('is_active', 'created_at')
search_fields = ('version', 'notes', 'checksum_md5')
readonly_fields = ('created_at', 'checksum_md5', 'file_size')
ordering = ('-version',)
def file_size_display(self, obj):
if obj.file_size:
return f"{obj.file_size / (1024 * 1024):.2f} MB"
return "0 MB"
file_size_display.short_description = _("حجم فایل")
# Register models with the custom admin site
dovoodi_admin_site.register(ContentRelease, ContentReleaseAdmin)
dovoodi_admin_site.register(HadisDatabaseVersion, HadisDatabaseVersionAdmin)

689
apps/hadis/hadis_sample.json

@ -0,0 +1,689 @@
{
"id": 1891,
"number": 1,
"slug": "the-merit-of-seeking-knowledge",
"category_id": 1036,
"category_slug": "reflections",
"title": "The Merit of Seeking Knowledge",
"title_narrator": "Jabir bin Abdullah",
"text": "اتَّقِ اللَّهَ حَيْثُمَا كُنْتَ، وَأَتْبِعِ السَّيِّئَةَ الْحَسَنَةَ تَمْحُهَا.",
"translation": [
{
"title": "Передается от Пророка (мир ему): Правда ведет к благочестию. Это путь праведных.",
"language_code": "ru"
},
{
"title": "The best among you are those who have the best manners and character.",
"language_code": "en"
}
],
"detail": {
"address": [
"Sahih al-Bukhari",
"Book of Revelation",
"Chapter 1: How Revelation Began"
],
"address_details": [],
"hadis_status": {
"id": 133,
"title": "Authentic / Accepted",
"description": "A Sahih (Authentic) Hadith represents the highest level of reliability. It possesses a continuous chain of narrators (Isnad) made up of upright (Adl) and retentive (Dabit) transmitters, free from any hidden defects (Illah) or irregularities (Shudhudh). It is universally accepted for deriving Islamic laws.",
"color": "green",
"main_color_code": "#1DAC43"
},
"status_text": null,
"share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773",
"links": {
"Dorar": "https://dorar.net/hadith/1804",
"Al-Islam": "https://www.al-islam.org/6596",
"IslamWeb": "https://www.islamweb.net/ar/fatwa/6463",
"Sunnah.com": "https://sunnah.com/bukhari/3147",
"IslamicFinder": "https://www.islamicfinder.org/1779"
},
"tags": [
{
"id": 510,
"title": "Хадж"
},
{
"id": 516,
"title": "Доверие"
},
{
"id": 561,
"title": "Kindness"
},
{
"id": 565,
"title": "Hellfire (Jahannam)"
},
{
"id": 567,
"title": "Marriage (Nikah)"
}
],
"references": [
{
"id": 2538,
"title": "Man La Yahduruhu al-Faqih",
"authors": [
{
"id": 9,
"name": "sheykh hamid"
}
],
"description": [
{
"title": "For him who is not in the presence of a jurisprudent - Shia collection.",
"language_code": "en"
},
{
"title": "توضیحات درباره من لا یحضره الفقیه",
"language_code": "fa"
}
],
"share_link": "https://dovodi.newhorizonco.uk/arguments/sources/man-la-yahduruhu-al-faqih"
}
],
"reference_images": [
{
"id": 2541,
"thumbnail": "https://testserver/media/hadis/reference_images/1891_ref1.png",
"priority": 1
},
{
"id": 2542,
"thumbnail": "https://testserver/media/hadis/reference_images/1891_ref2.png",
"priority": 2
},
{
"id": 2543,
"thumbnail": "https://testserver/media/hadis/reference_images/1891_ref3.png",
"priority": 3
},
{
"id": 2544,
"thumbnail": "https://testserver/media/hadis/reference_images/1891_ref4.png",
"priority": 4
}
]
},
"narrators": {
"description": "Focuses on the relationship between the Creator and the creation.",
"transmitters": [
{
"id": 850,
"name": "Ali ibn Yaqtin",
"slug": "ali-ibn-yaqtin",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 14,
"title": "Reliable",
"slug": "reliable-1-190858903395",
"color": "blue",
"main_color_code": "#5172E1"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": true,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "40",
"order": 0
},
{
"id": 859,
"name": "Al-Muhaqqiq al-Hilli (Ja'far ibn al-Hasan)",
"slug": "al-muhaqqiq-al-hilli-jafar-ibn-al-hasan",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 17,
"title": "Very Weak",
"slug": "very-weak-1-190858905701",
"color": "red",
"main_color_code": "#D33A3A"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": false,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "36",
"order": 1
},
{
"id": 867,
"name": "Al-Miqdad ibn al-Aswad",
"slug": "al-miqdad-ibn-al-aswad",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 12,
"title": "Unknown",
"slug": "unknown",
"color": "gray",
"main_color_code": "#374151"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": true,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "117",
"order": 2
},
{
"id": 1016,
"name": "Muhammad ibn Yahya al-Attar al-Qummi",
"slug": "muhammad-ibn-yahya-al-attar-al-qummi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 58,
"title": "сподвижник",
"slug": "сподвижник",
"color": "gray",
"main_color_code": "#374151"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": false,
"is_uncertain": true,
"uncertain_transmitters": [
{
"id": 871,
"name": "Malik al-Ashtar al-Nakha'i",
"slug": "malik-al-ashtar-al-nakhai",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "161",
"reliability": {
"id": 7,
"title": "Very Reliable",
"slug": "very-reliable",
"color": "green",
"main_color_code": "#1DAC43"
}
},
{
"id": 1016,
"name": "Muhammad ibn Yahya al-Attar al-Qummi",
"slug": "muhammad-ibn-yahya-al-attar-al-qummi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": "",
"death_year_hijri": "57",
"reliability": {
"id": 58,
"title": "сподвижник",
"slug": "сподвижник",
"color": "gray",
"main_color_code": "#374151"
}
}
],
"birth_year_hijri": "",
"death_year_hijri": "57",
"order": 3
},
{
"id": 886,
"name": "Ali ibn Mahziyar al-Ahwazi",
"slug": "ali-ibn-mahziyar-al-ahwazi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 16,
"title": "Weak",
"slug": "weak-1-190858898727",
"color": "orange",
"main_color_code": "#E67E22"
},
"layer_id": 5699,
"narrator_layer_id": 5699,
"layer_level": 1,
"layer_name": "Primary Chain & Early Narrators",
"layer_description": "Early generation transmitters, companions, and direct students.",
"is_gap": false,
"is_uncertain": true,
"uncertain_transmitters": [
{
"id": 884,
"name": "Abd Allah ibn Mughirah",
"slug": "abd-allah-ibn-mughirah",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "385",
"reliability": {
"id": 13,
"title": "Very Reliable",
"slug": "very-reliable-1-190858893632",
"color": "green",
"main_color_code": "#1DAC43"
}
},
{
"id": 886,
"name": "Ali ibn Mahziyar al-Ahwazi",
"slug": "ali-ibn-mahziyar-al-ahwazi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "405",
"reliability": {
"id": 16,
"title": "Weak",
"slug": "weak-1-190858898727",
"color": "orange",
"main_color_code": "#E67E22"
}
}
],
"birth_year_hijri": null,
"death_year_hijri": "405",
"order": 4
},
{
"id": 1033,
"name": "Muhammad Hasan al-Shirazi",
"slug": "muhammad-hasan-al-shirazi",
"known_as": "زراره",
"nickname": "زراره شیبانی",
"reliability": {
"id": 7,
"title": "Very Reliable",
"slug": "very-reliable",
"color": "green",
"main_color_code": "#1DAC43"
},
"layer_id": 5700,
"narrator_layer_id": 5700,
"layer_level": 2,
"layer_name": "Prominent Hadith Compilers",
"layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.",
"is_gap": false,
"is_uncertain": true,
"uncertain_transmitters": [
{
"id": 1033,
"name": "Muhammad Hasan al-Shirazi",
"slug": "muhammad-hasan-al-shirazi",
"known_as": "زراره",
"nickname": "زراره شیبانی",
"birth_year_hijri": "80",
"death_year_hijri": "150",
"reliability": {
"id": 7,
"title": "Very Reliable",
"slug": "very-reliable",
"color": "green",
"main_color_code": "#1DAC43"
}
},
{
"id": 853,
"name": "Muhammad ibn Ya'qub al-Kulayni",
"slug": "muhammad-ibn-yaqub-al-kulayni",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"birth_year_hijri": null,
"death_year_hijri": "35",
"reliability": {
"id": 15,
"title": "Acceptable",
"slug": "acceptable-1-190858896271",
"color": "yellow",
"main_color_code": "#EDC130"
}
}
],
"birth_year_hijri": "80",
"death_year_hijri": "150",
"order": 5
},
{
"id": 1016,
"name": "Muhammad ibn Yahya al-Attar al-Qummi",
"slug": "muhammad-ibn-yahya-al-attar-al-qummi",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 58,
"title": "сподвижник",
"slug": "сподвижник",
"color": "gray",
"main_color_code": "#374151"
},
"layer_id": 5700,
"narrator_layer_id": 5700,
"layer_level": 2,
"layer_name": "Prominent Hadith Compilers",
"layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.",
"is_gap": true,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": "",
"death_year_hijri": "57",
"order": 6
},
{
"id": 856,
"name": "Ali ibn al-Husayn al-Sharif al-Murtada",
"slug": "ali-ibn-al-husayn-al-sharif-al-murtada",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 12,
"title": "Unknown",
"slug": "unknown",
"color": "gray",
"main_color_code": "#374151"
},
"layer_id": 5700,
"narrator_layer_id": 5700,
"layer_level": 2,
"layer_name": "Prominent Hadith Compilers",
"layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.",
"is_gap": false,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "37",
"order": 7
},
{
"id": 89,
"name": "Al-Fudayl ibn Yasar",
"slug": "al-fudayl-ibn-yasar",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 9,
"title": "Acceptable",
"slug": "acceptable",
"color": "yellow",
"main_color_code": "#EDC130"
},
"layer_id": 5700,
"narrator_layer_id": 5700,
"layer_level": 2,
"layer_name": "Prominent Hadith Compilers",
"layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.",
"is_gap": false,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "250",
"order": 8
},
{
"id": 863,
"name": "Al-Hurr al-Amili (Muhammad ibn al-Hasan)",
"slug": "al-hurr-al-amili-muhammad-ibn-al-hasan",
"known_as": "ibn Hilal",
"nickname": "Al-Basri",
"reliability": {
"id": 10,
"title": "Weak",
"slug": "weak",
"color": "orange",
"main_color_code": "#E67E22"
},
"layer_id": 5700,
"narrator_layer_id": 5700,
"layer_level": 2,
"layer_name": "Prominent Hadith Compilers",
"layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.",
"is_gap": true,
"is_uncertain": false,
"uncertain_transmitters": [],
"birth_year_hijri": null,
"death_year_hijri": "94",
"order": 9
}
]
},
"explanations": [
{
"title": "Vocabulary & Linguistic Analysis",
"detail": "The term 'Niyyah' (Intention) is pivotal in Islamic lexicon, derived from the root 'nawa', meaning to aim, direct, or purpose oneself towards a specific destination. In the terminology of the jurists (Fuqaha), it serves a dual function: firstly, to distinguish acts of worship (Ibadah) from mundane customary habits (Adah)—such as distinguishing the act of washing oneself for cooling off from the act of Ghusl (ritual purification). Secondly, it distinguishes different ranks of worship from one another, such as separating a noon prayer that is obligatory (Fard) from one that is voluntary (Sunnah). Philosophically, scholars like Al-Ghazali have elaborated that Niyyah is the 'spirit' of the action, while the physical movement is merely the 'body'; a body without a spirit is lifeless, just as an action without sincere intention holds no weight in the Divine Court."
},
{
"title": "Context of Revelation (Asbab al-Wurud)",
"detail": "This tradition is famously known as the 'Hadith of the Migrant to Umm Qais'. It was delivered by the Prophet (PBUH) from the pulpit in Medina to address a subtle but critical issue within the community. Reports indicate that a man had migrated from Mecca to Medina not solely for the sake of Allah and His Messenger, but because a woman named Umm Qais had refused to marry him unless he undertook the migration. While his physical journey was identical to the other companions who sacrificed their homes for faith, his internal compass was directed towards a worldly gain. The Prophet (PBUH) used this specific incident to establish a universal maxim: that the validity and reward of any deed are inextricably bound to the hidden motive behind it, warning that one who targets the world will attain only that, with no share in the Hereafter."
}
],
"corrections": [
{
"id": 26,
"title": "Scholar Note #4",
"slug": "scholar-note-4",
"narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ",
"description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.\nاللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.\nوَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ.",
"translation": "Revised text segment for correction 4...",
"share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/scholar-note-4",
"hadis_info": {
"id": 1891,
"number": 1,
"slug": "the-merit-of-seeking-knowledge"
},
"address": [
"Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes."
],
"images": [],
"references": [
{
"id": 4034,
"book": null,
"author": null,
"edition": null,
"volume_info": null,
"hadith_number": null,
"volume": null,
"pages": null,
"address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.",
"url": null,
"open_book_url": null,
"citation_summary": "",
"images": [],
"images_count": 0,
"book_title": null,
"book_authors": [],
"edition_info": null
}
],
"links": [
{
"link": "google.com",
"title": "Первая ссылка Первая ссылка ссылкассы..."
},
{
"link": "google.com",
"title": "Первая ссылка."
},
{
"link": "google.com",
"title": "Первая ссылка."
}
]
},
{
"id": 25,
"title": "Alternative Translation #3",
"slug": "alternative-translation-3",
"narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ",
"description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.\nاللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.\nوَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ.",
"translation": "Revised text segment for correction 3...",
"share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/alternative-translation-3",
"hadis_info": {
"id": 1891,
"number": 1,
"slug": "the-merit-of-seeking-knowledge"
},
"address": [
"Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes."
],
"images": [],
"references": [
{
"id": 4035,
"book": null,
"author": null,
"edition": null,
"volume_info": null,
"hadith_number": null,
"volume": null,
"pages": null,
"address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.",
"url": null,
"open_book_url": null,
"citation_summary": "",
"images": [],
"images_count": 0,
"book_title": null,
"book_authors": [],
"edition_info": null
}
],
"links": [
{
"link": "google.com",
"title": "Первая ссылка Первая ссылка ссылкассы..."
},
{
"link": "google.com",
"title": "Первая ссылка."
},
{
"link": "google.com",
"title": "Первая ссылка."
}
]
},
{
"id": 24,
"title": "Context Clarification #2",
"slug": "context-clarification-2",
"narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ",
"description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\r\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.\r\nاللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.\r\nوَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ.",
"translation": "Revised text segment for correction 2...",
"share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/context-clarification-2",
"hadis_info": {
"id": 1891,
"number": 1,
"slug": "the-merit-of-seeking-knowledge"
},
"address": [
"Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes."
],
"images": [
{
"id": 85,
"image": "https://testserver/media/hadis/correction_images/image.png",
"thumbnail": "https://testserver/media/hadis/correction_images/image.png"
}
],
"references": [
{
"id": 4036,
"book": null,
"author": null,
"edition": null,
"volume_info": null,
"hadith_number": null,
"volume": null,
"pages": null,
"address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.",
"url": null,
"open_book_url": null,
"citation_summary": "",
"images": [
{
"id": 85,
"image": "https://testserver/media/hadis/correction_images/image.png",
"thumbnail": "https://testserver/media/hadis/correction_images/image.png",
"priority": 0
}
],
"images_count": 1,
"book_title": null,
"book_authors": [],
"edition_info": null
}
],
"links": {}
}
],
"interpretations": [
{
"id": 12,
"title": null,
"slug": "cat-l3-25-7f5bcb",
"narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ",
"text": "<p dir=\"rtl\"><span style=\"color: #e65050;\">الْمَسْأَلَةُ الثَّامِنَةُ : قَوْلُهُ : { أَهْلَ الْبَيْتِ }</span></p>\n\n<p dir=\"rtl\">رُوِيَ عَنْ عُمَرَ بْنِ أَبِي سَلَمَةَ أَنَّهُ قَالَ : { لَمَّا نَزَلَتْ هَذِهِ الْآيَةُ عَلَى النَّبِيِّ صَلَّى اللَّهُ عَلَيْهِ وَسَلَّمَ : { إنَّمَا يُرِيدُ اللَّهُ لِيُذْهِبَ عَنْكُمْ الرِّجْسَ أَهْلَ الْبَيْتِ وَيُطَهِّرَكُمْ تَطْهِيرًا } فِي بَيْتِ أُمِّ سَلَمَةَ دَعَا النَّبِيُّ صَلَّى اللَّهُ عَلَيْهِ وَسَلَّمَ&nbsp;<span style=\"color: #e65050;\">فَاطِمَةَ وَحَسَنًا وَحُسَيْنًا</span>&nbsp;، وَجَعَلَ&nbsp;<span style=\"color: #e65050;\">عَلِيًّا</span>&nbsp;خَلْفَ ظَهْرِهِ ، وَجَلَّلَهُمْ بِكِسَاءٍ ، ثُمَّ قَالَ : اللَّهُمَّ إنَّ هَؤُلَاءِ أَهْلَ بَيْتِي ، فَأَذْهِبْ عَنْهُمْ الرِّجْسَ وَطَهِّرْهُمْ تَطْهِيرًا . قَالَتْ أُمُّ سَلَمَةَ : وَأَنَا مَعَهُمْ يَا نَبِيَّ اللَّهِ . قَالَ : أَنْتِ عَلَى مَكَانِك وَأَنْتِ عَلَى خَيْرٍ } .</p>\n\n<p dir=\"rtl\">وَرَوَى أَنَسُ بْنُ مَالِكٍ { أَنَّ رَسُولَ اللَّهِ صَلَّى اللَّهُ عَلَيْهِ وَسَلَّمَ كَانَ يَمُرُّ بِبَابِ فَاطِمَةَ سِتَّةَ أَشْهُرٍ إذَا خَرَجَ إلَى صَلَاةِ الْفَجْرِ يَقُولُ : الصَّلَاةَ يَا أَهْلَ الْبَيْتِ ، إنَّمَا يُرِيدُ اللَّهُ لِيُذْهِبَ عَنْكُمْ الرِّجْسَ أَهْلَ الْبَيْتِ وَيُطَهِّرَكُمْ تَطْهِيرًا } . خَرَّجَ هَذَيْنِ الْحَدِيثَيْنِ التِّرْمِذِيُّ وَغَيْرُهُ .</p>",
"translation": "<p><span style=\"color: #e65050;\">Вопрос восьмой: (что означают) слова: Ахл аль-бейт</span></p>\n\n<p>Передают от Умар ибн Аби Саламе, что когда был ниспослан аят &laquo;Аллах хочет удалить скверну от вас, о Люди дома (Ахл аль-бейт), и очистить вас наилучшим очищением&raquo; (Сура 33, аят 33) в доме Ум Саламы, Посланник Аллаха (с) позвал к себе&nbsp;<span style=\"color: #e65050;\">Фатиму, Хасана и Хусейна</span>, затем накрыл их своей накидкой, а&nbsp;<span style=\"color: #e65050;\">Али</span>&nbsp;был за Пророком, Пророк и его накрыл. После того, как Пророк накрыл их всех накидкой, он изволил сказать следующее: &laquo;О, Аллах, они люди моего дома, очисти же их наилучшим очищением!&raquo;. Ум Салама говорит: &laquo;Я сказала: &laquo;О, Пророк Аллаха! Я тоже с ними?&raquo; Посланник Аллаха изволил сказать: &laquo;Ты оставайся при своем положении, и ты в добре&raquo;.</p>\n\n<p>Передают от Анас Ибн Малика: Поистине посланник Аллах (мир ему и благословение Аллаха!) шесть месяцев проходил мимо&nbsp;<span style=\"color: #e65050;\">двери Фатимы</span>, когда выходил на утреннюю молитву, и говорил: время молитвы, о Ахл аль бейт! \"Аллах хочет удалить скверну от вас, о Люди дома (Ахл аль-бейт), и очистить вас наилучшим очищением\" (Сура 33, аят 33).</p>\n\n<p>Эти два хадиса привели Тирмизи и другие.</p>",
"category_info": {
"id": 1036,
"slug": "reflections",
"title": "Reflections"
},
"address": [
"Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes."
],
"images": [],
"links": [
{
"link": "google.com",
"title": "Первая ссылка Первая ссылка ссылкассы..."
},
{
"link": "google.com",
"title": "Первая ссылка."
},
{
"link": "google.com",
"title": "Первая ссылка."
}
],
"references": [
{
"id": 76,
"book_title": "last book",
"book_authors": [
{
"id": 9,
"name": "sheykh hamid"
}
],
"edition_info": null,
"volume_info": null,
"hadith_number": null,
"volume": "",
"pages": "",
"address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.",
"url": null,
"images": []
}
]
}
]
}

415
apps/hadis/management/commands/export_hadis_sqlite.py

@ -0,0 +1,415 @@
import os
import gzip
import json
import sqlite3
import hashlib
from pathlib import Path
from django.core.management.base import BaseCommand
from django.conf import settings
from django.db import transaction
from apps.hadis.models import (
Hadis,
HadisCategory,
HadisInterpretation,
HadisCorrection,
HadisDatabaseVersion
)
class Command(BaseCommand):
help = "Export Hadis and Categories into an optimized, compressed SQLite database for offline mobile sync."
def add_arguments(self, parser):
parser.add_argument(
'--notes',
type=str,
default='',
help='Release notes for this SQLite database version'
)
parser.add_argument(
'--force-version',
type=int,
default=None,
help='Force a specific version number'
)
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS("Starting Hadis SQLite database generation (Single-File Architecture)..."))
# 1. Determine destination and version number
latest_ver = HadisDatabaseVersion.objects.order_by('-version').first()
if options['force_version'] is not None:
new_version = options['force_version']
else:
new_version = (latest_ver.version + 1) if latest_ver else 1
output_dir = Path(settings.MEDIA_ROOT) / 'hadis_db'
output_dir.mkdir(parents=True, exist_ok=True)
temp_db_path = output_dir / "temp_hadis_build.sqlite"
final_gz_name = "hadis_offline.sqlite.gz"
final_gz_path = output_dir / final_gz_name
if temp_db_path.exists():
temp_db_path.unlink()
# 2. Initialize SQLite database
conn = sqlite3.connect(str(temp_db_path))
cursor = conn.cursor()
# Optimize SQLite for bulk writes
cursor.execute("PRAGMA synchronous = OFF;")
cursor.execute("PRAGMA journal_mode = MEMORY;")
cursor.execute("PRAGMA encoding = 'UTF-8';")
self.stdout.write("Creating SQLite tables and indexes...")
self._create_tables(cursor)
# 3. Export data
categories_count = self._export_categories(cursor)
interp_count = self._export_category_interpretations(cursor)
hadis_count, corr_count = self._export_hadiths_and_corrections(cursor)
conn.commit()
# Create indexes after insertion for maximum performance
self.stdout.write("Building indexes...")
self._create_indexes(cursor)
conn.commit()
conn.close()
# 4. Gzip compress directly to the single target file
self.stdout.write(f"Compressing database to single file: {final_gz_name}...")
hasher = hashlib.md5()
temp_gz_path = output_dir / "temp_hadis_build.sqlite.gz"
with open(temp_db_path, 'rb') as f_in:
with gzip.open(temp_gz_path, 'wb', compresslevel=9) as f_out:
while chunk := f_in.read(1024 * 1024):
f_out.write(chunk)
hasher.update(chunk)
checksum = hasher.hexdigest()
temp_size = temp_db_path.stat().st_size
# Atomic replacement: replace final_gz_path with temp_gz_path
if final_gz_path.exists():
final_gz_path.unlink()
temp_gz_path.rename(final_gz_path)
file_size = final_gz_path.stat().st_size
# Clean up any remaining temp files in output_dir
temp_db_path.unlink(missing_ok=True)
for extra_file in output_dir.iterdir():
if extra_file.name != final_gz_name:
try:
extra_file.unlink(missing_ok=True)
except OSError:
pass
# 5. Record version in database (Single Active Record)
with transaction.atomic():
# Delete old records to keep DB clean
HadisDatabaseVersion.objects.all().delete()
db_version = HadisDatabaseVersion.objects.create(
version=new_version,
file=f"hadis_db/{final_gz_name}",
file_size=file_size,
checksum_md5=checksum,
hadis_count=hadis_count,
categories_count=categories_count,
is_active=True,
notes=options['notes'],
)
self.stdout.write(self.style.SUCCESS(
f"Successfully updated single Hadis SQLite DB to v{new_version}!\n"
f" - Uncompressed size: {temp_size / (1024*1024):.2f} MB\n"
f" - Gzipped size: {file_size / (1024*1024):.2f} MB\n"
f" - Hadiths: {hadis_count}\n"
f" - Categories: {categories_count}\n"
f" - Interpretations: {interp_count}\n"
f" - Corrections: {corr_count}\n"
f" - MD5 Checksum: {checksum}\n"
f" - File path: {final_gz_path}"
))
def _create_tables(self, cursor):
cursor.executescript("""
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY,
title TEXT,
slug TEXT UNIQUE,
source_type TEXT,
sect_type TEXT
);
CREATE TABLE IF NOT EXISTS category_interpretations (
id INTEGER PRIMARY KEY,
category_id INTEGER,
title TEXT,
slug TEXT,
narrator TEXT,
text TEXT,
translation TEXT,
references_json TEXT,
links_json TEXT,
FOREIGN KEY (category_id) REFERENCES categories (id)
);
CREATE TABLE IF NOT EXISTS hadiths (
id INTEGER PRIMARY KEY,
slug TEXT UNIQUE,
category_id INTEGER,
category_slug TEXT,
title TEXT,
title_narrator TEXT,
text TEXT,
translation_json TEXT,
address TEXT,
share_link TEXT,
hadis_status_id INTEGER,
status_title TEXT,
status_color TEXT,
status_main_color_code TEXT,
status_text TEXT,
tags_json TEXT,
references_json TEXT,
reference_images_json TEXT,
narrators_json TEXT,
explanations_json TEXT,
FOREIGN KEY (category_id) REFERENCES categories (id)
);
CREATE TABLE IF NOT EXISTS hadith_corrections (
id INTEGER PRIMARY KEY,
hadis_id INTEGER,
title TEXT,
slug TEXT,
narrator TEXT,
description TEXT,
translation TEXT,
share_link TEXT,
address TEXT,
images_json TEXT,
references_json TEXT,
links_json TEXT,
FOREIGN KEY (hadis_id) REFERENCES hadiths (id)
);
""")
def _create_indexes(self, cursor):
cursor.executescript("""
CREATE INDEX IF NOT EXISTS idx_cat_slug ON categories(slug);
CREATE INDEX IF NOT EXISTS idx_interp_cat ON category_interpretations(category_id);
CREATE INDEX IF NOT EXISTS idx_hadith_cat ON hadiths(category_id);
CREATE INDEX IF NOT EXISTS idx_hadith_slug ON hadiths(slug);
CREATE INDEX IF NOT EXISTS idx_corr_hadis ON hadith_corrections(hadis_id);
""")
def _export_categories(self, cursor):
cats = HadisCategory.objects.all()
rows = [
(
c.id,
json.dumps(c.title, ensure_ascii=False) if isinstance(c.title, (dict, list)) else (c.title or ''),
c.slug,
getattr(c, 'source_type', None),
getattr(c, 'sect_type', None),
)
for c in cats
]
cursor.executemany(
"INSERT INTO categories (id, title, slug, source_type, sect_type) VALUES (?, ?, ?, ?, ?)",
rows
)
return len(rows)
def _export_category_interpretations(self, cursor):
interps = HadisInterpretation.objects.select_related('category').prefetch_related(
'references__book_reference', 'references__images'
).all()
rows = []
for inp in interps:
refs_list = []
for ref in inp.references.all():
book = ref.book_reference
refs_list.append({
'id': ref.id,
'book_title': getattr(book, 'title', None),
'address': getattr(ref, 'address', None),
'edition': getattr(ref, 'edition_id', None),
'volume': getattr(ref, 'volume_id', None),
'pages': getattr(ref, 'pages', None),
'hadith_number': getattr(ref, 'hadith_number', None),
})
rows.append((
inp.id,
inp.category_id,
json.dumps(inp.title, ensure_ascii=False) if isinstance(inp.title, (dict, list)) else (inp.title or ''),
inp.slug,
inp.narrator or '',
inp.text or '',
json.dumps(inp.translation, ensure_ascii=False) if isinstance(inp.translation, (dict, list)) else (inp.translation or ''),
json.dumps(refs_list, ensure_ascii=False),
json.dumps(inp.links or [], ensure_ascii=False),
))
cursor.executemany(
"""INSERT INTO category_interpretations
(id, category_id, title, slug, narrator, text, translation, references_json, links_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
rows
)
return len(rows)
def _export_hadiths_and_corrections(self, cursor):
hadiths = (
Hadis.objects
.filter(status=True)
.select_related('category', 'hadis_status')
.prefetch_related(
'tags',
'references__book_reference__author',
'references__images',
'transmitters__transmitter__reliability',
'transmitters__uncertain_transmitters__reliability',
'transmitters__narrator_layer',
'hadiscorrection_set__references__images',
)
.order_by('id')
)
hadis_rows = []
correction_rows = []
for h in hadiths:
# 1. Status Block
status_id = h.hadis_status.id if h.hadis_status else None
status_title = h.hadis_status.title if h.hadis_status else None
status_color = h.hadis_status.color if h.hadis_status else None
status_code = h.hadis_status.main_color_code if h.hadis_status else None
# 2. Tags
tags_list = [{'id': t.id, 'title': t.title} for t in h.tags.all()]
# 3. References & Reference Images
refs_list = []
ref_imgs_list = []
for ref in h.references.all():
book = ref.book_reference
authors = [{'id': book.author.id, 'name': book.author.name}] if book and book.author else []
refs_list.append({
'id': ref.id,
'title': book.title if book else None,
'authors': authors,
'share_link': getattr(book, 'share_link', None) if book else None,
})
for img in ref.images.all():
ref_imgs_list.append({
'id': img.id,
'thumbnail': img.thumbnail.url if img.thumbnail else None,
'priority': img.priority,
})
# 4. Narrators
transmitters_data = []
for tr in h.transmitters.all():
t = tr.transmitter
rel = t.reliability if t else None
transmitters_data.append({
'id': t.id if t else None,
'name': t.full_name if t else None,
'slug': t.slug if t else None,
'layer_id': tr.narrator_layer_id,
'is_uncertain': tr.is_uncertain,
'reliability': {
'id': rel.id,
'title': rel.title,
'slug': rel.slug,
'color': rel.color,
'main_color_code': rel.main_color_code,
} if rel else None,
})
# 5. Explanations
explanations_data = []
if hasattr(h, 'explanations') and h.explanations:
for exp in h.explanations.all():
explanations_data.append({
'title': exp.title,
'detail': getattr(exp, 'detail', '')
})
hadis_rows.append((
h.id,
h.slug,
h.category_id,
h.category.slug if h.category else None,
json.dumps(h.title, ensure_ascii=False) if isinstance(h.title, (dict, list)) else (h.title or ''),
json.dumps(h.title_narrator, ensure_ascii=False) if isinstance(h.title_narrator, (dict, list)) else (h.title_narrator or ''),
h.text or '',
json.dumps(h.translation or [], ensure_ascii=False),
json.dumps(h.address, ensure_ascii=False) if isinstance(h.address, (dict, list)) else (h.address or ''),
h.share_link or '',
status_id,
json.dumps(status_title, ensure_ascii=False) if isinstance(status_title, (dict, list)) else (status_title or ''),
status_color or '',
status_code or '',
json.dumps(h.hadis_status_text, ensure_ascii=False) if isinstance(h.hadis_status_text, (dict, list)) else (h.hadis_status_text or ''),
json.dumps(tags_list, ensure_ascii=False),
json.dumps(refs_list, ensure_ascii=False),
json.dumps(ref_imgs_list, ensure_ascii=False),
json.dumps({'transmitters': transmitters_data}, ensure_ascii=False),
json.dumps(explanations_data, ensure_ascii=False),
))
# 6. Corrections
for corr in h.hadiscorrection_set.all():
corr_imgs = []
corr_refs = []
for cref in corr.references.all():
for cimg in cref.images.all():
if cimg.image:
corr_imgs.append({'id': cimg.id, 'image': cimg.image.url})
corr_refs.append({
'id': cref.id,
'address': getattr(cref, 'address', None),
})
correction_rows.append((
corr.id,
h.id,
json.dumps(corr.title, ensure_ascii=False) if isinstance(corr.title, (dict, list)) else (corr.title or ''),
corr.slug,
corr.narrator or '',
corr.text or '',
json.dumps(corr.translation, ensure_ascii=False) if isinstance(corr.translation, (dict, list)) else (corr.translation or ''),
corr.share_link or '',
corr.address if hasattr(corr, 'address') else '',
json.dumps(corr_imgs, ensure_ascii=False),
json.dumps(corr_refs, ensure_ascii=False),
json.dumps(corr.links or [], ensure_ascii=False),
))
cursor.executemany(
"""INSERT INTO hadiths (
id, slug, category_id, category_slug, title, title_narrator, text,
translation_json, address, share_link, hadis_status_id, status_title,
status_color, status_main_color_code, status_text, tags_json,
references_json, reference_images_json, narrators_json, explanations_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
hadis_rows
)
cursor.executemany(
"""INSERT INTO hadith_corrections (
id, hadis_id, title, slug, narrator, description, translation,
share_link, address, images_json, references_json, links_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
correction_rows
)
return len(hadis_rows), len(correction_rows)

34
apps/hadis/migrations/0040_hadisdatabaseversion.py

@ -0,0 +1,34 @@
# Generated by Django on 2026-09-10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0039_bookvolume_file'),
]
operations = [
migrations.CreateModel(
name='HadisDatabaseVersion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.PositiveIntegerField(unique=True, verbose_name='نسخه دیتابیس')),
('file', models.FileField(upload_to='hadis_db/', verbose_name='فایل فشرده دیتابیس (.sqlite.gz)')),
('file_size', models.BigIntegerField(default=0, verbose_name='حجم فایل (بایت)')),
('checksum_md5', models.CharField(blank=True, max_length=64, verbose_name='هش MD5')),
('hadis_count', models.PositiveIntegerField(default=0, verbose_name='تعداد احادیث')),
('categories_count', models.PositiveIntegerField(default=0, verbose_name='تعداد دسته‌بندی‌ها')),
('is_active', models.BooleanField(default=True, verbose_name='نسخه فعال')),
('notes', models.TextField(blank=True, verbose_name='یادداشت‌های نسخه')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='آخرین به‌روزرسانی')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='زمان ایجاد')),
],
options={
'verbose_name': 'نسخه دیتابیس SQLite احادیث',
'verbose_name_plural': 'نسخه‌های دیتابیس SQLite احادیث',
'ordering': ['-version'],
},
),
]

19
apps/hadis/models/reference.py

@ -194,9 +194,24 @@ class BookReference(LowercaseSlugMixin, models.Model):
@property
def authors(self):
"""
Backward compatibility helper returning a queryset of authors.
Allows serializers calling obj.authors.all() to function correctly.
Backward compatibility helper returning a queryset/list of authors.
Allows serializers calling obj.authors.all() to function correctly without hitting DB if author is already cached.
"""
if hasattr(self, '_state') and hasattr(self._state, 'fields_cache') and 'author' in self._state.fields_cache:
author_obj = self._state.fields_cache['author']
class InMemAuthorList(list):
def all(self):
return self
def first(self):
return self[0] if self else None
def exists(self):
return bool(self)
def count(self):
return len(self)
def filter(self, *args, **kwargs):
return self
return InMemAuthorList([author_obj] if author_obj else [])
if self.author_id:
return BookAuthor.objects.filter(id=self.author_id)
return BookAuthor.objects.none()

28
apps/hadis/models/version.py

@ -1,5 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
import os
class ContentRelease(models.Model):
version_name = models.CharField(max_length=50, verbose_name=_('Version Name')) # e.g., "v1.2 - Muharram Update"
@ -11,3 +13,29 @@ class ContentRelease(models.Model):
class Meta:
ordering = ['-published_at']
class HadisDatabaseVersion(models.Model):
"""مدل نگهداری نسخه فعلی فایل فشرده SQLite احادیث (Single-File Architecture)"""
version = models.PositiveIntegerField(default=1, verbose_name=_("شماره نسخه دیتابیس"))
file = models.FileField(upload_to='hadis_db/', verbose_name=_("فایل فشرده دیتابیس (.sqlite.gz)"))
file_size = models.BigIntegerField(default=0, verbose_name=_("حجم فایل (بایت)"))
checksum_md5 = models.CharField(max_length=64, blank=True, verbose_name=_("هش MD5"))
hadis_count = models.PositiveIntegerField(default=0, verbose_name=_("تعداد احادیث"))
categories_count = models.PositiveIntegerField(default=0, verbose_name=_("تعداد دسته‌بندی‌ها"))
is_active = models.BooleanField(default=True, verbose_name=_("نسخه فعال"))
notes = models.TextField(blank=True, verbose_name=_("یادداشت‌های نسخه"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("آخرین به‌روزرسانی"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("زمان ایجاد"))
class Meta:
ordering = ['-version']
verbose_name = _("نسخه دیتابیس SQLite احادیث")
verbose_name_plural = _("نسخه‌های دیتابیس SQLite احادیث")
def __str__(self):
return f"Hadis SQLite DB v{self.version} ({self.hadis_count} احادیث)"
@classmethod
def get_latest_active(cls):
return cls.objects.filter(is_active=True).order_by('-version').first()

35
apps/hadis/serializers/hadis.py

@ -128,7 +128,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
model = Hadis
fields = [
# header (no-extend)
'id', 'number', 'slug', 'category_id', 'category_slug', 'title', 'title_narrator', 'text', 'translation',
'id', 'slug', 'category_id', 'category_slug', 'title', 'title_narrator', 'text', 'translation',
# grouped sections
'detail', 'narrators', 'explanations', 'corrections', 'interpretations'
]
@ -170,7 +170,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
{'id': a.id, 'name': get_localized_text(a.name, request)}
for a in authors
],
'description': book.description if book else None,
'description': None,
'share_link': book.share_link if book else None,
})
@ -184,17 +184,9 @@ class HadisSyncSerializer(serializers.ModelSerializer):
'priority': img.priority,
})
# Process address_details
address_details_list = []
if hasattr(obj, 'address_details') and obj.address_details and isinstance(obj.address_details, list):
address_details_list = sorted(
obj.address_details,
key=lambda x: x.get('priority', 0)
)
return {
'address': get_localized_text(obj.address, request),
'address_details': address_details_list,
'address_details': [],
'hadis_status': status_block,
'status_text': get_localized_text(obj.hadis_status_text, request),
'share_link': obj.share_link,
@ -295,18 +287,17 @@ class HadisSyncSerializer(serializers.ModelSerializer):
return explanations_list if explanations_list else None
def get_corrections(self, obj):
"""گرفتن لیست کامل تصحیحات با تمام جزئیات و رفرنس‌ها"""
# استفاده از سریالایزر Detail که در مرحله قبل ساختیم
return HadisCorrectionDetailSerializer(
"""گرفتن لیست تصحیحات بهینه‌شده مخصوص سینک"""
return HadisSyncCorrectionSerializer(
obj.hadiscorrection_set.all(),
many=True,
context=self.context
).data
def get_interpretations(self, obj):
"""گرفتن لیست کامل تفاسیر متصل به دسته‌بندی این حدیث با جزئیات کامل"""
"""گرفتن لیست تفاسیر بهینه‌شده مخصوص سینک"""
if obj.category:
return HadisInterpretationDetailSerializer(
return HadisSyncInterpretationSerializer(
obj.category.interpretations.all(),
many=True,
context=self.context
@ -1737,6 +1728,18 @@ class HadisInterpretationDetailSerializer(serializers.ModelSerializer):
return images_list
class HadisSyncCorrectionSerializer(HadisCorrectionDetailSerializer):
"""نسخه سبک و بهینه تصحیحات مخصوص سینک بدون hadis_info"""
class Meta(HadisCorrectionDetailSerializer.Meta):
fields = ['id', 'title', 'slug', 'narrator', 'description', 'translation', 'share_link', 'address', 'images', 'references', 'links']
class HadisSyncInterpretationSerializer(HadisInterpretationDetailSerializer):
"""نسخه سبک و بهینه تفاسیر مخصوص سینک بدون category_info"""
class Meta(HadisInterpretationDetailSerializer.Meta):
fields = ['id', 'title', 'slug', 'narrator', 'text', 'translation', 'address', 'images', 'links', 'references']
class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer):
title = LocalizedField()
text = LocalizedField()

6
apps/hadis/serializers/reference.py

@ -205,8 +205,10 @@ class BookReferenceSyncSerializer(serializers.ModelSerializer):
def get_detail(self, obj):
"""Get basic book information"""
request = self.context.get('request')
edition = obj.editions.first()
vol = obj.volumes.first()
editions = list(obj.editions.all())
edition = editions[0] if editions else None
volumes = list(obj.volumes.all())
vol = volumes[0] if volumes else None
publisher_data = edition.publisher if edition else []
isbn_data = edition.isbn if edition else ""

42
apps/hadis/serializers/version.py

@ -1,9 +1,38 @@
from rest_framework import serializers
from ..models import ContentRelease
from ..models import ContentRelease, HadisDatabaseVersion
from ..utils.urls import absolute_https_url
class HadisDatabaseVersionSerializer(serializers.ModelSerializer):
"""سریالایزر دریافت آخرین اطلاعات فایل فشرده دیتابیس SQLite جهت دانلود و سینک آفلاین"""
download_url = serializers.SerializerMethodField()
class Meta:
model = HadisDatabaseVersion
fields = [
'version',
'download_url',
'file_size',
'checksum_md5',
'hadis_count',
'categories_count',
'is_active',
'notes',
'created_at',
]
def get_download_url(self, obj):
if obj.file:
request = self.context.get('request')
if request:
return absolute_https_url(obj.file.url, request)
return obj.file.url
return None
class ContentReleaseSyncSerializer(serializers.ModelSerializer):
"""Serializer for syncing content release data for offline mode"""
"""Serializer for syncing content release data for offline mode with SQLite support"""
sqlite_database = serializers.SerializerMethodField()
class Meta:
model = ContentRelease
@ -12,5 +41,12 @@ class ContentReleaseSyncSerializer(serializers.ModelSerializer):
'version_name',
'published_at',
'description',
'is_active'
'is_active',
'sqlite_database',
]
def get_sqlite_database(self, obj):
latest_db = HadisDatabaseVersion.get_latest_active()
if latest_db:
return HadisDatabaseVersionSerializer(latest_db, context=self.context).data
return None

3
apps/hadis/urls.py

@ -11,7 +11,7 @@ from .views.transmitter import (
)
from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView, ReferenceExcerptsListView
from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView
from .views.version import ContentReleaseSyncView
from .views.version import ContentReleaseSyncView, HadisDatabaseVersionView
from .views.info import HadisInfoView
from .views_admin import (
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminCorrectionReferenceViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
@ -90,6 +90,7 @@ urlpatterns = [
path('sync/references/', cached_view(BookReferenceSyncView.as_view()), name='reference-sync'),
path('v2/sync/references/', cached_view(BookReferenceV2SyncView.as_view()), name='reference-sync-v2'),
path('sync/version/', ContentReleaseSyncView.as_view(), name='content-release-sync'),
path('sync/database/', HadisDatabaseVersionView.as_view(), name='hadis-database-sync'),
path('sync/filters/', cached_view(HadisFiltersSyncAPIView.as_view()), name='hadis-filters-sync'),
path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'),

25
apps/hadis/views/hadis.py

@ -208,6 +208,21 @@ class HadisSyncView(ListAPIView):
.order_by('id')
)
# Incremental sync support to prevent transferring 25MB on subsequent syncs
req = getattr(self, 'request', None)
updated_after = (req.query_params.get('updated_after') or req.query_params.get('since')) if req and hasattr(req, 'query_params') else None
if updated_after:
# pyrefly: ignore [missing-import]
from django.utils.dateparse import parse_datetime
try:
dt = parse_datetime(updated_after)
if dt:
qs = qs.filter(updated_at__gte=dt)
except Exception:
pass
return qs
@hadis_sync_swagger
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
@ -403,8 +418,14 @@ class HadisMainListView(ListAPIView):
return self.list(request, *args, **kwargs)
def get_queryset(self):
# queryset = Hadis.objects.select_related('category', 'hadis_status')
queryset = Hadis.objects.select_related('category__sect', 'hadis_status')
queryset = Hadis.objects.select_related('category__sect', 'hadis_status').prefetch_related(
Prefetch(
'references',
queryset=HadisReference.objects.select_related('book_reference__author').prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority'))
)
)
)
# Get search parameters
search_query = self.request.query_params.get('search', None)

2
apps/hadis/views/reference.py

@ -112,7 +112,7 @@ class BookReferenceSyncView(ListAPIView):
Prefetch ALL related data to avoid N+1 queries
"""
qs = BookReference.objects.select_related('author', 'type').prefetch_related(
'attributes', 'images', 'hadis_references__hadis'
'editions', 'volumes', 'attributes', 'images', 'hadis_references__hadis'
).distinct().order_by('id')
# Category (Multi-select)

25
apps/hadis/views/version.py

@ -1,8 +1,9 @@
from rest_framework.generics import RetrieveAPIView
from rest_framework.response import Response
from rest_framework import status
from ..models import ContentRelease
from ..serializers import ContentReleaseSyncSerializer
from ..models import ContentRelease, HadisDatabaseVersion
from ..serializers.version import ContentReleaseSyncSerializer, HadisDatabaseVersionSerializer
from ..docs import content_release_sync_swagger
@ -18,3 +19,23 @@ class ContentReleaseSyncView(RetrieveAPIView):
def get_object(self):
return ContentRelease.objects.filter(is_active=True).order_by('-published_at').first()
class HadisDatabaseVersionView(RetrieveAPIView):
"""
API view to get the latest pre-built SQLite database info and direct Nginx download URL
"""
serializer_class = HadisDatabaseVersionSerializer
def get_object(self):
return HadisDatabaseVersion.get_latest_active()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
if not instance:
return Response(
{"detail": "No active SQLite database version found."},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(instance)
return Response(serializer.data)

25
apps/hadis/views_admin.py

@ -990,6 +990,9 @@ class AdminContentReleaseViewSet(ModelViewSet):
from .signals import invalidate_hadis_cache
invalidate_hadis_cache()
# تریگر خودکار ساخت و فشرده‌سازی دیتابیس SQLite در پس‌زمینه
self._trigger_sqlite_export(notes=f"Auto-generated for version {version_name}")
return Response({
"status": "success",
"message": f"Version incremented to {version_name}",
@ -997,10 +1000,30 @@ class AdminContentReleaseViewSet(ModelViewSet):
"next_suggested_version": compute_next_version(version_name)
})
def _trigger_sqlite_export(self, notes=""):
"""اجرای پس‌زمینه دستور تولید دیتابیس SQLite بدون متوقف کردن پاسخ HTTP"""
import threading
import logging
from django.core.management import call_command
logger = logging.getLogger(__name__)
def _task():
try:
logger.info("Starting background SQLite export...")
call_command('export_hadis_sqlite', notes=notes)
logger.info("Background SQLite export completed successfully.")
except Exception as e:
logger.error(f"Failed to export SQLite in background: {e}", exc_info=True)
thread = threading.Thread(target=_task, daemon=True)
thread.start()
def perform_create(self, serializer):
super().perform_create(serializer)
instance = serializer.save()
from .signals import invalidate_hadis_cache
invalidate_hadis_cache()
self._trigger_sqlite_export(notes=f"Auto-generated for created version {instance.version_name}")
def perform_update(self, serializer):
super().perform_update(serializer)

2
apps/library/views.py

@ -120,7 +120,7 @@ class BookListView(ListAPIView):
return context
def get_queryset(self):
queryset = Book.objects.filter(status=True).select_related('author')
queryset = Book.objects.filter(status=True).select_related('author', 'language')
# Filter by collection if provided
collection_id = self.request.query_params.get('collection_id')

10
apps/podcast/models.py

@ -204,6 +204,16 @@ class PodcastPlaylist(LowercaseSlugMixin, models.Model):
"""
if self.thumbnail:
return self.thumbnail
if hasattr(self, '_prefetched_objects_cache') and 'playlist_items' in self._prefetched_objects_cache:
items = sorted(
[item for item in self.playlist_items.all() if getattr(item, 'podcast', None) and item.podcast.status and item.podcast.thumbnail],
key=lambda x: getattr(x, 'priority', 0)
)
if items and items[0].podcast and items[0].podcast.thumbnail:
return items[0].podcast.thumbnail
return None
first_item = self.playlist_items.filter(
podcast__thumbnail__isnull=False,
podcast__status=True

2
apps/podcast/serializers.py

@ -213,6 +213,8 @@ class PodcastPlaylistListSerializer(serializers.ModelSerializer):
def get_episodes_count(self, obj):
"""Return the number of episodes (podcasts) in this playlist"""
if hasattr(obj, 'episodes_count_annotated'):
return obj.episodes_count_annotated
return obj.playlist_items.filter(podcast__status=True).count()

10
apps/podcast/views.py

@ -1,4 +1,4 @@
from django.db.models import Q, Prefetch
from django.db.models import Q, Prefetch, Count
from rest_framework import generics, status
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action
@ -189,7 +189,13 @@ class PodcastListAPIView(generics.ListAPIView):
queryset = PodcastPlaylist.objects.filter(
status=True,
playlist_items__podcast__status=True
).distinct()
).annotate(
episodes_count_annotated=Count(
'playlist_items',
filter=Q(playlist_items__podcast__status=True),
distinct=True
)
).prefetch_related('playlist_items__podcast').distinct()
# Search by title if search parameter is provided
search_query = self.request.query_params.get('search', None)

10
apps/video/models.py

@ -304,6 +304,16 @@ class VideoPlaylist(LowercaseSlugMixin, models.Model):
"""
if self.thumbnail:
return self.thumbnail
if hasattr(self, '_prefetched_objects_cache') and 'playlist_items' in self._prefetched_objects_cache:
items = sorted(
[item for item in self.playlist_items.all() if getattr(item, 'video', None) and item.video.status and item.video.thumbnail],
key=lambda x: getattr(x, 'priority', 0)
)
if items and items[0].video and items[0].video.thumbnail:
return items[0].video.thumbnail
return None
first_item = self.playlist_items.filter(
video__thumbnail__isnull=False,
video__status=True

2
apps/video/views.py

@ -207,7 +207,7 @@ class VideoPlaylistListAPIView(generics.ListAPIView):
queryset = VideoPlaylist.objects.filter(
status=True,
playlist_items__video__status=True
).distinct().order_by('order', '-created_at')
).prefetch_related('playlist_items__video').distinct().order_by('order', '-created_at')
# Search by title if search parameter is provided
search_query = self.request.query_params.get('search', None)

2
docker-compose.prod.yml

@ -7,7 +7,7 @@ services:
build:
context: .
dockerfile: Dockerfile.prod
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers=2 --threads=2 --max-requests=1000 --max-requests-jitter=100 --timeout 180
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers=4 --threads=4 --worker-class=gthread --max-requests=1000 --max-requests-jitter=100 --timeout 180
volumes:
# - static_volume:/usr/src/app/static
- media_volume:/usr/src/app/media

93
utils/youtube.py

@ -37,53 +37,56 @@ def get_youtube_stream_url(youtube_url: str) -> Optional[str]:
Returns direct stream URL string if successful, or None if extraction fails.
"""
if not youtube_url or not isinstance(youtube_url, str):
return None
youtube_url = youtube_url.strip()
if not youtube_url:
return None
cache_key = _get_cache_key(youtube_url)
cached_url = cache.get(cache_key)
if cached_url:
return cached_url
clean_url = normalize_youtube_url(youtube_url)
try:
import yt_dlp
ydl_opts = {
'format': 'best[ext=mp4]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
'quiet': True,
'no_warnings': True,
'skip_download': True,
'nocheckcertificate': True,
'extractor_args': {
'youtube': {
'player_client': ['android', 'ios', 'web', 'mweb'],
}
}
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(clean_url, download=False)
if not info:
return None
if 'entries' in info:
info = info['entries'][0]
stream_url = info.get('url')
if stream_url:
cache.set(cache_key, stream_url, YOUTUBE_STREAM_CACHE_TTL)
return stream_url
except Exception as e:
logger.error(f"Failed to extract YouTube stream URL for '{youtube_url}': {e}")
# Temporarily disabled - return None without resolving stream URL
return None
# if not youtube_url or not isinstance(youtube_url, str):
# return None
#
# youtube_url = youtube_url.strip()
# if not youtube_url:
# return None
#
# cache_key = _get_cache_key(youtube_url)
# cached_url = cache.get(cache_key)
# if cached_url:
# return cached_url
#
# clean_url = normalize_youtube_url(youtube_url)
#
# try:
# import yt_dlp
#
# ydl_opts = {
# 'format': 'best[ext=mp4]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best',
# 'quiet': True,
# 'no_warnings': True,
# 'skip_download': True,
# 'nocheckcertificate': True,
# 'extractor_args': {
# 'youtube': {
# 'player_client': ['android', 'ios', 'web', 'mweb'],
# }
# }
# }
#
# with yt_dlp.YoutubeDL(ydl_opts) as ydl:
# info = ydl.extract_info(clean_url, download=False)
# if not info:
# return None
#
# if 'entries' in info:
# info = info['entries'][0]
#
# stream_url = info.get('url')
# if stream_url:
# cache.set(cache_key, stream_url, YOUTUBE_STREAM_CACHE_TTL)
# return stream_url
# except Exception as e:
# logger.error(f"Failed to extract YouTube stream URL for '{youtube_url}': {e}")
#
# return None
def clear_youtube_stream_cache(youtube_url: str) -> None:
"""

Loading…
Cancel
Save