biblia-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- biblia_cli-0.1.0/.env.example +5 -0
- biblia_cli-0.1.0/.gitignore +32 -0
- biblia_cli-0.1.0/PKG-INFO +69 -0
- biblia_cli-0.1.0/README.md +59 -0
- biblia_cli-0.1.0/biblia_cli/__init__.py +1 -0
- biblia_cli-0.1.0/biblia_cli/annotations.py +70 -0
- biblia_cli-0.1.0/biblia_cli/api/__init__.py +0 -0
- biblia_cli-0.1.0/biblia_cli/api/bolls_client.py +43 -0
- biblia_cli-0.1.0/biblia_cli/app.py +254 -0
- biblia_cli-0.1.0/biblia_cli/book_names.py +86 -0
- biblia_cli-0.1.0/biblia_cli/cache.py +22 -0
- biblia_cli-0.1.0/biblia_cli/config.py +17 -0
- biblia_cli-0.1.0/biblia_cli/css/app.tcss +59 -0
- biblia_cli-0.1.0/biblia_cli/favorites.py +29 -0
- biblia_cli-0.1.0/biblia_cli/notes.py +41 -0
- biblia_cli-0.1.0/biblia_cli/pipe_mode.py +69 -0
- biblia_cli-0.1.0/biblia_cli/settings.py +12 -0
- biblia_cli-0.1.0/biblia_cli/themes.py +4 -0
- biblia_cli-0.1.0/biblia_cli/widgets/__init__.py +0 -0
- biblia_cli-0.1.0/biblia_cli/widgets/annotation_modal.py +127 -0
- biblia_cli-0.1.0/biblia_cli/widgets/favorites_modal.py +36 -0
- biblia_cli-0.1.0/biblia_cli/widgets/note_modal.py +39 -0
- biblia_cli-0.1.0/biblia_cli/widgets/search_modal.py +52 -0
- biblia_cli-0.1.0/biblia_cli/widgets/splash_screen.py +104 -0
- biblia_cli-0.1.0/biblia_cli/widgets/translation_modal.py +26 -0
- biblia_cli-0.1.0/main.py +14 -0
- biblia_cli-0.1.0/pyproject.toml +18 -0
- biblia_cli-0.1.0/setup_supabase.sql +13 -0
- biblia_cli-0.1.0/~/.pypirc +4 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Secretos — NUNCA al repo
|
|
2
|
+
.env
|
|
3
|
+
admin_annotations.py
|
|
4
|
+
cargar_anotaciones.py
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.pyc
|
|
9
|
+
*.pyo
|
|
10
|
+
*.pyd
|
|
11
|
+
*.egg-info/
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
.eggs/
|
|
15
|
+
|
|
16
|
+
# Datos locales del usuario
|
|
17
|
+
.biblia-cli/
|
|
18
|
+
|
|
19
|
+
# Entornos virtuales
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
|
|
24
|
+
# IDEs
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
*.suo
|
|
28
|
+
*.user
|
|
29
|
+
|
|
30
|
+
# Windows
|
|
31
|
+
Thumbs.db
|
|
32
|
+
desktop.ini
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: biblia-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 📖 Lee la Biblia en tu terminal — Castellano & Português
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx>=0.27.0
|
|
7
|
+
Requires-Dist: supabase>=2.0.0
|
|
8
|
+
Requires-Dist: textual>=0.70.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# ⚡ BIBLIA CLI: CRISTO Edition ⚡
|
|
12
|
+
|
|
13
|
+
> **La Palabra de Dios no solo se lee, se vive. Y ahora, se domina desde la Terminal.**
|
|
14
|
+
|
|
15
|
+
Bienvenido a **Biblia CLI**, la herramienta definitiva para el guerrero espiritual moderno y el desarrollador que no sacrifica su fe por su productividad. Olvida las interfaces lentas, las distracciones de la web y el ruido visual. Esto es velocidad pura. Esto es **CRISTO** en tu consola.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 ¿Por qué Biblia CLI?
|
|
20
|
+
|
|
21
|
+
Biblia CLI no es un simple lector; es un motor de búsqueda y estudio bíblico optimizado para el rendimiento. Diseñado con una estética **Premium**, ofrece una experiencia de usuario que te hará volar la cabeza.
|
|
22
|
+
|
|
23
|
+
### ✨ Características que rompen moldes:
|
|
24
|
+
|
|
25
|
+
* **⚡ Velocidad de Vértigo**: Acceso instantáneo a cualquier libro y capítulo. Navegación fluida diseñada para los que no tienen tiempo que perder.
|
|
26
|
+
* **📖 Sincronización Litúrgica Real**: Pulsa `D` y conecta directamente con el **calendario litúrgico oficial del Vaticano**. No es una lectura aleatoria, es la Palabra que la Iglesia proclama en todo el mundo, hoy mismo.
|
|
27
|
+
* **🎨 Estética de Vanguardia**: Temas visuales impecables. Desde el modo **Sepia** para lecturas profundas hasta el **Matrix** para sesiones de código nocturnas, y un **Tema Claro** refinado con alto contraste que se ve simplemente increíble.
|
|
28
|
+
* **🛰️ Conexión Bolls.life**: Acceso a la API más robusta del mercado para garantizar que los textos siempre estén ahí cuando los necesites con la máxima fidelidad.
|
|
29
|
+
* **⭐ Gestión de Favoritos y Notas**: Marca tus versículos clave y guarda tus reflexiones directamente en la terminal. Tu estudio bíblico, centralizado y potente.
|
|
30
|
+
* **🕊️ CRISTO Splash Screen**: Una pantalla de inicio en arte ASCII que impone respeto y te recuerda el propósito de tu estudio desde el segundo uno.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🛠️ Instalación para Elegidos
|
|
35
|
+
|
|
36
|
+
Si tienes Python 3.10+, estás a un comando de la gloria:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
pip install -e .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Una vez instalado, simplemente escribe:
|
|
43
|
+
```powershell
|
|
44
|
+
biblia
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ⌨️ Domina el Arca (Atajos de Teclado)
|
|
50
|
+
|
|
51
|
+
| Tecla | Acción |
|
|
52
|
+
| :--- | :--- |
|
|
53
|
+
| `D` | **Sincronización Divina**: Abre la lectura litúrgica de HOY. |
|
|
54
|
+
| `/` | **Buscador Ninja**: Encuentra cualquier pasaje en milisegundos. |
|
|
55
|
+
| `T` | **Camaleón**: Cambia entre temas visuales épicos. |
|
|
56
|
+
| `F` | **Favoritos**: Tus versículos para la eternidad. |
|
|
57
|
+
| `N` | **Apostolado**: Añade tus notas personales. |
|
|
58
|
+
| `Q` | **Salir**: Hasta la próxima sesión de gloria. |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🔥 El Futuro es Ahora
|
|
63
|
+
|
|
64
|
+
Biblia CLI no es solo software, es una declaración de intenciones. Es la unión de la tecnología más puntera con la sabiduría más antigua. **Rápido. Elegante. Infalible.**
|
|
65
|
+
|
|
66
|
+
> "Lámpara es a mis pies tu palabra, Y lumbrera a mi camino." — Salmos 119:105
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
© 2026 - Biblia CLI Project. Built with love for the Glory of Christ, made in Katowice, Poland, born in Basque Country.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ⚡ BIBLIA CLI: CRISTO Edition ⚡
|
|
2
|
+
|
|
3
|
+
> **La Palabra de Dios no solo se lee, se vive. Y ahora, se domina desde la Terminal.**
|
|
4
|
+
|
|
5
|
+
Bienvenido a **Biblia CLI**, la herramienta definitiva para el guerrero espiritual moderno y el desarrollador que no sacrifica su fe por su productividad. Olvida las interfaces lentas, las distracciones de la web y el ruido visual. Esto es velocidad pura. Esto es **CRISTO** en tu consola.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 ¿Por qué Biblia CLI?
|
|
10
|
+
|
|
11
|
+
Biblia CLI no es un simple lector; es un motor de búsqueda y estudio bíblico optimizado para el rendimiento. Diseñado con una estética **Premium**, ofrece una experiencia de usuario que te hará volar la cabeza.
|
|
12
|
+
|
|
13
|
+
### ✨ Características que rompen moldes:
|
|
14
|
+
|
|
15
|
+
* **⚡ Velocidad de Vértigo**: Acceso instantáneo a cualquier libro y capítulo. Navegación fluida diseñada para los que no tienen tiempo que perder.
|
|
16
|
+
* **📖 Sincronización Litúrgica Real**: Pulsa `D` y conecta directamente con el **calendario litúrgico oficial del Vaticano**. No es una lectura aleatoria, es la Palabra que la Iglesia proclama en todo el mundo, hoy mismo.
|
|
17
|
+
* **🎨 Estética de Vanguardia**: Temas visuales impecables. Desde el modo **Sepia** para lecturas profundas hasta el **Matrix** para sesiones de código nocturnas, y un **Tema Claro** refinado con alto contraste que se ve simplemente increíble.
|
|
18
|
+
* **🛰️ Conexión Bolls.life**: Acceso a la API más robusta del mercado para garantizar que los textos siempre estén ahí cuando los necesites con la máxima fidelidad.
|
|
19
|
+
* **⭐ Gestión de Favoritos y Notas**: Marca tus versículos clave y guarda tus reflexiones directamente en la terminal. Tu estudio bíblico, centralizado y potente.
|
|
20
|
+
* **🕊️ CRISTO Splash Screen**: Una pantalla de inicio en arte ASCII que impone respeto y te recuerda el propósito de tu estudio desde el segundo uno.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🛠️ Instalación para Elegidos
|
|
25
|
+
|
|
26
|
+
Si tienes Python 3.10+, estás a un comando de la gloria:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Una vez instalado, simplemente escribe:
|
|
33
|
+
```powershell
|
|
34
|
+
biblia
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ⌨️ Domina el Arca (Atajos de Teclado)
|
|
40
|
+
|
|
41
|
+
| Tecla | Acción |
|
|
42
|
+
| :--- | :--- |
|
|
43
|
+
| `D` | **Sincronización Divina**: Abre la lectura litúrgica de HOY. |
|
|
44
|
+
| `/` | **Buscador Ninja**: Encuentra cualquier pasaje en milisegundos. |
|
|
45
|
+
| `T` | **Camaleón**: Cambia entre temas visuales épicos. |
|
|
46
|
+
| `F` | **Favoritos**: Tus versículos para la eternidad. |
|
|
47
|
+
| `N` | **Apostolado**: Añade tus notas personales. |
|
|
48
|
+
| `Q` | **Salir**: Hasta la próxima sesión de gloria. |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🔥 El Futuro es Ahora
|
|
53
|
+
|
|
54
|
+
Biblia CLI no es solo software, es una declaración de intenciones. Es la unión de la tecnología más puntera con la sabiduría más antigua. **Rápido. Elegante. Infalible.**
|
|
55
|
+
|
|
56
|
+
> "Lámpara es a mis pies tu palabra, Y lumbrera a mi camino." — Salmos 119:105
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
© 2026 - Biblia CLI Project. Built with love for the Glory of Christ, made in Katowice, Poland, born in Basque Country.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anotaciones del autor — cifradas en Supabase, descifradas en memoria.
|
|
3
|
+
|
|
4
|
+
Flujo:
|
|
5
|
+
· En Supabase: texto cifrado con Fernet (ilegible sin la clave)
|
|
6
|
+
· En la app: se descifra al vuelo con ANN_ENCRYPT_KEY de settings.py
|
|
7
|
+
· En disco: nada se guarda
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_chapter_cache: dict[str, dict[int, str]] = {}
|
|
14
|
+
_LOCAL_DRAFT = Path.home() / ".biblia-cli" / "ann_drafts.json"
|
|
15
|
+
|
|
16
|
+
def _prefix(t, b, c): return f"{t}/{b}/{c}"
|
|
17
|
+
def _key(t, b, c, v): return f"{t}/{b}/{c}/{v}"
|
|
18
|
+
|
|
19
|
+
def _client():
|
|
20
|
+
from .settings import SUPABASE_URL, SUPABASE_ANON_KEY
|
|
21
|
+
from supabase import create_client
|
|
22
|
+
return create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
|
|
23
|
+
|
|
24
|
+
def _decrypt(text: str) -> str:
|
|
25
|
+
try:
|
|
26
|
+
from cryptography.fernet import Fernet
|
|
27
|
+
from .settings import ANN_ENCRYPT_KEY
|
|
28
|
+
return Fernet(ANN_ENCRYPT_KEY.encode()).decrypt(text.encode()).decode()
|
|
29
|
+
except Exception:
|
|
30
|
+
return text # si falla el descifrado devuelve el texto tal cual
|
|
31
|
+
|
|
32
|
+
# ── Supabase (lectura + descifrado) ──────────────────────────────────────────
|
|
33
|
+
async def get_chapter(translation: str, book_id: int, chapter: int) -> dict[int, str]:
|
|
34
|
+
key = _prefix(translation, book_id, chapter)
|
|
35
|
+
if key in _chapter_cache: return _chapter_cache[key]
|
|
36
|
+
try:
|
|
37
|
+
res = _client().table("annotations") \
|
|
38
|
+
.select("verse, text") \
|
|
39
|
+
.eq("translation", translation) \
|
|
40
|
+
.eq("book_id", book_id) \
|
|
41
|
+
.eq("chapter", chapter) \
|
|
42
|
+
.execute()
|
|
43
|
+
result = {r["verse"]: _decrypt(r["text"]) for r in (res.data or [])}
|
|
44
|
+
except Exception:
|
|
45
|
+
result = {}
|
|
46
|
+
_chapter_cache[key] = result
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
def invalidate(translation, book_id, chapter):
|
|
50
|
+
_chapter_cache.pop(_prefix(translation, book_id, chapter), None)
|
|
51
|
+
|
|
52
|
+
# ── Borrador local (modal de escritura in-app) ────────────────────────────────
|
|
53
|
+
def _load_drafts():
|
|
54
|
+
if _LOCAL_DRAFT.exists():
|
|
55
|
+
try: return json.loads(_LOCAL_DRAFT.read_text(encoding="utf-8"))
|
|
56
|
+
except Exception: pass
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
def _save_drafts(data):
|
|
60
|
+
_LOCAL_DRAFT.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
_LOCAL_DRAFT.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
62
|
+
|
|
63
|
+
def get_note_local(translation, book_id, chapter, verse) -> str | None:
|
|
64
|
+
return _load_drafts().get(_key(translation, book_id, chapter, verse))
|
|
65
|
+
|
|
66
|
+
def save_local(translation, book_id, chapter, verse, text):
|
|
67
|
+
data = _load_drafts(); data[_key(translation, book_id, chapter, verse)] = text; _save_drafts(data)
|
|
68
|
+
|
|
69
|
+
def delete_local(translation, book_id, chapter, verse):
|
|
70
|
+
data = _load_drafts(); data.pop(_key(translation, book_id, chapter, verse), None); _save_drafts(data)
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import re, httpx
|
|
2
|
+
from .. import cache as local_cache
|
|
3
|
+
|
|
4
|
+
BASE = "https://bolls.life"
|
|
5
|
+
TIMEOUT = 15.0
|
|
6
|
+
|
|
7
|
+
TRANSLATIONS = {
|
|
8
|
+
"Español 🇪🇸": [("RV1960", "Reina-Valera 1960")],
|
|
9
|
+
"Português 🇧🇷": [("ARA", "Almeida Revista e Atualizada")],
|
|
10
|
+
}
|
|
11
|
+
PT_CODES = {"ARA","ARC09","NVIPT","NTLH","NVT","NAA","OL","KJA","CNBB","TB10","ACF11","NTJud","VFL","NBV07","ALM21"}
|
|
12
|
+
|
|
13
|
+
def _clean(text):
|
|
14
|
+
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
|
|
15
|
+
return re.sub(r"<[^>]+>", "", text).strip()
|
|
16
|
+
|
|
17
|
+
class BollsClient:
|
|
18
|
+
async def get_books(self, translation):
|
|
19
|
+
async with httpx.AsyncClient(timeout=TIMEOUT) as c:
|
|
20
|
+
r = await c.get(f"{BASE}/get-books/{translation}/"); r.raise_for_status()
|
|
21
|
+
return r.json()
|
|
22
|
+
|
|
23
|
+
async def get_chapter(self, translation, book_id, chapter):
|
|
24
|
+
cached = local_cache.load(translation, book_id, chapter)
|
|
25
|
+
if cached: return cached
|
|
26
|
+
async with httpx.AsyncClient(timeout=TIMEOUT) as c:
|
|
27
|
+
r = await c.get(f"{BASE}/get-text/{translation}/{book_id}/{chapter}/"); r.raise_for_status()
|
|
28
|
+
verses = r.json()
|
|
29
|
+
for v in verses: v["text"] = _clean(v["text"])
|
|
30
|
+
local_cache.save(translation, book_id, chapter, verses)
|
|
31
|
+
return verses
|
|
32
|
+
|
|
33
|
+
async def search(self, translation, query, page=1, limit=50):
|
|
34
|
+
async with httpx.AsyncClient(timeout=TIMEOUT) as c:
|
|
35
|
+
r = await c.get(f"{BASE}/v2/find/{translation}", params={"search":query,"page":page,"limit":limit})
|
|
36
|
+
r.raise_for_status(); data = r.json()
|
|
37
|
+
for res in data.get("results",[]): res["text"] = _clean(res["text"])
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
async def get_random_verse(self, translation):
|
|
41
|
+
async with httpx.AsyncClient(timeout=TIMEOUT) as c:
|
|
42
|
+
r = await c.get(f"{BASE}/get-random-verse/{translation}/"); r.raise_for_status()
|
|
43
|
+
v = r.json(); v["text"] = _clean(v["text"]); return v
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from textual import on, work
|
|
3
|
+
import httpx
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Footer, Header, Input, Label, ListItem, ListView, Static
|
|
10
|
+
from .api.bolls_client import BollsClient
|
|
11
|
+
from .book_names import get_books_for_lang, lang_for_translation
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .favorites import toggle as fav_toggle, is_favorite
|
|
14
|
+
from .notes import get_chapter_notes
|
|
15
|
+
from .annotations import get_chapter as ann_get_chapter
|
|
16
|
+
from .themes import next_theme, LABELS
|
|
17
|
+
from .widgets.annotation_modal import AnnotationReadModal, AnnotationWriteModal
|
|
18
|
+
from .widgets.favorites_modal import FavoritesModal
|
|
19
|
+
from .widgets.note_modal import NoteModal
|
|
20
|
+
from .widgets.search_modal import SearchModal
|
|
21
|
+
from .widgets.splash_screen import SplashScreen
|
|
22
|
+
from .widgets.translation_modal import TranslationModal
|
|
23
|
+
|
|
24
|
+
class BibliaApp(App):
|
|
25
|
+
CSS_PATH = "css/app.tcss"
|
|
26
|
+
ENABLE_COMMAND_PALETTE = False
|
|
27
|
+
BINDINGS = [
|
|
28
|
+
Binding("q", "quit", "Salir"),
|
|
29
|
+
Binding("t", "switch_lang", "[T] Idioma"),
|
|
30
|
+
Binding("ctrl+t", "cycle_theme", "[^T] Tema"),
|
|
31
|
+
Binding("/", "open_search", "[/] Buscar"),
|
|
32
|
+
Binding("ctrl+f", "toggle_fav", "[^F] ★"),
|
|
33
|
+
Binding("f", "open_favs", "[F] Marcadores"),
|
|
34
|
+
Binding("n", "add_note", "[N] Nota"),
|
|
35
|
+
Binding("h", "read_ann", "[H] ✦ Leer"),
|
|
36
|
+
Binding("ctrl+h", "write_ann", "[^H] ✦ Escribir"),
|
|
37
|
+
Binding("d", "daily", "[D] Lectura hoy"),
|
|
38
|
+
Binding("escape", "clear_filter", "Limpiar"),
|
|
39
|
+
]
|
|
40
|
+
translation: reactive[str] = reactive("RV1960")
|
|
41
|
+
biblia_theme: reactive[str] = reactive("dark")
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.cfg = Config(); self.client = BollsClient()
|
|
46
|
+
self.books: list[dict] = []; self.book: dict | None = None
|
|
47
|
+
self.chapter: int = 1; self._verses_cache: list[dict] = []
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
yield Header(show_clock=True)
|
|
51
|
+
with Horizontal(id="main"):
|
|
52
|
+
with Vertical(id="books-panel"):
|
|
53
|
+
yield Label("📚 LIBROS", classes="panel-title")
|
|
54
|
+
yield Input(placeholder="🔍 Filtrar...", id="books-filter")
|
|
55
|
+
yield ListView(id="books-list")
|
|
56
|
+
with Vertical(id="chapters-panel"):
|
|
57
|
+
yield Label("📖 CAP.", classes="panel-title")
|
|
58
|
+
yield ListView(id="chapters-list")
|
|
59
|
+
with VerticalScroll(id="scripture-panel"):
|
|
60
|
+
yield Label("", id="scripture-title")
|
|
61
|
+
yield Label("", id="fav-indicator")
|
|
62
|
+
yield Static("", id="scripture-content", markup=True)
|
|
63
|
+
yield Footer()
|
|
64
|
+
|
|
65
|
+
def on_mount(self):
|
|
66
|
+
self.push_screen(SplashScreen())
|
|
67
|
+
self._start()
|
|
68
|
+
|
|
69
|
+
def _start(self, _=None):
|
|
70
|
+
s = self.cfg.load(); self.translation = s["translation"]; self.biblia_theme = s.get("theme","dark")
|
|
71
|
+
self._set_title()
|
|
72
|
+
self.load_books(s.get("last_book_id"), s.get("last_chapter",1))
|
|
73
|
+
|
|
74
|
+
def _set_title(self):
|
|
75
|
+
self.title = f"📖 Biblia CLI · {self.translation} · {LABELS[self.biblia_theme]}"
|
|
76
|
+
|
|
77
|
+
def watch_biblia_theme(self, t: str):
|
|
78
|
+
self._apply_theme(t)
|
|
79
|
+
|
|
80
|
+
def on_screen_resume(self) -> None:
|
|
81
|
+
self._apply_theme(self.biblia_theme)
|
|
82
|
+
|
|
83
|
+
def _apply_theme(self, t):
|
|
84
|
+
for o in [self, self.screen]:
|
|
85
|
+
for c in list(o.classes):
|
|
86
|
+
if c.startswith("theme-"): o.remove_class(c)
|
|
87
|
+
o.add_class(f"theme-{t}")
|
|
88
|
+
@work(exclusive=True)
|
|
89
|
+
async def load_books(self, restore_id=None, restore_ch=1):
|
|
90
|
+
bl = self.query_one("#books-list", ListView); bl.clear()
|
|
91
|
+
try: self.books = await self.client.get_books(self.translation)
|
|
92
|
+
except Exception:
|
|
93
|
+
self.books = get_books_for_lang(lang_for_translation(self.translation))
|
|
94
|
+
self.notify("Sin conexión — lista offline", timeout=3)
|
|
95
|
+
seen = set()
|
|
96
|
+
for b in self.books:
|
|
97
|
+
bid = b["bookid"]
|
|
98
|
+
if bid in seen: continue
|
|
99
|
+
seen.add(bid)
|
|
100
|
+
li = ListItem(Label(b["name"]))
|
|
101
|
+
li.book_id = bid
|
|
102
|
+
bl.append(li)
|
|
103
|
+
if restore_id:
|
|
104
|
+
tgt = next((b for b in self.books if b["bookid"]==restore_id), None)
|
|
105
|
+
if tgt: self.book=tgt; self._fill_chapters(tgt["chapters"],restore_ch); self.load_scripture()
|
|
106
|
+
|
|
107
|
+
def _fill_chapters(self, total, highlight=1):
|
|
108
|
+
cl = self.query_one("#chapters-list", ListView); cl.clear()
|
|
109
|
+
for i in range(1, total+1): cl.append(ListItem(Label(str(i))))
|
|
110
|
+
if highlight <= total: self.chapter=highlight; cl.index=highlight-1
|
|
111
|
+
|
|
112
|
+
@work(exclusive=True)
|
|
113
|
+
async def load_scripture(self):
|
|
114
|
+
if not self.book: return
|
|
115
|
+
tw=self.query_one("#scripture-title",Label); cw=self.query_one("#scripture-content",Static); fw=self.query_one("#fav-indicator",Label)
|
|
116
|
+
tw.update("⏳ Cargando..."); cw.update(""); fw.update("")
|
|
117
|
+
try: raw = await self.client.get_chapter(self.translation, self.book["bookid"], self.chapter)
|
|
118
|
+
except Exception as e: tw.update(f"[red]❌ {e}[/red]"); return
|
|
119
|
+
self._verses_cache = raw
|
|
120
|
+
verses = raw
|
|
121
|
+
notes = get_chapter_notes(self.translation, self.book["bookid"], self.chapter)
|
|
122
|
+
ann = await ann_get_chapter(self.translation, self.book["bookid"], self.chapter)
|
|
123
|
+
tw.update(f"[bold #d4a017]{self.book['name']}[/bold #d4a017] [bold cyan]Cap. {self.chapter}[/bold cyan] [dim]{self.translation}[/dim]")
|
|
124
|
+
fw.update("[bold #d4a017]⭐ Marcado[/bold #d4a017]" if is_favorite(self.translation,self.book["bookid"],self.chapter) else "")
|
|
125
|
+
lines=[]
|
|
126
|
+
for v in verses:
|
|
127
|
+
vn=v["verse"]
|
|
128
|
+
note_icon = " [bold #d4a017]✏[/bold #d4a017]" if vn in notes else ""
|
|
129
|
+
ann_marker = " [bold #aaaaaa]***[/bold #aaaaaa]" if vn in ann else ""
|
|
130
|
+
lines.append(f"[bold cyan]{vn:>3}[/bold cyan]{note_icon} {v['text']}{ann_marker}")
|
|
131
|
+
if vn in notes:
|
|
132
|
+
for nl in notes[vn].splitlines(): lines.append(f" [italic #d4a017]│ {nl}[/italic #d4a017]")
|
|
133
|
+
lines.append("")
|
|
134
|
+
cw.update("\n".join(lines))
|
|
135
|
+
self.cfg.save({"translation":self.translation,"last_book_id":self.book["bookid"],"last_chapter":self.chapter,"theme":self.biblia_theme})
|
|
136
|
+
|
|
137
|
+
@on(ListView.Selected,"#books-list")
|
|
138
|
+
def on_book(self,event):
|
|
139
|
+
if event.item and hasattr(event.item, "book_id"):
|
|
140
|
+
bid = event.item.book_id
|
|
141
|
+
self.book=next((b for b in self.books if b["bookid"]==bid),None)
|
|
142
|
+
if self.book: self._fill_chapters(self.book["chapters"]); self.chapter=1; self.load_scripture(); self.query_one("#chapters-list",ListView).focus()
|
|
143
|
+
|
|
144
|
+
@on(ListView.Selected,"#chapters-list")
|
|
145
|
+
def on_chapter(self,_):
|
|
146
|
+
idx=self.query_one("#chapters-list",ListView).index
|
|
147
|
+
if idx is not None: self.chapter=idx+1; self.load_scripture()
|
|
148
|
+
|
|
149
|
+
@on(Input.Changed,"#books-filter")
|
|
150
|
+
def filter_books(self,event):
|
|
151
|
+
q=event.value.strip().lower(); bl=self.query_one("#books-list",ListView); bl.clear()
|
|
152
|
+
seen = set()
|
|
153
|
+
for b in self.books:
|
|
154
|
+
bid = b["bookid"]
|
|
155
|
+
if q in b["name"].lower() and bid not in seen:
|
|
156
|
+
seen.add(bid)
|
|
157
|
+
li = ListItem(Label(b["name"]))
|
|
158
|
+
li.book_id = bid
|
|
159
|
+
bl.append(li)
|
|
160
|
+
|
|
161
|
+
def action_switch_lang(self): self.push_screen(TranslationModal(),self._on_trans)
|
|
162
|
+
def _on_trans(self,t):
|
|
163
|
+
if t: self.translation=t; self.cfg.save({"translation":t}); self._set_title(); self.load_books()
|
|
164
|
+
def action_cycle_theme(self):
|
|
165
|
+
self.biblia_theme=next_theme(self.biblia_theme); self._set_title(); self.cfg.save({"theme":self.biblia_theme}); self.notify(f"Tema: {LABELS[self.biblia_theme]}",timeout=1.5)
|
|
166
|
+
def action_open_search(self):
|
|
167
|
+
if self.books: self.push_screen(SearchModal(self.translation,self.books),self._on_search)
|
|
168
|
+
def _on_search(self,r):
|
|
169
|
+
if r:
|
|
170
|
+
bid,ch=r; b=next((x for x in self.books if x["bookid"]==bid),None)
|
|
171
|
+
if b: self.book=b; self.chapter=ch; self._fill_chapters(b["chapters"],ch); self.load_scripture()
|
|
172
|
+
def action_toggle_fav(self):
|
|
173
|
+
if not self.book: self.notify("Abre un capítulo primero.",timeout=2); return
|
|
174
|
+
added=fav_toggle(self.translation,self.book["bookid"],self.book["name"],self.chapter)
|
|
175
|
+
self.notify("⭐ Marcado" if added else "★ Eliminado",timeout=2)
|
|
176
|
+
self.query_one("#fav-indicator",Label).update("[bold #d4a017]⭐ Marcado[/bold #d4a017]" if added else "")
|
|
177
|
+
def action_open_favs(self): self.push_screen(FavoritesModal(),self._on_fav)
|
|
178
|
+
def _on_fav(self,fav):
|
|
179
|
+
if fav:
|
|
180
|
+
b=next((x for x in self.books if x["bookid"]==fav["book_id"]),None)
|
|
181
|
+
if b: self.book=b; self.chapter=fav["chapter"]; self._fill_chapters(b["chapters"],fav["chapter"]); self.load_scripture()
|
|
182
|
+
def action_add_note(self):
|
|
183
|
+
if not self.book: self.notify("Abre un capítulo primero.",timeout=2); return
|
|
184
|
+
self.push_screen(_VP(self.book["name"],self.chapter,"✏️ Nota en"),self._open_note)
|
|
185
|
+
def _open_note(self,v):
|
|
186
|
+
if v: self.push_screen(NoteModal(self.translation,self.book["bookid"],self.book["name"],self.chapter,v),lambda c: self.load_scripture() if c else None)
|
|
187
|
+
def action_read_ann(self):
|
|
188
|
+
if not self.book: self.notify("Abre un capítulo primero.",timeout=2); return
|
|
189
|
+
self.push_screen(_VP(self.book["name"],self.chapter,"✦ Ver nota de"),self._open_ann_read)
|
|
190
|
+
def _open_ann_read(self,verse):
|
|
191
|
+
if verse: self._do_read_ann(verse)
|
|
192
|
+
@work
|
|
193
|
+
async def _do_read_ann(self,verse:int):
|
|
194
|
+
ann=await ann_get_chapter(self.translation,self.book["bookid"],self.chapter)
|
|
195
|
+
if verse in ann: self.push_screen(AnnotationReadModal(self.book["name"],self.chapter,verse,ann[verse]))
|
|
196
|
+
else: self.notify(f"No hay nota ✦ en el versículo {verse}.",timeout=2)
|
|
197
|
+
def action_write_ann(self):
|
|
198
|
+
if not self.book: self.notify("Abre un capítulo primero.",timeout=2); return
|
|
199
|
+
self.push_screen(_VP(self.book["name"],self.chapter,"✦ Nota de autor en"),self._open_ann_write)
|
|
200
|
+
def _open_ann_write(self,verse):
|
|
201
|
+
if verse: self.push_screen(AnnotationWriteModal(self.translation,self.book["bookid"],self.book["name"],self.chapter,verse),lambda c: self.load_scripture() if c else None)
|
|
202
|
+
def action_daily(self): self._do_daily()
|
|
203
|
+
@work(exclusive=True)
|
|
204
|
+
async def _do_daily(self):
|
|
205
|
+
if not self.books: return
|
|
206
|
+
self.notify("⏳ Obteniendo lectura litúrgica...", timeout=2)
|
|
207
|
+
try:
|
|
208
|
+
import json, re
|
|
209
|
+
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as c:
|
|
210
|
+
r = await c.get("https://universalis.com/Europe.Spain/jsonpmass.js")
|
|
211
|
+
r.raise_for_status()
|
|
212
|
+
txt = r.text
|
|
213
|
+
mj = re.search(r"universalisCallback\((.*)\);", txt, re.DOTALL)
|
|
214
|
+
if not mj: raise Exception("Error de formato")
|
|
215
|
+
data = json.loads(mj.group(1))
|
|
216
|
+
src = data.get("Mass_R1",{}).get("source","")
|
|
217
|
+
if not src: src = data.get("Mass_G",{}).get("source","")
|
|
218
|
+
m = re.search(r"^([1-3]?\s?[a-zA-Z\s]+)\s+(\d+)", src)
|
|
219
|
+
if not m: raise Exception(f"No parseable: {src}")
|
|
220
|
+
bname, ch = m.group(1).strip(), int(m.group(2))
|
|
221
|
+
from .book_names import resolve_book
|
|
222
|
+
bid = resolve_book(bname)
|
|
223
|
+
if not bid: raise Exception(f"No reconozco {bname}")
|
|
224
|
+
tgt = next((b for b in self.books if b["bookid"]==bid),None)
|
|
225
|
+
if not tgt: raise Exception(f"{bname} no disponible")
|
|
226
|
+
self.book=tgt; self.chapter=ch; self._fill_chapters(tgt["chapters"],ch); self.load_scripture()
|
|
227
|
+
self.notify(f"📖 {src}", timeout=3)
|
|
228
|
+
except Exception as e: self.notify(f"Error lectura: {e}", severity="error")
|
|
229
|
+
def action_clear_filter(self):
|
|
230
|
+
f=self.query_one("#books-filter",Input)
|
|
231
|
+
if f.value: f.value=""; self.filter_books(Input.Changed(f,""))
|
|
232
|
+
else: self.query_one("#books-list",ListView).focus()
|
|
233
|
+
|
|
234
|
+
class _VP(ModalScreen):
|
|
235
|
+
DEFAULT_CSS="""
|
|
236
|
+
_VP { align: center middle; }
|
|
237
|
+
#vp-box { width: 46; height: auto; background: $surface; border: thick $primary; padding: 1 2; }
|
|
238
|
+
#vp-title { text-style: bold; color: $accent; text-align: center; width: 100%; margin-bottom: 1; }
|
|
239
|
+
#vp-foot { height: 3; margin-top: 1; align: right middle; }
|
|
240
|
+
"""
|
|
241
|
+
def __init__(self,book_name,chapter,label): super().__init__(); self._label=f"{label} {book_name} {chapter}"
|
|
242
|
+
def compose(self) -> ComposeResult:
|
|
243
|
+
with Vertical(id="vp-box"):
|
|
244
|
+
yield Label(self._label,id="vp-title"); yield Input(placeholder="Número de versículo...",id="vp-input")
|
|
245
|
+
with Horizontal(id="vp-foot"): yield Button("Aceptar",id="vp-ok",variant="primary"); yield Button("Cancelar",id="vp-no")
|
|
246
|
+
def on_mount(self): self.query_one("#vp-input",Input).focus()
|
|
247
|
+
@on(Button.Pressed,"#vp-ok")
|
|
248
|
+
@on(Input.Submitted,"#vp-input")
|
|
249
|
+
def accept(self,_=None):
|
|
250
|
+
val=self.query_one("#vp-input",Input).value.strip(); self.dismiss(int(val) if val.isdigit() else None)
|
|
251
|
+
@on(Button.Pressed,"#vp-no")
|
|
252
|
+
def cancel(self): self.dismiss(None)
|
|
253
|
+
|
|
254
|
+
def main(): BibliaApp().run()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import unicodedata
|
|
2
|
+
from .api.bolls_client import PT_CODES
|
|
3
|
+
|
|
4
|
+
def _n(s):
|
|
5
|
+
return "".join(c for c in unicodedata.normalize("NFD",s.lower()) if unicodedata.category(c)!="Mn")
|
|
6
|
+
|
|
7
|
+
BOOKS = [
|
|
8
|
+
(1,"Génesis","Gênesis",50),(2,"Éxodo","Êxodo",40),(3,"Levítico","Levítico",27),
|
|
9
|
+
(4,"Números","Números",36),(5,"Deuteronomio","Deuteronômio",34),(6,"Josué","Josué",24),
|
|
10
|
+
(7,"Jueces","Juízes",21),(8,"Rut","Rute",4),(9,"1 Samuel","1 Samuel",31),
|
|
11
|
+
(10,"2 Samuel","2 Samuel",24),(11,"1 Reyes","1 Reis",22),(12,"2 Reyes","2 Reis",25),
|
|
12
|
+
(13,"1 Crónicas","1 Crônicas",29),(14,"2 Crónicas","2 Crônicas",36),
|
|
13
|
+
(15,"Esdras","Esdras",10),(16,"Nehemías","Neemias",13),(17,"Ester","Ester",10),
|
|
14
|
+
(18,"Job","Jó",42),(19,"Salmos","Salmos",150),(20,"Proverbios","Provérbios",31),
|
|
15
|
+
(21,"Eclesiastés","Eclesiastes",12),(22,"Cantares","Cantares",8),
|
|
16
|
+
(23,"Isaías","Isaías",66),(24,"Jeremías","Jeremias",52),
|
|
17
|
+
(25,"Lamentaciones","Lamentações",5),(26,"Ezequiel","Ezequiel",48),
|
|
18
|
+
(27,"Daniel","Daniel",12),(28,"Oseas","Oséias",14),(29,"Joel","Joel",3),
|
|
19
|
+
(30,"Amós","Amós",9),(31,"Abdías","Obadias",1),(32,"Jonás","Jonas",4),
|
|
20
|
+
(33,"Miqueas","Miquéias",7),(34,"Nahúm","Naum",3),(35,"Habacuc","Habacuque",3),
|
|
21
|
+
(36,"Sofonías","Sofonias",3),(37,"Hageo","Ageu",2),(38,"Zacarías","Zacarias",14),
|
|
22
|
+
(39,"Malaquías","Malaquias",4),(40,"Mateo","Mateus",28),(41,"Marcos","Marcos",16),
|
|
23
|
+
(42,"Lucas","Lucas",24),(43,"Juan","João",21),(44,"Hechos","Atos",28),
|
|
24
|
+
(45,"Romanos","Romanos",16),(46,"1 Corintios","1 Coríntios",16),
|
|
25
|
+
(47,"2 Corintios","2 Coríntios",13),(48,"Gálatas","Gálatas",6),
|
|
26
|
+
(49,"Efesios","Efésios",6),(50,"Filipenses","Filipenses",4),
|
|
27
|
+
(51,"Colosenses","Colossenses",4),(52,"1 Tesalonicenses","1 Tessalonicenses",5),
|
|
28
|
+
(53,"2 Tesalonicenses","2 Tessalonicenses",3),(54,"1 Timoteo","1 Timóteo",6),
|
|
29
|
+
(55,"2 Timoteo","2 Timóteo",4),(56,"Tito","Tito",3),(57,"Filemón","Filemom",1),
|
|
30
|
+
(58,"Hebreos","Hebreus",13),(59,"Santiago","Tiago",5),(60,"1 Pedro","1 Pedro",5),
|
|
31
|
+
(61,"2 Pedro","2 Pedro",3),(62,"1 Juan","1 João",5),(63,"2 Juan","2 João",1),
|
|
32
|
+
(64,"3 Juan","3 João",1),(65,"Judas","Judas",1),(66,"Apocalipsis","Apocalipse",22),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def get_books_for_lang(lang):
|
|
36
|
+
return [{"bookid":b,"name":es if lang=="es" else pt,"chapters":ch} for b,es,pt,ch in BOOKS]
|
|
37
|
+
|
|
38
|
+
def lang_for_translation(code):
|
|
39
|
+
return "pt" if code in PT_CODES else "es"
|
|
40
|
+
|
|
41
|
+
_ES, _PT = {}, {}
|
|
42
|
+
for _bid,_nes,_npt,_ in BOOKS:
|
|
43
|
+
_ES[_n(_nes)]=_bid; _PT[_n(_npt)]=_bid
|
|
44
|
+
|
|
45
|
+
_ES.update({"gn":1,"gen":1,"ex":2,"lv":3,"lev":3,"nm":4,"num":4,"dt":5,"jos":6,"jue":7,"rt":8,
|
|
46
|
+
"1sm":9,"1sa":9,"2sm":10,"2sa":10,"1re":11,"2re":12,"1cr":13,"2cr":14,"esd":15,"ne":16,
|
|
47
|
+
"neh":16,"est":17,"jb":18,"sal":19,"sl":19,"ps":19,"pr":20,"prov":20,"ec":21,"ecl":21,
|
|
48
|
+
"ct":22,"is":23,"isa":23,"jr":24,"jer":24,"lm":25,"ez":26,"dn":27,"dan":27,"os":28,
|
|
49
|
+
"jl":29,"am":30,"ab":31,"jon":32,"mi":33,"na":34,"hab":35,"sof":36,"sf":36,"hag":37,
|
|
50
|
+
"zac":38,"mal":39,"mt":40,"mc":41,"mr":41,"lc":42,"jn":43,"hch":44,"act":44,"ro":45,
|
|
51
|
+
"rom":45,"1co":46,"2co":47,"gl":48,"ga":48,"ef":49,"fp":50,"col":51,"1ts":52,"2ts":53,
|
|
52
|
+
"1tm":54,"1ti":54,"2tm":55,"2ti":55,"tt":56,"flm":57,"he":58,"heb":58,"stg":59,"sg":59,
|
|
53
|
+
"1pe":60,"2pe":61,"1jn":62,"1j":62,"2jn":63,"3jn":64,"jud":65,"ap":66,"apoc":66})
|
|
54
|
+
_PT.update({"gn":1,"ex":2,"lv":3,"nm":4,"dt":5,"js":6,"jz":7,"rt":8,"1sm":9,"2sm":10,
|
|
55
|
+
"1rs":11,"2rs":12,"1cr":13,"2cr":14,"ed":15,"ne":16,"et":17,"sl":19,"pv":20,"ec":21,
|
|
56
|
+
"ct":22,"is":23,"jr":24,"lm":25,"ez":26,"dn":27,"os":28,"jl":29,"am":30,"ob":31,
|
|
57
|
+
"mq":33,"na":34,"hc":35,"sf":36,"ag":37,"zc":38,"ml":39,"mt":40,"mc":41,"lc":42,
|
|
58
|
+
"jo":43,"at":44,"rm":45,"1co":46,"2co":47,"gl":48,"ef":49,"fp":50,"cl":51,"1ts":52,
|
|
59
|
+
"2ts":53,"1tm":54,"2tm":55,"tt":56,"fm":57,"hb":58,"tg":59,"1pe":60,"2pe":61,
|
|
60
|
+
"1jo":62,"2jo":63,"3jo":64,"jd":65,"ap":66})
|
|
61
|
+
|
|
62
|
+
_EN = {"gn":1,"gen":1,"ex":2,"lv":3,"lev":3,"nm":4,"num":4,"dt":5,"jos":6,"jue":7,"rt":8,
|
|
63
|
+
"1sm":9,"1sa":9,"2sm":10,"2sa":10,"1re":11,"2re":12,"1cr":13,"2cr":14,"esd":15,"ne":16,
|
|
64
|
+
"est":17,"jb":18,"sal":19,"sl":19,"ps":19,"pr":20,"prov":20,"ec":21,"ecl":21,
|
|
65
|
+
"ct":22,"is":23,"isa":23,"jr":24,"jer":24,"lm":25,"ez":26,"dn":27,"dan":27,"os":28,
|
|
66
|
+
"jl":29,"am":30,"ab":31,"jon":32,"mi":33,"na":34,"hab":35,"sof":36,"sf":36,"hag":37,
|
|
67
|
+
"zac":38,"mal":39,"mt":40,"mc":41,"mr":41,"lc":42,"jn":43,"hch":44,"act":44,"ro":45,
|
|
68
|
+
"rom":45,"1co":46,"2co":47,"gl":48,"ga":48,"ef":49,"fp":50,"col":51,"1ts":52,"2ts":53,
|
|
69
|
+
"1tm":54,"1ti":54,"2tm":55,"2ti":55,"tt":56,"flm":57,"he":58,"heb":58,"stg":59,"sg":59,
|
|
70
|
+
"1pe":60,"2pe":61,"1jn":62,"1j":62,"2jn":63,"3jn":64,"jud":65,"ap":66,"apoc":66,
|
|
71
|
+
"genesis":1,"exodus":2,"leviticus":3,"numbers":4,"deuteronomy":5,"joshua":6,"judges":7,"ruth":8,
|
|
72
|
+
"1 samuel":9,"2 samuel":10,"1 kings":11,"2 kings":12,"1 chronicles":13,"2 chronicles":14,
|
|
73
|
+
"ezra":15,"nehemiah":16,"esther":17,"job":18,"psalms":19,"proverbs":20,"ecclesiastes":21,
|
|
74
|
+
"song of songs":22,"isaiah":23,"jeremiah":24,"lamentations":25,"ezekiel":26,"daniel":27,
|
|
75
|
+
"hosea":28,"joel":29,"amos":30,"obadiah":31,"jonah":32,"micah":33,"nahum":34,"habakkuk":35,
|
|
76
|
+
"zephaniah":36,"haggai":37,"zechariah":38,"malachi":39,"matthew":40,"mark":41,"luke":42,
|
|
77
|
+
"john":43,"acts":44,"romans":45,"1 corinthians":46,"2 corinthians":47,"galatians":48,
|
|
78
|
+
"ephesians":49,"philippians":50,"colossians":51,"1 thessalonians":52,"2 thessalonians":53,
|
|
79
|
+
"1 timothy":54,"2 timothy":55,"titus":56,"philemon":57,"hebrews":58,"james":59,"1 peter":60,
|
|
80
|
+
"2 peter":61,"1 john":62,"2 john":63,"3 john":64,"jude":65,"revelation":66}
|
|
81
|
+
|
|
82
|
+
def resolve_book(name, lang="es"):
|
|
83
|
+
n = _n(name.strip())
|
|
84
|
+
if lang == "es": return _ES.get(n) or _EN.get(n)
|
|
85
|
+
if lang == "pt": return _PT.get(n) or _EN.get(n)
|
|
86
|
+
return _EN.get(n)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
CACHE_DIR = Path.home() / ".biblia-cli" / "cache"
|
|
5
|
+
|
|
6
|
+
def _p(t, b, c): return CACHE_DIR / t / f"{b}_{c}.json"
|
|
7
|
+
|
|
8
|
+
def load(translation, book_id, chapter):
|
|
9
|
+
p = _p(translation, book_id, chapter)
|
|
10
|
+
if p.exists():
|
|
11
|
+
try: return json.loads(p.read_text(encoding="utf-8"))
|
|
12
|
+
except Exception: pass
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
def save(translation, book_id, chapter, verses):
|
|
16
|
+
p = _p(translation, book_id, chapter)
|
|
17
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
p.write_text(json.dumps(verses, ensure_ascii=False), encoding="utf-8")
|
|
19
|
+
|
|
20
|
+
def stats():
|
|
21
|
+
if not CACHE_DIR.exists(): return {}
|
|
22
|
+
return {d.name: len(list(d.glob("*.json"))) for d in CACHE_DIR.iterdir() if d.is_dir()}
|