mobile friendly + update readme

This commit is contained in:
Markbeep
2025-02-22 17:47:53 +01:00
parent 5ef22bb847
commit eb0ac838a6
7 changed files with 115 additions and 29 deletions

View File

@@ -1,8 +1,5 @@
# AudioBookRequest
> [!CAUTION]
> This project is in its very early stages. It's basically a weekend project at this state. There might be some bugs or unfinished parts.
Your tool for handling audiobook requests on a Plex/AudioBookShelf/Jellyfin instance.
If you've heard of Overseer, Ombi, or Jellyseer; this is in the similar vein, <ins>but for audiobooks</ins>.
@@ -11,9 +8,10 @@ If you've heard of Overseer, Ombi, or Jellyseer; this is in the similar vein, <i
# Workflow
1. Admin creates user accounts for their friends. Each account's group is one of: `Admin`, `Trusted`, and `Untrusted`. All groups can request/remove book requests. Trusted and above can have downloads automatically start when they select a book in the search tab. This requires the download client to be set up correctly in Prowlarr.
2. The requests/wishlist page shows a list of all books that have been requested. An admin can directly view the torrent sources gotten from Prowlarr and start any download.
3. Settings page allows for admins to create new accounts and set the required Prowlarr configs.
1. Admin creates user accounts for their friends. Each account's group is one of: `Admin`, `Trusted`, and `Untrusted`. All groups can request/remove book requests.
2. When a Trusted and above requests a book, it'll automatically start downloading. This requires the download client to be set up correctly in Prowlarr and the "Auto Download" option to be on in the settings.
3. If configured, a notification will be sent to Apprise.
4. The requests/wishlist page shows a list of all books that have been requested. An admin can directly view the torrent sources gotten from Prowlarr and start downloading requests or reject them.
# Docker

View File

