Skip to content

Commit

Permalink
feat(books): add Readarr support
Browse files Browse the repository at this point in the history
* Support Readarr bot using `/book` command
* Add en-us translations
* Update docs
* Use metadata profile
  • Loading branch information
aymanbagabas committed Oct 11, 2022
1 parent e01d14b commit 8d51b6a
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 20 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
### By Todd Roberts
https://github.com/toddrob99/searcharr

This bot allows users to add movies to Radarr and series to Sonarr via Telegram messaging app.
This bot allows users to add movies to Radarr, series to Sonarr, and books to Readarr via Telegram messaging app.

## Setup & Run

Expand All @@ -20,6 +20,7 @@ You are required to update the following settings, at minimum:
* Telegram Bot > Token (see [Telegram Bot Setup Instructions](https://core.telegram.org/bots#6-botfather))
* Sonarr > URL, API Key, Quality Profile ID
* Radarr > URL, API Key, Quality Profile ID
* Readarr > URL, API Key, Quality Profile ID, Metadata Profile ID

### Docker & Docker-Compose

Expand All @@ -45,9 +46,9 @@ Send a private message to your bot saying `/start <password>` where `<password>`

**Double Caution**: Do not authenticate as an admin in a group chat. Always use a private message with your bot.

### Search & Add a Series to Sonarr or a Movie to Radarr
### Search & Add a Series to Sonarr, a Movie to Radarr, or a Book to Readarr

Send the bot a (private or group) message saying `/series <title>` or `/movie <title>` (replace with custom command aliases, as configured in `settings.py`). The bot will reply with information about the first result, along with buttons to move forward and back within the search results, pop out to tvdb, TMDB, or IMDb, add the current series/movie to Sonarr/Radarr, or cancel the search. When you click the button to add the series/movie to Sonarr/Radarr, the bot will ask what root folder to put the series/movie in, then what quality profile to use--unless you have only one root folder or quality profile enabled in Searcharr settings, in which case it will skip those steps and add the series/movie straight away.
Send the bot a (private or group) message saying `/series <title>`, `/movie <title>`, or `/book <title>` (replace with custom command aliases, as configured in `settings.py`). The bot will reply with information about the first result, along with buttons to move forward and back within the search results, pop out to tvdb, TMDB, or IMDb, or Goodreads for books, add the current series/movie/book to Sonarr/Radarr/Readarr, or cancel the search. When you click the button to add the series/movie/book to Sonarr/Radarr/Readarr, the bot will ask what root folder to put the series/movie/book in, then what quality profile to use--unless you have only one root folder or quality profile enabled in Searcharr settings, in which case it will skip those steps and add the series/movie straight away.

### Manage Users

Expand Down
7 changes: 7 additions & 0 deletions lang/en-us.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ language_label: English
movie: movie
series: series
season: season
book: book
title: title
title_here: title here
password: password
Expand Down Expand Up @@ -57,3 +58,9 @@ help_sonarr: Use {series_commands} to add a series to Sonarr.
help_radarr: Use {movie_commands} to add a movie to Radarr.
no_features: Sorry, but all of my features are currently disabled.
admin_help: Since you are an admin, you can also use {commands} to manage users.
readarr_disabled: Sorry, but book support is disabled.
include_book_title_in_cmd: Please include the book title in the command, e.g. {commands}
no_matching_books: Sorry, but I didn't find any matching books.
help_readarr: Use {book_commands} to add a book to Readarr.
no_metadata_profiles: "Error adding {kind}: no metadata profiles enabled for {app}! Please check your Searcharr configuration and try again."
add_metadata_button: "Add Metadata: {metadata}"
2 changes: 1 addition & 1 deletion log.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Searcharr
Sonarr & Radarr Telegram Bot
Sonarr, Radarr & Readarr Telegram Bot
Log Helper
By Todd Roberts
https://github.com/toddrob99/searcharr
Expand Down
2 changes: 1 addition & 1 deletion radarr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Searcharr
Sonarr & Radarr Telegram Bot
Sonarr, Radarr & Readarr Telegram Bot
Radarr API Wrapper
By Todd Roberts
https://github.com/toddrob99/searcharr
Expand Down
257 changes: 257 additions & 0 deletions readarr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"""
Searcharr
Sonarr, Radarr & Readarr Telegram Bot
Readarr API Wrapper
By Ayman Bagabas
https://github.com/toddrob99/searcharr
"""
import requests
from urllib.parse import quote

from log import set_up_logger


class Readarr(object):
def __init__(self, api_url, api_key, verbose=False):
self.logger = set_up_logger("searcharr.readarr", verbose, False)
self.logger.debug("Logging started!")
if api_url[-1] == "/":
api_url = api_url[:-1]
if api_url[:4] != "http":
self.logger.error(
"Invalid Readarr URL detected. Please update your settings to include http:// or https:// on the beginning of the URL."
)
self.readarr_version = self.discover_version(api_url, api_key)
if not self.readarr_version.startswith("0."):
self.api_url = api_url + "/api/v1/{endpoint}?apikey=" + api_key
self._quality_profiles = self.get_all_quality_profiles()
self._metadata_profiles = self.get_all_metadata_profiles()
self._root_folders = self.get_root_folders()

def discover_version(self, api_url, api_key):
try:
self.api_url = api_url + "/api/v1/{endpoint}?apikey=" + api_key
readarrInfo = self._api_get("system/status")
self.logger.debug(
f"Discovered Readarr version {readarrInfo.get('version')}. Using v1 api."
)
return readarrInfo.get("version")
except requests.exceptions.HTTPError as e:
self.logger.debug(f"Readarr v1 API threw exception: {e}")

try:
self.api_url = api_url + "/api/{endpoint}?apikey=" + api_key
readarrInfo = self._api_get("system/status")
self.logger.warning(
f"Discovered Readarr version {readarrInfo.get('version')}. Using legacy API. Consider upgrading to the latest version of Readarr for the best experience."
)
return readarrInfo.get("version")
except requests.exceptions.HTTPError as e:
self.logger.debug(f"Readarr legacy API threw exception: {e}")

self.logger.debug("Failed to discover Readarr version")
return None

def lookup_book(self, title):
r = self._api_get(
"search", {"term": quote(title)}
)
if not r:
return []

return [
{
"title": x.get("book").get("title"),
"authorId": x.get("book").get("authorId"),
"authorTitle": x.get("book").get("authorTitle"),
"seriesTitle": x.get("book").get("seriesTitle"),
"disambiguation": x.get("book").get("disambiguation"),
"overview": x.get("book").get("overview", "No overview available."),
"remotePoster": x.get("book").get(
"remoteCover",
"https://artworks.thetvdb.com/banners/images/missing/movie.jpg",
),
"releaseDate": x.get("book").get("releaseDate"),
"foreignBookId": x.get("book").get("foreignBookId"),
"id": x.get("book").get("id"),
"pageCount": x.get("book").get("pageCount"),
"titleSlug": x.get("book").get("titleSlug"),
"images": x.get("book").get("images"),
"links": x.get("book").get("links"),
"author": x.get("book").get("author"),
"editions": x.get("book").get("editions"),
}
for x in r if x.get("book")
]

def add_book(
self,
book_info=None,
search=True,
monitored=True,
additional_data={},
):
if not book_info:
return False

if not book_info:
book_info = self.lookup_book(book_info['title'])
if len(book_info):
book_info = book_info[0]
else:
return False

self.logger.debug(f"Additional data: {additional_data}")

path = additional_data["p"]
quality = int(additional_data["q"])
metadata = int(additional_data["m"])
tags = additional_data.get("t", "")
if len(tags):
tag_ids = [int(x) for x in tags.split(",")]
else:
tag_ids = []

params = {
"title": book_info["title"],
"releaseDate": book_info["releaseDate"],
"foreignBookId": book_info["foreignBookId"],
"titleSlug": book_info["titleSlug"],
"monitored": monitored,
"anyEditionOk": True,
"addOptions": {"searchForNewBook": search},
"editions": book_info["editions"],
"author": {
"qualityProfileId": quality,
"metadataProfileId": metadata,
"foreignAuthorId": book_info["author"]["foreignAuthorId"],
"rootFolderPath": path,
"tags": tag_ids,
}
}

return self._api_post("book", params)

def get_root_folders(self):
r = self._api_get("rootfolder", {})
if not r:
return []

return [
{
"path": x.get("path"),
"freeSpace": x.get("freeSpace"),
"totalSpace": x.get("totalSpace"),
"id": x.get("id"),
}
for x in r
]

def _api_get(self, endpoint, params={}):
url = self.api_url.format(endpoint=endpoint)
for k, v in params.items():
url += f"&{k}={v}"
self.logger.debug(f"Submitting GET request: [{url}]")
r = requests.get(url)
if r.status_code not in [200, 201, 202, 204]:
r.raise_for_status()
return None
else:
return r.json()

def get_all_tags(self):
r = self._api_get("tag", {})
self.logger.debug(f"Result of API call to get all tags: {r}")
return [] if not r else r

def get_filtered_tags(self, allowed_tags):
r = self.get_all_tags()
if not r:
return []
elif allowed_tags == []:
return [x for x in r if not x["label"].startswith("searcharr-")]
else:
return [
x
for x in r
if not x["label"].startswith("searcharr-")
and (x["label"] in allowed_tags or x["id"] in allowed_tags)
]

def add_tag(self, tag):
params = {
"label": tag,
}
t = self._api_post("tag", params)
self.logger.debug(f"Result of API call to add tag: {t}")
return t

def get_tag_id(self, tag):
if i := next(
iter(
[
x.get("id")
for x in self.get_all_tags()
if x.get("label").lower() == tag.lower()
]
),
None,
):
self.logger.debug(f"Found tag id [{i}] for tag [{tag}]")
return i
else:
self.logger.debug(f"No tag id found for [{tag}]; adding...")
t = self.add_tag(tag)
if not isinstance(t, dict):
self.logger.error(
f"Wrong data type returned from Readarr API when attempting to add tag [{tag}]. Expected dict, got {type(t)}."
)
return None
else:
self.logger.debug(
f"Created tag id for tag [{tag}]: {t['id']}"
if t.get("id")
else f"Could not add tag [{tag}]"
)
return t.get("id", None)

def lookup_quality_profile(self, v):
# Look up quality profile from a profile name or id
return next(
(x for x in self._quality_profiles if str(v) in [x["name"], str(x["id"])]),
None,
)

def get_all_quality_profiles(self):
return (
self._api_get("qualityProfile", {})
) or None

def lookup_metadata_profile(self, v):
# Look up metadata profile from a profile name or id
return next(
(x for x in self._metadata_profiles if str(v) in [x["name"], str(x["id"])]),
None,
)

def get_all_metadata_profiles(self):
return (
self._api_get("metadataprofile", {})
) or None

def lookup_root_folder(self, v):
# Look up root folder from a path or id
return next(
(x for x in self._root_folders if str(v) in [x["path"], str(x["id"])]),
None,
)

def _api_post(self, endpoint, params={}):
url = self.api_url.format(endpoint=endpoint)
self.logger.debug(f"Submitting POST request: [{url}]; params: [{params}]")
r = requests.post(url, json=params)
if r.status_code not in [200, 201, 202, 204]:
r.raise_for_status()
return None
else:
return r.json()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ argparse
requests
python-telegram-bot
pyyaml
arrow
Loading

0 comments on commit 8d51b6a

Please sign in to comment.