Compare commits
7 Commits
claude/med
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8bef94e7f | ||
|
|
e4c696d142 | ||
|
|
7cc5fcb514 | ||
|
|
dbaa7b0d8b | ||
|
|
d22fb6ae10 | ||
|
|
8d7a66c431 | ||
|
|
6812758af0 |
14
README.md
14
README.md
@@ -30,22 +30,30 @@ cp .env.example .env
|
|||||||
|
|
||||||
Edit `.env` with your actual values (see [Environment Variables](#environment-variables) below).
|
Edit `.env` with your actual values (see [Environment Variables](#environment-variables) below).
|
||||||
|
|
||||||
### 2. Verify your configuration
|
### 2. Set up a virtual environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify your configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
python -m app --check
|
python -m app --check
|
||||||
```
|
```
|
||||||
|
|
||||||
This validates that all required environment variables are set, the Matrix homeserver is reachable, the bot token is valid, the bot has joined the announcements room, and the database path is writable. Fix any failing checks before starting the server.
|
This validates that all required environment variables are set, the Matrix homeserver is reachable, the bot token is valid, the bot has joined the announcements room, and the database path is writable. Fix any failing checks before starting the server.
|
||||||
|
|
||||||
### 3. Run locally
|
### 4. Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Run with Docker
|
### 5. Run with Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t melora .
|
docker build -t melora .
|
||||||
|
|||||||
@@ -9,17 +9,27 @@ def _strip_markdown(text: str) -> str:
|
|||||||
return text.replace("**", "").replace("*", "").replace("> ", "")
|
return text.replace("**", "").replace("*", "").replace("> ", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_quality(quality_obj) -> str:
|
||||||
|
"""Extract quality name from *arr quality field, which may be a string or nested dict."""
|
||||||
|
if isinstance(quality_obj, str):
|
||||||
|
return quality_obj
|
||||||
|
if isinstance(quality_obj, dict):
|
||||||
|
inner = quality_obj.get("quality", quality_obj)
|
||||||
|
if isinstance(inner, str):
|
||||||
|
return inner
|
||||||
|
if isinstance(inner, dict):
|
||||||
|
return inner.get("name", "Unknown")
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def format_radarr(payload: dict) -> tuple[str, str]:
|
def format_radarr(payload: dict) -> tuple[str, str]:
|
||||||
movie = payload.get("movie", {})
|
movie = payload.get("movie", {})
|
||||||
title = movie.get("title", "Unknown")
|
title = movie.get("title", "Unknown")
|
||||||
year = movie.get("year", "")
|
year = movie.get("year", "")
|
||||||
is_upgrade = payload.get("isUpgrade", False)
|
is_upgrade = payload.get("isUpgrade", False)
|
||||||
|
|
||||||
quality = (
|
quality = _extract_quality(
|
||||||
payload.get("movieFile", {})
|
payload.get("movieFile", {}).get("quality", {})
|
||||||
.get("quality", {})
|
|
||||||
.get("quality", {})
|
|
||||||
.get("name", "Unknown")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_upgrade:
|
if is_upgrade:
|
||||||
@@ -52,11 +62,8 @@ def format_sonarr(payload: dict) -> tuple[str, str]:
|
|||||||
episode = ep.get("episodeNumber", 0)
|
episode = ep.get("episodeNumber", 0)
|
||||||
ep_title = ep.get("title", "")
|
ep_title = ep.get("title", "")
|
||||||
|
|
||||||
quality = (
|
quality = _extract_quality(
|
||||||
payload.get("episodeFile", {})
|
payload.get("episodeFile", {}).get("quality", {})
|
||||||
.get("quality", {})
|
|
||||||
.get("quality", {})
|
|
||||||
.get("name", "Unknown")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
header = f"\U0001f4fa **{series_title}** \u2014 S{season:02d}E{episode:02d}"
|
header = f"\U0001f4fa **{series_title}** \u2014 S{season:02d}E{episode:02d}"
|
||||||
@@ -91,11 +98,7 @@ def format_lidarr(payload: dict) -> tuple[str, str]:
|
|||||||
|
|
||||||
track_files = payload.get("trackFiles", [{}])
|
track_files = payload.get("trackFiles", [{}])
|
||||||
tf = track_files[0] if track_files else {}
|
tf = track_files[0] if track_files else {}
|
||||||
quality = (
|
quality = _extract_quality(tf.get("quality", {}))
|
||||||
tf.get("quality", {})
|
|
||||||
.get("quality", {})
|
|
||||||
.get("name", "Unknown")
|
|
||||||
)
|
|
||||||
|
|
||||||
header = f"\U0001f3b5 **{artist_name}**"
|
header = f"\U0001f3b5 **{artist_name}**"
|
||||||
if album_title:
|
if album_title:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from nio import AsyncClient, RoomSendResponse
|
from nio import AsyncClient, PresenceSetError, RoomSendResponse
|
||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
|
|
||||||
@@ -38,23 +38,28 @@ async def close_client() -> None:
|
|||||||
pass
|
pass
|
||||||
_presence_task = None
|
_presence_task = None
|
||||||
if _client is not None:
|
if _client is not None:
|
||||||
await _set_presence("offline")
|
try:
|
||||||
|
await _set_presence("offline")
|
||||||
|
except Exception:
|
||||||
|
log.warning("Failed to set offline presence during shutdown", exc_info=True)
|
||||||
await _client.close()
|
await _client.close()
|
||||||
_client = None
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
async def _set_presence(state: str) -> None:
|
async def _set_presence(state: str) -> None:
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
try:
|
resp = await client.set_presence(state)
|
||||||
await client.set_presence(state)
|
if isinstance(resp, PresenceSetError):
|
||||||
except Exception:
|
raise RuntimeError(f"Presence update to '{state}' failed: {resp.message}")
|
||||||
log.debug("Presence update failed", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def _presence_loop() -> None:
|
async def _presence_loop() -> None:
|
||||||
while True:
|
while True:
|
||||||
await _set_presence("online")
|
|
||||||
await asyncio.sleep(PRESENCE_INTERVAL_SECONDS)
|
await asyncio.sleep(PRESENCE_INTERVAL_SECONDS)
|
||||||
|
try:
|
||||||
|
await _set_presence("online")
|
||||||
|
except Exception:
|
||||||
|
log.warning("Presence heartbeat failed", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
async def start_presence() -> None:
|
async def start_presence() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user