add mam integration backend

This commit is contained in:
Leandro Zazzi
2025-03-10 21:08:16 +01:00
parent 3876731a6b
commit 2bab24de57
5 changed files with 252 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
import json
import logging
from datetime import datetime
from typing import Any, Literal, Optional, Dict
from urllib.parse import urlencode, urljoin
from aiohttp import ClientSession
from sqlmodel import Session
from app.internal.models import (
TorrentSource,
ProwlarrSource,
)
from app.util.cache import SimpleCache, StringConfigCache
logger = logging.getLogger(__name__)
class MamMisconfigured(ValueError):
pass
MamConfigKey = Literal[
"mam_session_id",
"mam_source_ttl",
"mam_active"
]
class MamConfig(StringConfigCache[MamConfigKey]):
def raise_if_invalid(self, session: Session):
if not self.get_session_id(session):
raise MamMisconfigured("mam_id not set")
def is_valid(self, session: Session) -> bool:
return (
self.get_session_id(session) is not None and self.get_session_id(session)!=""
)
def get_session_id(self, session: Session) -> Optional[str]:
return self.get(session, "mam_session_id")
def set_mam_id(self, session: Session, mam_id: str):
self.set(session, "mam_session_id", mam_id)
def get_source_ttl(self, session: Session) -> int:
return self.get_int(session, "mam_source_ttl", 24 * 60 * 60)
def set_source_ttl(self, session: Session, source_ttl: int):
self.set_int(session, "mam_source_ttl", source_ttl)
def is_active(self, session: Session) -> bool:
return self.get(session, "mam_active")=="True"
def set_active(self, session: Session, state: bool):
self.set(session, "mam_active", str(state))
mam_config = MamConfig()
mam_source_cache = SimpleCache[dict[str, TorrentSource]]()
def flush_Mam_cache():
mam_source_cache.flush()
# Downloading is still handled via prowlarr.
async def query_mam(
session: Session,
query: Optional[str],
force_refresh: bool = False,
) -> dict[str, TorrentSource]:
if not query:
return {}
base_url = "https://www.myanonamouse.net"
session_id = mam_config.get_session_id(session)
assert session_id is not None
if not force_refresh:
source_ttl = mam_config.get_source_ttl(session)
cached_sources = mam_source_cache.get(source_ttl, query)
if cached_sources:
return cached_sources
params: dict[str, Any] = {
"text": query, # book title + author(s)
"perpage": 100,
"tor": {
"main_cat": {13}, # 13 is the audiobook category on mam
"searchIn": "torrents",
"searchType": "active", # retrieve only torrents with at least 1 seed.
"srchIn": {
"title": "true",
"author": "true",
},
},
"startNumber": 0 #offset
}
url = urljoin(base_url, f"/tor/js/loadSearchJSONbasic.php?{urlencode(params, doseq=True)}")
logger.info("Querying Mam: %s", url)
async with ClientSession() as client_session:
async with client_session.get(
url,
cookies={"mam_id":mam_config.get_session_id}
) as response:
search_results = await response.json()
sources : Dict[str,TorrentSource] = {}
for result in search_results:
# TODO reduce to just authors / narrator unless there is a use for the other data.
sources.update({
f'https://www.myanonamouse.net/t/{result["id"]}':
TorrentSource(
protocol="torrent",
guid=f'https://www.myanonamouse.net/t/{result["id"]}',
indexer_id=-1, # We don't know MAM's id within prowlarr.
indexer="MyAnonamouse",
title=result["title"],
seeders=result.get("seeders", 0),
leechers=result.get("leechers", 0),
size=-1,
info_url=f'https://www.myanonamouse.net/t/{result["id"]}',
indexer_flags=["freeleech"] if result["personal_freeleech"]==1 else [], # TODO add differentiate between freeleech and VIP freeleech availible flags in result: [free, fl_vip, personal_freeleech]
publish_date=datetime.fromisoformat(result["added"]),
authors=list(json.load(result["author_info"]).values()),
narrators=list(json.load(result["narrator_info"]).values())
)
}
)
mam_source_cache.set(sources, query)
return sources
def inject_mam_metadata(prowlarrData: list[ProwlarrSource], mamData: Dict[str,TorrentSource]) -> list[ProwlarrSource]:
for p in prowlarrData:
m =mamData.get(p.guid)
if m is None:
continue
p.authors= m.authors
p.narrators = m.narrators
return prowlarrData
+2
View File
@@ -127,6 +127,8 @@ class BaseSource(BaseModel):
indexer_id: int
indexer: str
title: str
authors: list[str] = Field(default_factory=list, sa_column=Column(JSON))
narrators: list[str] = Field(default_factory=list, sa_column=Column(JSON))
size: int # in bytes
publish_date: datetime
info_url: str
+15
View File
@@ -12,6 +12,12 @@ from app.internal.prowlarr.prowlarr import (
query_prowlarr,
start_download,
)
from app.internal.mam.mam import (
mam_config,
query_mam,
inject_mam_metadata
)
from app.internal.ranking.download_ranking import rank_sources
querying: set[str] = set()
@@ -61,6 +67,15 @@ async def query_sources(
query,
force_refresh=force_refresh,
)
if mam_config.is_active(session):
mam_config.raise_if_invalid(session)
mam_sources = await query_mam(
session,
query,
force_refresh=force_refresh,
)
sources = inject_mam_metadata(sources,mam_sources)
ranked = await rank_sources(session, client_session, sources, book)
+39
View File
@@ -10,6 +10,8 @@ from app.internal.models import EventEnum, GroupEnum, Notification, User
from app.internal.prowlarr.indexer_categories import indexer_categories
from app.internal.notifications import send_notification
from app.internal.prowlarr.prowlarr import flush_prowlarr_cache, prowlarr_config
from app.internal.mam.mam import mam_config
from app.internal.ranking.quality import IndexerFlag, QualityRange, quality_config
from app.util.auth import (
DetailedUser,
@@ -264,6 +266,8 @@ def read_prowlarr(
prowlarr_base_url = prowlarr_config.get_base_url(session)
prowlarr_api_key = prowlarr_config.get_api_key(session)
selected = set(prowlarr_config.get_categories(session))
mam_is_active = mam_config.is_active(session)
mam_id = mam_config.get_session_id(session)
return template_response(
"settings_page/prowlarr.html",
@@ -276,6 +280,9 @@ def read_prowlarr(
"indexer_categories": indexer_categories,
"selected_categories": selected,
"prowlarr_misconfigured": True if prowlarr_misconfigured else False,
"mam_active": mam_is_active,
"mam_id": mam_id,
},
)
@@ -329,6 +336,38 @@ def update_indexer_categories(
block_name="category",
)
@router.put("/mam/mam_id")
def update_mam_id(
mam_id: Annotated[str, Form()],
session: Annotated[Session, Depends(get_session)],
admin_user: Annotated[
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
],
):
mam_config.set_mam_id(session, mam_id)
return Response(status_code=204, headers={"HX-Refresh": "true"})
@router.put("/mam/activate")
def activate_mam(
session: Annotated[Session, Depends(get_session)],
admin_user: Annotated[
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
],
):
mam_config.set_active(session, True)
return Response(status_code=204, headers={"HX-Refresh": "true"})
@router.put("/mam/deactivate")
def deactivate_mam(
session: Annotated[Session, Depends(get_session)],
admin_user: Annotated[
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
],
):
mam_config.set_active(session, False)
return Response(status_code=204, headers={"HX-Refresh": "true"})
@router.get("/download")
def read_download(
+49
View File
@@ -111,5 +111,54 @@
</button>
</form>
{% endblock %}
<h2 class="text-lg">MyAnonamouse integration</h2>
<!-- {% if mam_misconfigured %}
<p class="text-red-400">MAM is misconfigured. Please configure it.</p>
{% endif %} -->
{% if mam_active %}
<form
class="join w-full"
hx-put="/settings/mam/deactivate"
>
<!-- prettier-ignore -->
<button id="mam-toggle-button" class="join-item btn">
Disable mam integration
</button>
</form>
<label for="mam-id">mam_id</label>
<form
class="join w-full"
hx-put="/settings/mam/mam_id"
hx-disabled-elt="#mam-id-button"
>
<!-- prettier-ignore -->
<input
id="mam-id"
name="mam_id"
type="password"
{% if mam_id %}placeholder="●●●●●●●●●●●●●●●●●"{% endif %}
class="input join-item w-full"
minlength="1"
required
/>
<button id="mam-id-button" class="join-item btn">
{% if mam_id %} Update {% else %} Add {% endif %}
</button>
</form>
{% else %}
<form
class="join w-full"
hx-put="/settings/mam/activate"
hx-disabled-elt="#mam-id-button"
>
<!-- prettier-ignore -->
<button id="mam-toggle-button" class="join-item btn">
Activate mam integration
</button>
</form>
{% endif %}
</div>
{% endblock %}