@@ -31,7 +31,7 @@ from app.util.book_search import (
list_audible_books,
)
from app.util.connection import get_connection
from app.util.notifications import send_notification
from app.util.notifications import send_manual_notification, send_notification
from app.util.ranking.quality import quality_config
from app.util.templates import template_response
@@ -188,6 +188,7 @@ async def add_manual(
request: Request,
user: Annotated[DetailedUser, Depends(get_authenticated_user())],
session: Annotated[Session, Depends(get_session)],
background_task: BackgroundTasks,
title: Annotated[str, Form()],
author: Annotated[str, Form()],
narrator: Annotated[Optional[str], Form()] = None,
@@ -205,8 +206,21 @@ async def add_manual(
additional_info=info,
)
session.add(book_request)
session.flush()
session.expunge_all() # so that we can pass down the object without the session
session.commit()
notifications = session.exec(
select(Notification).where(Notification.event == EventEnum.on_new_request)
).all()
for notif in notifications:
background_task.add_task(
send_manual_notification,
notification=notif,
book=book_request,
requester_username=user.username,
)
return template_response(
"manual.html",
request,

View File

@@ -3,7 +3,38 @@ from aiohttp import ClientSession
from sqlmodel import select
from app.db import open_session
from app.models import BookRequest, Notification
from app.models import BookRequest, ManualBookRequest, Notification
def create_title_body(
title_template: str,
body_template: str,
username: Optional[str] = None,
book_title: Optional[str] = None,
book_authors: Optional[str] = None,
book_narrators: Optional[str] = None,
event_type: Optional[str] = None,
):
title = title_template
body = body_template
if username:
title = title.replace("{eventUser}", username)
body = body.replace("{eventUser}", username)
if book_title:
title = title.replace("{bookTitle}", book_title)
body = body.replace("{bookTitle}", book_title)
if book_authors:
title = title.replace("{bookAuthors}", book_authors)
body = body.replace("{bookAuthors}", book_authors)
if book_narrators:
title = title.replace("{bookNarrators}", book_narrators)
body = body.replace("{bookNarrators}", book_narrators)
if event_type:
title = title.replace("{eventType}", event_type)
body = body.replace("{eventType}", event_type)
return title, body
async def send_notification(
@@ -13,27 +44,27 @@ async def send_notification(
):
with open_session() as session:
async with ClientSession() as client_session:
title = notification.title_template
body = notification.body_template
if requester_username:
title = title.replace("{eventUser}", requester_username)
body = body.replace("{eventUser}", requester_username)
book_title = None
book_authors = None
book_narrators = None
if book_asin:
book = session.exec(
select(BookRequest).where(BookRequest.asin == book_asin)
).first()
if book:
title = title.replace("{bookTitle}", book.title)
body = body.replace("{bookTitle}", book.title)
title = title.replace("{bookAuthors}", ",".join(book.authors))
body = body.replace("{bookAuthors}", ",".join(book.authors))
title = title.replace("{bookNarrators}", ",".join(book.narrators))
body = body.replace("{bookNarrators}", ",".join(book.narrators))
book_title = book.title
book_authors = ",".join(book.authors)
book_narrators = ",".join(book.narrators)
title = title.replace("{eventType}", notification.event.value)
body = body.replace("{eventType}", notification.event.value)
title, body = create_title_body(
notification.title_template,
notification.body_template,
requester_username,
book_title,
book_authors,
book_narrators,
notification.event.value,
)
async with client_session.post(
notification.apprise_url,
@@ -45,3 +76,37 @@ async def send_notification(
) as response:
response.raise_for_status()
return await response.json()
async def send_manual_notification(
notification: Notification,
book: ManualBookRequest,
requester_username: Optional[str] = None,
):
print("SENDING", "CALLED")
try:
async with ClientSession() as client_session:
title, body = create_title_body(
notification.title_template,
notification.body_template,
requester_username,
book.title,
",".join(book.authors),
",".join(book.narrators),
notification.event.value,
)
print("SENDING", title, body)
async with client_session.post(
notification.apprise_url,
json={
"title": title,
"body": body,
},
headers=notification.headers,
) as response:
response.raise_for_status()
return await response.json()
except Exception as e:
print("SENDING", e)
return None

View File

@@ -79,7 +79,7 @@ class QualityProfile(StringConfigCache[QualityConfigKey]):
self.delete(session, key)
def get_auto_download(self, session: Session) -> bool:
return bool(self.get_int(session, "quality_auto_download", 1))
return bool(self.get_int(session, "quality_auto_download", 0))
def set_auto_download(self, session: Session, auto_download: bool):
self.set_int(session, "quality_auto_download", int(auto_download))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -22,7 +22,13 @@
<header class="bg-base-100 text-neutral shadow-lg">
<nav class="navbar">
<div class="flex-1">
<a preload href="/" class="btn btn-ghost text-lg">AudioBookRequest</a>
<a
preload
href="/"
class="btn btn-ghost text-lg hidden sm:inline-flex"
>AudioBookRequest</a
>
<a preload href="/" class="btn btn-ghost text-lg sm:hidden">ABR</a>
<a
preload

View File

@@ -16,7 +16,7 @@
</script>
{% endblock %} {% block body %}
<div
class="w-screen flex flex-col items-center justify-center p-8 overflow-x-hidden gap-4"
class="w-screen flex flex-col items-center justify-center p-6 sm:p-8 overflow-x-hidden gap-4"
>
<div class="flex w-full justify-between items-center">
<h1 class="text-3xl font-bold text-left">Search</h1>
@@ -40,7 +40,10 @@
{% if not search_term %}autofocus{% endif %}
value="{{ search_term }}"
/>
<select class="select join-item max-w-[5rem]" name="region">
<select
class="select join-item max-w-[4rem] sm:max-w-[5rem]"
name="region"
>
{% for region in regions %}
<!-- prettier-ignore -->
<option
@@ -60,12 +63,12 @@
{% block book_results %}
<div
id="book-results"
class="min-w-[60vw] max-w-[80vw] h-full grid gap-1 gap-y-2 sm:gap-y-4 sm:gap-2 p-1 grid-flow-row grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-7"
class="min-w-[60vw] max-w-[90vw] sm:max-w-[80vw] h-full grid gap-1 gap-y-2 sm:gap-y-4 sm:gap-2 p-1 grid-flow-row grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-7"
>
{% for book in search_results %}
<div class="flex flex-col">
<div
class="relative w-[10rem] h-[10rem] rounded-md overflow-hidden shadow shadow-black items-center justify-center flex"
class="relative w-[8rem] h-[8rem] sm:w-[10rem] sm:h-[10rem] rounded-md overflow-hidden shadow shadow-black items-center justify-center flex"
>
{% if book.cover_image %}
<img