├── .nojekyll ├── pyarr ├── py.typed ├── lib │ ├── __init__.py │ └── alias_decorator.py ├── models │ ├── __init__.py │ ├── lidarr.py │ ├── readarr.py │ ├── radarr.py │ ├── sonarr.py │ └── common.py ├── const.py ├── __init__.py ├── types.py ├── exceptions.py └── request_handler.py ├── tests ├── fixtures │ ├── common │ │ ├── delete.json │ │ ├── blank_dict.json │ │ ├── blank_list.json │ │ ├── rootfolder.json │ │ ├── blocklist.json │ │ └── qualityprofile.json │ ├── lidarr │ │ ├── metadataprovider.json │ │ ├── queue.json │ │ ├── track_error.json │ │ ├── rootfolder.json │ │ ├── track.json │ │ ├── artist.json │ │ ├── track_all.json │ │ ├── artist_all.json │ │ ├── album.json │ │ ├── lookup.json │ │ ├── metadataprofile.json │ │ ├── metadataprofile_all.json │ │ ├── wanted_missing.json │ │ └── album_all.json │ ├── readarr │ │ ├── queue.json │ │ ├── wanted_cutoff.json │ │ ├── metadataprovider.json │ │ ├── delayprofile.json │ │ ├── metadataprofile.json │ │ ├── delayprofile_all.json │ │ ├── releaseprofile.json │ │ ├── releaseprofile_all.json │ │ ├── rootfolder.json │ │ ├── logfile_all.json │ │ ├── metadataprofile_all.json │ │ ├── command.json │ │ ├── lookup_book.json │ │ ├── rootfolder_all.json │ │ ├── wanted_missing.json │ │ ├── author.json │ │ ├── author_all.json │ │ ├── book.json │ │ ├── lookup_author.json │ │ ├── book_all.json │ │ ├── qualityprofile.json │ │ └── lookup.json │ ├── sonarr │ │ ├── file_quality.json │ │ ├── release_download.json │ │ ├── episodefile.json │ │ ├── queue.json │ │ └── parse.json │ └── radarr │ │ ├── moviefile.json │ │ ├── queue_details.json │ │ ├── queue.json │ │ ├── movie_blocklist.json │ │ ├── moviefiles.json │ │ └── movie_import.json ├── docker_configs │ ├── sonarr │ │ └── config.xml │ ├── lidarr │ │ └── config.xml │ ├── radarr │ │ └── config.xml │ └── readarr │ │ └── config.xml ├── conftest.py └── __init__.py ├── mypy.ini ├── .github ├── img │ └── logo.png ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── dependabot.yml ├── workflows │ ├── docs.yml │ ├── coverage.yml │ ├── test.yml │ ├── release.yml.old │ └── release.yml └── PULL_REQUEST_TEMPLATE.md ├── docs ├── requirements.txt ├── modules │ ├── lidarr.rst │ ├── radarr.rst │ ├── sonarr.rst │ └── readarr.rst ├── models │ ├── common.rst │ ├── lidarr.rst │ ├── radarr.rst │ ├── sonarr.rst │ └── readarr.rst ├── index.rst ├── toc.rst ├── Makefile ├── installing.rst ├── make.bat ├── quickstart.rst ├── conf.py └── contributing.rst ├── .pylintrc ├── .coveragerc ├── .pre-commit-hooks.yaml ├── .flake8 ├── .devcontainer ├── docker-compose.workspace.yml ├── post-install.sh ├── Dockerfile ├── docker-compose.yml └── devcontainer.json ├── codecov.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── .releaserc ├── .ci └── docker-compose.yml ├── pyproject.toml ├── noxfile.py ├── README.md └── testing_notebook.ipynb /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyarr/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyarr/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyarr/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/common/delete.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/common/blank_dict.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/fixtures/common/blank_list.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disable_error_code = misc 3 | -------------------------------------------------------------------------------- /.github/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totaldebug/pyarr/HEAD/.github/img/logo.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme>=^1.2.0 2 | toml>=^0.10.2 3 | myst-parser>=^1.0.0 4 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable=missing-module-docstring, line-too-long, too-many-public-methods, too-many-arguments 3 | -------------------------------------------------------------------------------- /docs/modules/lidarr.rst: -------------------------------------------------------------------------------- 1 | LidarrAPI 2 | ---------------------------------------- 3 | .. automodule:: pyarr.lidarr 4 | :members: 5 | :inherited-members: 6 | -------------------------------------------------------------------------------- /docs/modules/radarr.rst: -------------------------------------------------------------------------------- 1 | RadarrAPI 2 | ---------------------------------------- 3 | .. automodule:: pyarr.radarr 4 | :members: 5 | :inherited-members: 6 | -------------------------------------------------------------------------------- /docs/modules/sonarr.rst: -------------------------------------------------------------------------------- 1 | SonarrAPI 2 | ---------------------------------------- 3 | .. automodule:: pyarr.sonarr 4 | :members: 5 | :inherited-members: 6 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pyarr 3 | 4 | omit = 5 | # omit tests 6 | tests/* 7 | 8 | [report] 9 | exclude_lines = 10 | if TYPE_CHECKING: 11 | -------------------------------------------------------------------------------- /docs/modules/readarr.rst: -------------------------------------------------------------------------------- 1 | ReadarrAPI 2 | ---------------------------------------- 3 | .. automodule:: pyarr.readarr 4 | :members: 5 | :inherited-members: 6 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/metadataprovider.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadataSource": "", 3 | "writeAudioTags": "no", 4 | "scrubAudioTags": false, 5 | "id": 1 6 | } 7 | -------------------------------------------------------------------------------- /docs/models/common.rst: -------------------------------------------------------------------------------- 1 | Common 2 | ---------------------------------------- 3 | .. automodule:: pyarr.models.common 4 | :members: 5 | :inherited-members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/models/lidarr.rst: -------------------------------------------------------------------------------- 1 | Lidarr 2 | ---------------------------------------- 3 | .. automodule:: pyarr.models.lidarr 4 | :members: 5 | :inherited-members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/models/radarr.rst: -------------------------------------------------------------------------------- 1 | Radarr 2 | ---------------------------------------- 3 | .. automodule:: pyarr.models.radarr 4 | :members: 5 | :inherited-members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/models/sonarr.rst: -------------------------------------------------------------------------------- 1 | Sonarr 2 | ---------------------------------------- 3 | .. automodule:: pyarr.models.sonarr 4 | :members: 5 | :inherited-members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/models/readarr.rst: -------------------------------------------------------------------------------- 1 | Readarr 2 | ---------------------------------------- 3 | .. automodule:: pyarr.models.readarr 4 | :members: 5 | :inherited-members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /tests/fixtures/common/rootfolder.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "/path/to/folder", 3 | "accessible": true, 4 | "freeSpace": 241118478336, 5 | "unmappedFolders": [], 6 | "id": 9 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "timeleft", 5 | "sortDirection": "ascending", 6 | "totalRecords": 0, 7 | "records": [] 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "timeleft", 5 | "sortDirection": "ascending", 6 | "totalRecords": 0, 7 | "records": [] 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/wanted_cutoff.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "Books.Id", 5 | "sortDirection": "default", 6 | "totalRecords": 0, 7 | "records": [] 8 | } 9 | -------------------------------------------------------------------------------- /pyarr/const.py: -------------------------------------------------------------------------------- 1 | """PyArr Constants""" 2 | from logging import Logger, getLogger 3 | 4 | LOGGER: Logger = getLogger(__package__) 5 | DEPRECATION_WARNING = "This method is deprecated and will be removed in v6.0.0." 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | blank_issues_enabled: false 4 | contact_links: 5 | - name: Total Debug Discord 6 | url: https://discord.gg/6fmekudc8Q 7 | about: Please ask and answer questions here. 8 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/metadataprovider.json: -------------------------------------------------------------------------------- 1 | { 2 | "writeAudioTags": "no", 3 | "scrubAudioTags": false, 4 | "writeBookTags": "newFiles", 5 | "updateCovers": true, 6 | "embedMetadata": false, 7 | "id": 1 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/track_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "BadRequest: One of artistId, albumId, albumReleaseId or trackIds must be provided", 3 | "content": "One of artistId, albumId, albumReleaseId or trackIds must be provided" 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/sonarr/file_quality.json: -------------------------------------------------------------------------------- 1 | { 2 | "quality": { 3 | "quality": { 4 | "id": 8 5 | }, 6 | "revision": { 7 | "version": 1, 8 | "real": 0 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: interrogate 2 | name: interrogate 3 | description: "interrogate your code base for missing docstrings" 4 | entry: interrogate 5 | language: python 6 | language_version: python3 7 | types: [python] 8 | require_serial: true 9 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/delayprofile.json: -------------------------------------------------------------------------------- 1 | { 2 | "enableUsenet": true, 3 | "enableTorrent": true, 4 | "preferredProtocol": "usenet", 5 | "usenetDelay": 0, 6 | "torrentDelay": 0, 7 | "order": 2147483647, 8 | "tags": [], 9 | "id": 1 10 | } 11 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | max-complexity = 18 4 | exclude = build/*, tests/*, .nox/*, .cache/* 5 | extend-ignore = 6 | # See https://github.com/PyCQA/pycodestyle/issues/373 7 | E203, 8 | ignore = E203, E266, E501, W503 9 | select = B,C,E,F,W,T4 10 | -------------------------------------------------------------------------------- /pyarr/__init__.py: -------------------------------------------------------------------------------- 1 | from .lidarr import LidarrAPI 2 | from .radarr import RadarrAPI 3 | from .readarr import ReadarrAPI 4 | from .request_handler import RequestHandler 5 | from .sonarr import SonarrAPI 6 | 7 | __all__ = ["SonarrAPI", "RadarrAPI", "RequestHandler", "ReadarrAPI", "LidarrAPI"] 8 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/metadataprofile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "None", 3 | "minPopularity": 10000000000, 4 | "skipMissingDate": false, 5 | "skipMissingIsbn": false, 6 | "skipPartsAndSets": false, 7 | "skipSeriesSecondary": false, 8 | "minPages": 0, 9 | "id": 2 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.workspace.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: '3' 4 | services: 5 | pyarr-workspace: 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | container_name: pyarr-workspace 10 | volumes: 11 | - ..:/workspaces:cached 12 | command: sleep infinity 13 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.md 2 | :parser: myst_parser.sphinx_ 3 | 4 | Table of Contents 5 | ---------------------------------------------------------- 6 | .. include:: toc.rst 7 | 8 | Indices and tables 9 | ================== 10 | 11 | * :ref:`genindex` 12 | * :ref:`modindex` 13 | * :ref:`search` 14 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/delayprofile_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "enableUsenet": true, 4 | "enableTorrent": true, 5 | "preferredProtocol": "usenet", 6 | "usenetDelay": 0, 7 | "torrentDelay": 0, 8 | "order": 2147483647, 9 | "tags": [], 10 | "id": 1 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /pyarr/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Union 2 | 3 | JsonDataType = TypeVar( 4 | "JsonDataType", bound=Union[str, int, float, list, dict, bool, None] 5 | ) 6 | 7 | JsonObject = dict[str, JsonDataType] 8 | JsonArray = list[JsonObject] 9 | 10 | _ReturnType = TypeVar("_ReturnType", bound=Union[dict, list]) 11 | 12 | __all__ = [_ReturnType, JsonArray] 13 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/releaseprofile.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": true, 3 | "required": "daa", 4 | "ignored": "ddd", 5 | "preferred": [ 6 | { 7 | "key": "qwe", 8 | "value": 3 9 | } 10 | ], 11 | "includePreferredWhenRenaming": false, 12 | "indexerId": 0, 13 | "tags": [], 14 | "id": 1 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/rootfolder.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teest", 3 | "path": "/music/", 4 | "defaultMetadataProfileId": 4, 5 | "defaultQualityProfileId": 1, 6 | "defaultMonitorOption": "all", 7 | "defaultNewItemMonitorOption": "all", 8 | "defaultTags": [], 9 | "accessible": true, 10 | "freeSpace": 25394282496, 11 | "totalSpace": 30006984704, 12 | "id": 4 13 | } 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | coverage: 3 | precision: 2 4 | status: 5 | project: 6 | default: 7 | target: auto # auto compares coverage to the previous base commit 8 | threshold: 2% # allows a 2% drop per pull request 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | ignore: 17 | - "tests" 18 | -------------------------------------------------------------------------------- /.devcontainer/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # Convenience workspace directory for later use 5 | WORKSPACE_DIR=$(pwd) 6 | 7 | # Change some Poetry settings to better deal with working in a container 8 | poetry config cache-dir ${WORKSPACE_DIR}/.cache 9 | 10 | # Now install all dependencies 11 | poetry install 12 | poetry run pre-commit install -t pre-commit -t commit-msg 13 | 14 | echo "Done!" 15 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/releaseprofile_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "enabled": true, 4 | "required": "daa", 5 | "ignored": "ddd", 6 | "preferred": [ 7 | { 8 | "key": "qwe", 9 | "value": 3 10 | } 11 | ], 12 | "includePreferredWhenRenaming": false, 13 | "indexerId": 0, 14 | "tags": [], 15 | "id": 1 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /docs/toc.rst: -------------------------------------------------------------------------------- 1 | 2 | .. toctree:: 3 | :caption: Overview 4 | :titlesonly: 5 | 6 | quickstart 7 | installing 8 | contributing 9 | 10 | .. toctree:: 11 | :caption: Modules 12 | :titlesonly: 13 | 14 | modules/sonarr 15 | modules/radarr 16 | modules/readarr 17 | modules/lidarr 18 | 19 | .. toctree:: 20 | :caption: Models 21 | 22 | models/common 23 | models/sonarr 24 | models/radarr 25 | models/readarr 26 | models/lidarr 27 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/python:latest 2 | 3 | RUN apt update && apt upgrade -y 4 | 5 | RUN apt install -y zsh python3-sphinx 6 | 7 | # Poetry 8 | RUN su vscode -c "umask 0002 && sudo pip3 install poetry" 9 | 10 | # Nox 11 | RUN su vscode -c "umask 0002 && sudo pip3 install nox-poetry nox" 12 | 13 | RUN wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O - | zsh || true 14 | 15 | RUN poetry config virtualenvs.in-project true 16 | 17 | CMD ["zsh"] 18 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/rootfolder.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "books", 3 | "path": "/books/", 4 | "defaultMetadataProfileId": 1, 5 | "defaultQualityProfileId": 1, 6 | "defaultMonitorOption": "all", 7 | "defaultNewItemMonitorOption": "all", 8 | "defaultTags": [], 9 | "isCalibreLibrary": false, 10 | "port": 0, 11 | "outputProfile": "default", 12 | "useSsl": false, 13 | "accessible": true, 14 | "freeSpace": 25373036544, 15 | "totalSpace": 30006984704, 16 | "id": 1 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/logfile_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "filename": "Readarr.txt", 4 | "lastWriteTime": "2022-09-20T11:49:10Z", 5 | "contentsUrl": "/api/v1//Readarr.txt", 6 | "downloadUrl": "/logfile/Readarr.txt", 7 | "id": 8 8 | }, 9 | { 10 | "filename": "Readarr.0.txt", 11 | "lastWriteTime": "2022-09-20T09:49:05Z", 12 | "contentsUrl": "/api/v1//Readarr.0.txt", 13 | "downloadUrl": "/logfile/Readarr.0.txt", 14 | "id": 7 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /tests/docker_configs/sonarr/config.xml: -------------------------------------------------------------------------------- 1 | 2 | info 3 | False 4 | 8989 5 | 9898 6 | 7 | * 8 | da96fdd18ce147b79b54c2fdadb7e19a 9 | None 10 | Docker 11 | True 12 | main 13 | 14 | Sonarr 15 | 514 16 | -------------------------------------------------------------------------------- /tests/docker_configs/lidarr/config.xml: -------------------------------------------------------------------------------- 1 | 2 | info 3 | 4 | Docker 5 | * 6 | 8686 7 | 6868 8 | False 9 | True 10 | f0b398ba17c04645bea28ca934d003e0 11 | None 12 | master 13 | 14 | 15 | Lidarr 16 | -------------------------------------------------------------------------------- /tests/docker_configs/radarr/config.xml: -------------------------------------------------------------------------------- 1 | 2 | info 3 | 4 | Docker 5 | * 6 | 7878 7 | 9898 8 | False 9 | True 10 | 6b95b67e9fd34417b002aada8bf5fa3e 11 | None 12 | master 13 | 14 | 15 | Radarr 16 | -------------------------------------------------------------------------------- /tests/docker_configs/readarr/config.xml: -------------------------------------------------------------------------------- 1 | 2 | info 3 | 4 | Docker 5 | * 6 | 8787 7 | 6868 8 | False 9 | True 10 | ccad0c53c68247ac99616747407c185b 11 | None 12 | develop 13 | 14 | 15 | Readarr 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /tests/fixtures/readarr/metadataprofile_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Standard", 4 | "minPopularity": 350, 5 | "skipMissingDate": true, 6 | "skipMissingIsbn": false, 7 | "skipPartsAndSets": true, 8 | "skipSeriesSecondary": false, 9 | "allowedLanguages": "eng, en-US, en-GB, null", 10 | "minPages": 0, 11 | "id": 1 12 | }, 13 | { 14 | "name": "None", 15 | "minPopularity": 10000000000, 16 | "skipMissingDate": false, 17 | "skipMissingIsbn": false, 18 | "skipPartsAndSets": false, 19 | "skipSeriesSecondary": false, 20 | "minPages": 0, 21 | "id": 2 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #osx 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # cache 7 | __pycache__ 8 | .idea 9 | .cache 10 | 11 | # python 12 | *.pyc 13 | .venv 14 | venv 15 | 16 | # PyPi 17 | dist 18 | build 19 | .vscode/* 20 | *.egg-info 21 | .mypy_cache 22 | .pytest_cache 23 | .coverage 24 | coverage.xml 25 | _build 26 | .nox 27 | 28 | tests/docker_configs/sonarr/* 29 | !tests/docker_configs/sonarr/config.xml 30 | tests/docker_configs/radarr/* 31 | !tests/docker_configs/radarr/config.xml 32 | tests/docker_configs/readarr/* 33 | !tests/docker_configs/readarr/config.xml 34 | tests/docker_configs/lidarr/* 35 | !tests/docker_configs/lidarr/config.xml 36 | tests/docker_configs/deluge/* 37 | tests/docker_configs/jackett/* 38 | !tests/docker_configs/jackett/config/jackett/Jackett/ServerConfig.json 39 | core.* 40 | -------------------------------------------------------------------------------- /tests/fixtures/sonarr/release_download.json: -------------------------------------------------------------------------------- 1 | { 2 | "guid": "https://ipt.beelyrics.net/t/1450590", 3 | "qualityWeight": 0, 4 | "age": 0, 5 | "ageHours": 0.0, 6 | "ageMinutes": 0.0, 7 | "size": 0, 8 | "indexerId": 2, 9 | "fullSeason": false, 10 | "sceneSource": false, 11 | "seasonNumber": 0, 12 | "languageWeight": 0, 13 | "approved": false, 14 | "temporarilyRejected": false, 15 | "rejected": false, 16 | "tvdbId": 0, 17 | "tvRageId": 0, 18 | "publishDate": "2020-05-17T00:00:00Z", 19 | "episodeRequested": false, 20 | "downloadAllowed": false, 21 | "releaseWeight": 0, 22 | "preferredWordScore": 0, 23 | "protocol": "unknown", 24 | "isDaily": false, 25 | "isAbsoluteNumbering": false, 26 | "isPossibleSpecialEpisode": false, 27 | "special": false 28 | } 29 | -------------------------------------------------------------------------------- /docs/installing.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Installation 3 | ************ 4 | 5 | Install the package (or add it to your ``requirements.txt`` file): 6 | 7 | .. code:: shell 8 | 9 | poetry add pyarr 10 | 11 | .. code:: shell 12 | 13 | pip install pyarr 14 | 15 | from source: 16 | 17 | .. code:: shell 18 | 19 | pip install -e https://github.com/totaldebug/pyarr.git#egg=pyarr 20 | 21 | add this to requirements.txt: 22 | 23 | .. code:: shell 24 | 25 | -e git+https://github.com/totaldebug/pyarr.git#egg=pyarr 26 | 27 | 28 | Via Git or Download 29 | =================== 30 | 31 | #. Go to `Pyarr Repo ` 32 | #. Download a copy to your project folders 33 | #. Import as below 34 | 35 | .. code:: python 36 | 37 | from pyarr import SonarrAPI 38 | from pyarr import RadarrAPI 39 | from pyarr import ReadarrAPI 40 | from pyarr import LidarrAPI 41 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: interrogate 6 | name: interrogate 7 | language: system 8 | entry: interrogate 9 | types: [python] 10 | exclude: ^(docs/conf.py|setup.py|tests|noxfile.py) 11 | args: [--config=pyproject.toml] 12 | - repo: https://github.com/psf/black 13 | rev: 24.3.0 14 | hooks: 15 | - id: black 16 | - repo: https://github.com/pycqa/isort 17 | rev: 5.12.0 18 | hooks: 19 | - id: isort 20 | additional_dependencies: [toml] 21 | 22 | - repo: https://github.com/pycqa/flake8 23 | rev: 6.1.0 24 | hooks: 25 | - id: flake8 26 | exclude: ^(tests) 27 | 28 | - repo: https://github.com/pre-commit/pre-commit-hooks 29 | rev: v4.4.0 30 | hooks: 31 | - id: trailing-whitespace 32 | - id: end-of-file-fixer 33 | - id: debug-statements 34 | - id: check-toml 35 | - id: check-yaml 36 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Sphinx Documentation Build and Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Adjust branch name as needed 7 | paths: 8 | - "docs/**" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: 3.12 # Adjust Python version as needed 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install poetry nox 28 | pip install -r docs/requirements.txt 29 | 30 | - name: Build documentation 31 | run: | 32 | nox -s build_docs 33 | 34 | - name: Deploy to gh-pages 35 | uses: peaceiris/actions-gh-pages@v3 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./build/ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Steven Marks 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RefreshMonitoredDownloads", 3 | "commandName": "Refresh Monitored Downloads", 4 | "message": "Completed", 5 | "body": { 6 | "sendUpdatesToClient": false, 7 | "updateScheduledTask": true, 8 | "completionMessage": "Completed", 9 | "requiresDiskAccess": false, 10 | "isExclusive": false, 11 | "isTypeExclusive": false, 12 | "name": "RefreshMonitoredDownloads", 13 | "lastExecutionTime": "2022-09-16T10:52:26Z", 14 | "lastStartTime": "2022-09-16T10:52:26Z", 15 | "trigger": "scheduled", 16 | "suppressMessages": false 17 | }, 18 | "priority": "low", 19 | "status": "completed", 20 | "queued": "2022-09-16T10:53:56Z", 21 | "started": "2022-09-16T10:53:56Z", 22 | "ended": "2022-09-16T10:53:56Z", 23 | "duration": "00:00:00.0213480", 24 | "trigger": "scheduled", 25 | "stateChangeTime": "2022-09-16T10:53:56Z", 26 | "sendUpdatesToClient": false, 27 | "updateScheduledTask": true, 28 | "lastExecutionTime": "2022-09-16T10:52:26Z", 29 | "id": 883340 30 | } 31 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/lookup_book.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "string", 4 | "authorTitle": "string", 5 | "seriesTitle": "", 6 | "disambiguation": "", 7 | "authorId": 0, 8 | "foreignBookId": "12345678", 9 | "titleSlug": "12345678", 10 | "monitored": false, 11 | "anyEditionOk": true, 12 | "ratings": { 13 | "votes": 8085, 14 | "value": 4.74, 15 | "popularity": 38322.9 16 | }, 17 | "releaseDate": "2015-07-03T23:00:00Z", 18 | "pageCount": 30, 19 | "genres": [ 20 | "string" 21 | ], 22 | "images": [ 23 | { 24 | "url": "string", 25 | "coverType": "cover", 26 | "extension": ".jpg" 27 | } 28 | ], 29 | "links": [ 30 | { 31 | "url": "string", 32 | "name": "Goodreads Editions" 33 | }, 34 | { 35 | "url": "string", 36 | "name": "Goodreads Book" 37 | } 38 | ], 39 | "added": "0001-01-01T00:01:00Z", 40 | "remoteCover": "string", 41 | "grabbed": false 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /tests/fixtures/sonarr/episodefile.json: -------------------------------------------------------------------------------- 1 | { 2 | "seriesId": 0, 3 | "seasonNumber": 0, 4 | "relativePath": "string", 5 | "path": "string", 6 | "size": 0, 7 | "dateAdded": "2019-05-19T05:33:25.295709Z", 8 | "releaseGroup": "string", 9 | "language": { 10 | "id": 0, 11 | "name": "string" 12 | }, 13 | "quality": { 14 | "quality": { 15 | "id": 0, 16 | "name": "string", 17 | "source": "string", 18 | "resolution": 0 19 | }, 20 | "revision": { 21 | "version": 0, 22 | "real": 0, 23 | "isRepack": false 24 | } 25 | }, 26 | "mediaInfo": { 27 | "audioBitrate": 0, 28 | "audioChannels": 0.0, 29 | "audioCodec": "string", 30 | "audioLanguages": "string", 31 | "audioStreamCount": 0, 32 | "videoBitDepth": 0, 33 | "videoBitrate": 0, 34 | "videoCodec": "string", 35 | "videoFps": 0.0, 36 | "resolution": "string", 37 | "runTime": "00:00", 38 | "scanType": "string", 39 | "subtitles": "string" 40 | }, 41 | "qualityCutoffNotMet": true, 42 | "languageCutoffNotMet": false, 43 | "id": 0 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - beta 7 | pull_request: 8 | branches: 9 | - beta 10 | workflow_dispatch: 11 | 12 | jobs: 13 | code-quality: 14 | name: 📊 Check code coverage 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup Python 3.12 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.12 22 | - name: Run containers 23 | run: docker-compose -f .ci/docker-compose.yml up -d 24 | - name: Add hosts 25 | run: | 26 | sudo echo "127.0.0.1 sonarr readarr radarr lidarr prowlarr deluge jackett" | sudo tee -a /etc/hosts 27 | - name: Sleep for 30s 28 | run: sleep 30s 29 | shell: bash 30 | - name: check ports are mapped 31 | run: docker ps 32 | - name: check one of the containers is up 33 | run: curl http://radarr:7878 34 | - name: 🧪 Check tests are passing 35 | run: | 36 | pip install poetry nox 37 | nox -s tests 38 | - name: 📤 Upload coverage to Codecov 39 | uses: codecov/codecov-action@v3 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | -------------------------------------------------------------------------------- /pyarr/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyarrError(Exception): 2 | """Generic PyArr Exception.""" 3 | 4 | 5 | class PyarrConnectionError(PyarrError): 6 | """Sonarr connection exception.""" 7 | 8 | 9 | class PyarrUnauthorizedError(PyarrError): 10 | """Unauthorised access exception""" 11 | 12 | 13 | class PyarrAccessRestricted(PyarrError): 14 | """Pyarr access restricted exception.""" 15 | 16 | 17 | class PyarrResourceNotFound(PyarrError): 18 | """Pyarr resource not found exception""" 19 | 20 | 21 | class PyarrBadGateway(PyarrError): 22 | """Pyarr bad gateway exception""" 23 | 24 | 25 | class PyarrMissingProfile(PyarrError): 26 | """Pyarr missing profile""" 27 | 28 | 29 | class PyarrMethodNotAllowed(PyarrError): 30 | """Pyarr method not allowed""" 31 | 32 | 33 | class PyarrRecordNotFound(PyarrError): 34 | """Pyarr record was not found""" 35 | 36 | 37 | class PyarrMissingArgument(PyarrError): 38 | """Missing one of multiple possible arguments""" 39 | 40 | 41 | class PyarrBadRequest(PyarrError): 42 | """Bad Request, possible bug.""" 43 | 44 | 45 | class PyarrServerError(PyarrError): 46 | """Server Error, missing or incorrect options.""" 47 | 48 | def __init__(self, message, response): 49 | super().__init__(message) 50 | self.response = response 51 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - beta 7 | pull_request: 8 | branches: 9 | - main 10 | - beta 11 | schedule: 12 | - cron: "0 0 * * *" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | code-quality: 17 | name: 📊 Check code quality 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python-version: ["3.10", "3.11", "3.12"] 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Setup Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Run containers 30 | run: docker-compose -f .ci/docker-compose.yml up -d 31 | - name: Add hosts 32 | run: | 33 | sudo echo "127.0.0.1 sonarr readarr radarr lidarr prowlarr deluge jackett" | sudo tee -a /etc/hosts 34 | - name: sleep 30s for containers to start-up 35 | run: sleep 30s 36 | shell: bash 37 | - name: check ports are mapped 38 | run: docker ps 39 | - name: check one of the containers is up 40 | run: curl http://radarr:7878 41 | - name: 🧪 Check tests are passing 42 | run: | 43 | pip install poetry nox 44 | nox -s tests 45 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyarr.lidarr import LidarrAPI 4 | from pyarr.radarr import RadarrAPI 5 | from pyarr.readarr import ReadarrAPI 6 | from pyarr.sonarr import SonarrAPI 7 | 8 | from tests import ( 9 | LIDARR_API_KEY, 10 | MOCK_API_KEY, 11 | MOCK_URL, 12 | RADARR_API_KEY, 13 | READARR_API_KEY, 14 | SONARR_API_KEY, 15 | ) 16 | 17 | 18 | @pytest.fixture() 19 | def sonarr_client(): 20 | yield SonarrAPI("http://localhost:8989", SONARR_API_KEY) 21 | 22 | 23 | @pytest.fixture() 24 | def sonarr_mock_client(): 25 | yield SonarrAPI(f"{MOCK_URL}:8989", MOCK_API_KEY) 26 | 27 | 28 | @pytest.fixture() 29 | def radarr_client(): 30 | yield RadarrAPI("http://localhost:7878", RADARR_API_KEY) 31 | 32 | 33 | @pytest.fixture() 34 | def radarr_mock_client(): 35 | yield RadarrAPI(f"{MOCK_URL}:7878", MOCK_API_KEY) 36 | 37 | 38 | @pytest.fixture() 39 | def lidarr_client(): 40 | yield LidarrAPI("http://localhost:8686", LIDARR_API_KEY) 41 | 42 | 43 | @pytest.fixture() 44 | def lidarr_mock_client(): 45 | yield LidarrAPI(f"{MOCK_URL}:8686", MOCK_API_KEY) 46 | 47 | 48 | @pytest.fixture() 49 | def readarr_client(): 50 | yield ReadarrAPI("http://localhost:8787", READARR_API_KEY) 51 | 52 | 53 | @pytest.fixture() 54 | def readarr_mock_client(): 55 | yield ReadarrAPI(f"{MOCK_URL}:8787", MOCK_API_KEY) 56 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | ************** 4 | 🚀 Quick Start 5 | ************** 6 | 7 | This quick start guide will take you through the easiest way to get up and running. 8 | 9 | Installation 10 | ############ 11 | 12 | This package is distributed on PyPI and can be installed with `pip`: 13 | 14 | .. code-block:: shell 15 | :linenos: 16 | pip install pyarr 17 | 18 | 19 | To use the package in your Python project, you will need to import the required modules from below: 20 | 21 | .. code-block:: python 22 | :linenos: 23 | 24 | from pyarr import SonarrAPI 25 | from pyarr import RadarrAPI 26 | from pyarr import ReadarrAPI 27 | from pyarr import LidarrAPI 28 | 29 | All of the library modules are based on the same format the below example can be 30 | modified for each one by changing the `sonarr` referances to the required `arr` API: 31 | 32 | .. code-block:: python 33 | :linenos: 34 | 35 | # Import SonarrAPI Class 36 | from pyarr import SonarrAPI 37 | 38 | # Set Host URL and API-Key 39 | host_url = 'http://your-domain.com' 40 | 41 | # You can find your API key in Settings > General. 42 | api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 43 | 44 | # Instantiate SonarrAPI Object 45 | sonarr = SonarrAPI(host_url, api_key) 46 | 47 | # Get and print TV Shows 48 | print(sonarr.get_series()) 49 | -------------------------------------------------------------------------------- /tests/fixtures/sonarr/queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "timeleft", 5 | "sortDirection": "ascending", 6 | "totalRecords": 1, 7 | "records": [ 8 | { 9 | "seriesId": 0, 10 | "episodeId": 0, 11 | "language": { 12 | "id": 0, 13 | "name": "string" 14 | }, 15 | "quality": { 16 | "quality": { 17 | "id": 0, 18 | "name": "string", 19 | "source": "string", 20 | "resolution": 0 21 | }, 22 | "revision": { 23 | "version": 0, 24 | "real": 0, 25 | "isRepack": false 26 | } 27 | }, 28 | "size": 0.0, 29 | "title": "string", 30 | "sizeleft": 0.0, 31 | "timeleft": "00:00:00", 32 | "estimatedCompletionTime": "2020-02-09T13:14:14.379532Z", 33 | "status": "string", 34 | "trackedDownloadStatus": "string", 35 | "trackedDownloadState": "string", 36 | "statusMessages": [ 37 | { 38 | "title": "string", 39 | "messages": ["string"] 40 | } 41 | ], 42 | "downloadId": "string", 43 | "protocol": "unknown", 44 | "downloadClient": "string", 45 | "indexer": "string", 46 | "outputPath": "string", 47 | "id": 0 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/radarr/moviefile.json: -------------------------------------------------------------------------------- 1 | { 2 | "movieId": 17, 3 | "relativePath": "string", 4 | "path": "string", 5 | "size": 5536497109, 6 | "dateAdded": "2018-01-21T02:11:37Z", 7 | "sceneName": "string", 8 | "indexerFlags": 0, 9 | "quality": { 10 | "quality": { 11 | "id": 3, 12 | "name": "WEBDL-1080p", 13 | "source": "webdl", 14 | "resolution": 1080, 15 | "modifier": "none" 16 | }, 17 | "revision": { 18 | "version": 1, 19 | "real": 0, 20 | "isRepack": false 21 | } 22 | }, 23 | "customFormats": [], 24 | "mediaInfo": { 25 | "audioBitrate": 384000, 26 | "audioChannels": 5.1, 27 | "audioCodec": "AC3", 28 | "audioLanguages": "eng", 29 | "audioStreamCount": 1, 30 | "videoBitDepth": 8, 31 | "videoBitrate": 0, 32 | "videoCodec": "x264", 33 | "videoDynamicRangeType": "", 34 | "videoFps": 23.976, 35 | "resolution": "1920x800", 36 | "runTime": "2:10:44", 37 | "scanType": "Progressive", 38 | "subtitles": "eng" 39 | }, 40 | "qualityCutoffNotMet": false, 41 | "languages": [ 42 | { 43 | "id": 1, 44 | "name": "English" 45 | } 46 | ], 47 | "releaseGroup": "string", 48 | "edition": "", 49 | "id": 102 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/radarr/queue_details.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "movieId": 311, 4 | "languages": [ 5 | { 6 | "id": 1, 7 | "name": "English" 8 | } 9 | ], 10 | "quality": { 11 | "quality": { 12 | "id": 7, 13 | "name": "Bluray-1080p", 14 | "source": "bluray", 15 | "resolution": 1080, 16 | "modifier": "none" 17 | }, 18 | "revision": { 19 | "version": 1, 20 | "real": 0, 21 | "isRepack": false 22 | } 23 | }, 24 | "customFormats": [], 25 | "size": 6202395598, 26 | "title": "Goodfellas 1990 25th Anniversary REMASTERED BRRip x264 1080p-NPW", 27 | "sizeleft": 6193674427, 28 | "timeleft": "01:33:22", 29 | "estimatedCompletionTime": "2022-07-26T10:34:55Z", 30 | "status": "downloading", 31 | "trackedDownloadStatus": "ok", 32 | "trackedDownloadState": "downloading", 33 | "statusMessages": [], 34 | "downloadId": "202B87F7190B45F86487C7A28A49EA5A5EB939A5", 35 | "protocol": "torrent", 36 | "downloadClient": "deluge", 37 | "indexer": "Jackett", 38 | "outputPath": "/downloads/Goodfellas 1990 25th Anniversary REMASTERED BRRip x264 1080p-NPW", 39 | "id": 1340002663 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 6 | 7 | ## Related issues 8 | 9 | 10 | 11 | 15 | 16 | ## Type of change 17 | 18 | 21 | - [ ] Bug fix (non-breaking change which fixes an issue) 22 | - [ ] New feature (non-breaking change which adds functionality) 23 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 24 | - [ ] CI / Tests update 25 | - [ ] Documentation update 26 | 27 | ## How has this been tested 28 | 29 | 32 | 33 | - [ ] I have added new tests where required 34 | - [ ] I have run `nox -s tests` locally and passed 35 | - [ ] I have tested this feature in a python script 36 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | services: 4 | sonarr: 5 | image: lscr.io/linuxserver/sonarr:latest 6 | container_name: sonarr 7 | environment: 8 | - PUID=1000 9 | - PGID=1000 10 | - TZ=Europe/London 11 | volumes: 12 | - ../tests/docker_configs/sonarr/config.xml:/config/config.xml 13 | ports: 14 | - 8989:8989 15 | restart: unless-stopped 16 | radarr: 17 | image: lscr.io/linuxserver/radarr:latest 18 | container_name: radarr 19 | environment: 20 | - PUID=1000 21 | - PGID=1000 22 | - TZ=Europe/London 23 | volumes: 24 | - ../tests/docker_configs/radarr/config.xml:/config/config.xml 25 | ports: 26 | - 7878:7878 27 | restart: unless-stopped 28 | readarr: 29 | image: lscr.io/linuxserver/readarr:develop 30 | container_name: readarr 31 | environment: 32 | - PUID=1000 33 | - PGID=1000 34 | - TZ=Europe/London 35 | volumes: 36 | - ../tests/docker_configs/readarr/config.xml:/config/config.xml 37 | ports: 38 | - 8787:8787 39 | restart: unless-stopped 40 | lidarr: 41 | image: lscr.io/linuxserver/lidarr:latest 42 | container_name: lidarr 43 | environment: 44 | - PUID=1000 45 | - PGID=1000 46 | - TZ=Europe/London 47 | volumes: 48 | - ../tests/docker_configs/lidarr/config.xml:/config/config.xml 49 | ports: 50 | - 8686:8686 51 | restart: unless-stopped 52 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "branches": [ 4 | "main" 5 | ], 6 | "plugins": [ 7 | [ 8 | "@semantic-release/commit-analyzer", 9 | { 10 | "preset": "conventionalcommits" 11 | } 12 | ], 13 | [ 14 | "@semantic-release/release-notes-generator", 15 | { 16 | "preset": "conventionalcommits" 17 | } 18 | ], 19 | "@semantic-release/changelog", 20 | [ 21 | "semantic-release-pypi", 22 | { 23 | "pypiPublish": false 24 | } 25 | ], 26 | [ 27 | "@semantic-release/exec", 28 | { 29 | "prepareCmd": "poetry build" 30 | } 31 | ], 32 | [ 33 | "@semantic-release/git", 34 | { 35 | "assets": [ 36 | "CHANGELOG.md", 37 | "pyproject.toml" 38 | ], 39 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 40 | } 41 | ], 42 | [ 43 | "@semantic-release/github", 44 | { 45 | "assets": [ 46 | { 47 | "path": "dist/*" 48 | } 49 | ] 50 | } 51 | ], 52 | "@semantic-release/changelog" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import xml.etree.ElementTree as ET 3 | 4 | MOCK_URL = "https://127.0.0.1" 5 | MOCK_API_KEY = "123456789abcdefg123456789" 6 | SONARR_API_KEY = ( 7 | ET.parse("tests/docker_configs/sonarr/config.xml").getroot().find("ApiKey").text 8 | ) 9 | RADARR_API_KEY = ( 10 | ET.parse("tests/docker_configs/radarr/config.xml").getroot().find("ApiKey").text 11 | ) 12 | READARR_API_KEY = ( 13 | ET.parse("tests/docker_configs/readarr/config.xml").getroot().find("ApiKey").text 14 | ) 15 | LIDARR_API_KEY = ( 16 | ET.parse("tests/docker_configs/lidarr/config.xml").getroot().find("ApiKey").text 17 | ) 18 | 19 | RADARR_IMDB = "tt1213644" 20 | RADARR_IMDB_LIST = ["tt0060666", "tt1316037"] 21 | RADARR_TMDB = 129 22 | RADARR_MOVIE_TERM = "Movie" 23 | 24 | SONARR_TVDB = 305288 25 | 26 | LIDARR_TERM = "Silvertin" 27 | LIDARR_ARTIST_TERM = "Silvertin" 28 | LIDARR_ALBUM_TERM = "Dawn" 29 | LIDARR_MUSICBRAINZ_ARTIST_ID = "171a57fb-1a66-4094-8373-72c7d1b0c621" 30 | LIDARR_MUSICBRAINZ_ALBUM_ID = "f3f61963-359d-4f2f-8fc6-63856ffbe070" 31 | 32 | READARR_GOODREADS_ID = "489521" 33 | READARR_ASIN_ID = "9780691017846" 34 | READARR_ISBN_ID = "9780691017846" 35 | READARR_AUTHOR_ID = "7182094" 36 | READARR_AUTHOR_TERM = "Maurice Maeterlinck" 37 | 38 | 39 | def load_fixture(filename) -> str: 40 | """Load a fixture.""" 41 | return ( 42 | pathlib.Path(__file__) 43 | .parent.joinpath("fixtures", filename) 44 | .read_text(encoding="utf8") 45 | ) 46 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/rootfolder_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "books", 4 | "path": "/books/", 5 | "defaultMetadataProfileId": 1, 6 | "defaultQualityProfileId": 1, 7 | "defaultMonitorOption": "all", 8 | "defaultNewItemMonitorOption": "all", 9 | "defaultTags": [], 10 | "isCalibreLibrary": false, 11 | "port": 0, 12 | "outputProfile": "default", 13 | "useSsl": false, 14 | "accessible": true, 15 | "freeSpace": 25373036544, 16 | "totalSpace": 30006984704, 17 | "id": 1 18 | }, 19 | { 20 | "name": "TSDF", 21 | "path": "/app/", 22 | "defaultMetadataProfileId": 1, 23 | "defaultQualityProfileId": 1, 24 | "defaultMonitorOption": "all", 25 | "defaultNewItemMonitorOption": "all", 26 | "defaultTags": [], 27 | "isCalibreLibrary": false, 28 | "port": 0, 29 | "outputProfile": "default", 30 | "useSsl": false, 31 | "accessible": true, 32 | "freeSpace": 7650742272, 33 | "totalSpace": 42924511232, 34 | "id": 6 35 | }, 36 | { 37 | "name": "test", 38 | "path": "/downloads/", 39 | "defaultMetadataProfileId": 1, 40 | "defaultQualityProfileId": 1, 41 | "defaultMonitorOption": "all", 42 | "defaultNewItemMonitorOption": "all", 43 | "defaultTags": [], 44 | "isCalibreLibrary": false, 45 | "port": 0, 46 | "outputProfile": "default", 47 | "useSsl": false, 48 | "accessible": true, 49 | "freeSpace": 198678937600, 50 | "totalSpace": 315006910464, 51 | "id": 7 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /tests/fixtures/radarr/queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "timeleft", 5 | "sortDirection": "ascending", 6 | "totalRecords": 1, 7 | "records": [ 8 | { 9 | "movieId": 311, 10 | "languages": [ 11 | { 12 | "id": 1, 13 | "name": "English" 14 | } 15 | ], 16 | "quality": { 17 | "quality": { 18 | "id": 7, 19 | "name": "Bluray-1080p", 20 | "source": "bluray", 21 | "resolution": 1080, 22 | "modifier": "none" 23 | }, 24 | "revision": { 25 | "version": 1, 26 | "real": 0, 27 | "isRepack": false 28 | } 29 | }, 30 | "customFormats": [], 31 | "size": 6202395598, 32 | "title": "Goodfellas 1990 25th Anniversary REMASTERED BRRip x264 1080p-NPW", 33 | "sizeleft": 6193674427, 34 | "timeleft": "01:33:22", 35 | "estimatedCompletionTime": "2022-07-26T10:34:55Z", 36 | "status": "downloading", 37 | "trackedDownloadStatus": "ok", 38 | "trackedDownloadState": "downloading", 39 | "statusMessages": [], 40 | "downloadId": "202B87F7190B45F86487C7A28A49EA5A5EB939A5", 41 | "protocol": "torrent", 42 | "downloadClient": "deluge", 43 | "indexer": "Jackett", 44 | "outputPath": "/downloads/Goodfellas 1990 25th Anniversary REMASTERED BRRip x264 1080p-NPW", 45 | "id": 1340002663 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/track.json: -------------------------------------------------------------------------------- 1 | { 2 | "artistId": 3, 3 | "foreignTrackId": "string", 4 | "foreignRecordingId": "string", 5 | "trackFileId": 0, 6 | "albumId": 12, 7 | "explicit": false, 8 | "absoluteTrackNumber": 1, 9 | "trackNumber": "1", 10 | "title": "string", 11 | "duration": 442466, 12 | "mediumNumber": 1, 13 | "hasFile": false, 14 | "artist": { 15 | "artistMetadataId": 6, 16 | "status": "ended", 17 | "ended": true, 18 | "artistName": "string", 19 | "foreignArtistId": "string", 20 | "tadbId": 0, 21 | "discogsId": 0, 22 | "overview": "string", 23 | "artistType": "Person", 24 | "disambiguation": "", 25 | "links": [ 26 | { 27 | "url": "string", 28 | "coverType": "poster", 29 | "extension": ".jpg" 30 | } 31 | ], 32 | "path": "string", 33 | "qualityProfileId": 1, 34 | "metadataProfileId": 2, 35 | "monitored": true, 36 | "monitorNewItems": "all", 37 | "genres": [ 38 | "string" 39 | ], 40 | "cleanName": "string", 41 | "sortName": "string", 42 | "tags": [], 43 | "added": "2022-03-06T19:32:33Z", 44 | "ratings": { 45 | "votes": 9, 46 | "value": 8.4 47 | }, 48 | "statistics": { 49 | "albumCount": 0, 50 | "trackFileCount": 0, 51 | "trackCount": 0, 52 | "totalTrackCount": 0, 53 | "sizeOnDisk": 0, 54 | "percentOfTracks": 0 55 | }, 56 | "id": 3 57 | }, 58 | "ratings": { 59 | "votes": 0, 60 | "value": 0 61 | }, 62 | "grabbed": false, 63 | "id": 6009 64 | } 65 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/wanted_missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "Books.Id", 5 | "sortDirection": "default", 6 | "totalRecords": 82, 7 | "records": [ 8 | { 9 | "title": "string", 10 | "authorTitle": "string", 11 | "seriesTitle": "", 12 | "disambiguation": "string", 13 | "authorId": 22, 14 | "foreignBookId": "string", 15 | "titleSlug": "string", 16 | "monitored": true, 17 | "anyEditionOk": true, 18 | "ratings": { 19 | "votes": 403868, 20 | "value": 4.02, 21 | "popularity": 1623549.3599999999 22 | }, 23 | "releaseDate": "1961-11-01T00:00:00Z", 24 | "pageCount": 146, 25 | "genres": [ 26 | "string" 27 | ], 28 | "images": [ 29 | { 30 | "url": "string", 31 | "coverType": "cover", 32 | "extension": ".jpg" 33 | } 34 | ], 35 | "links": [ 36 | { 37 | "url": "string", 38 | "name": "Goodreads Editions" 39 | }, 40 | { 41 | "url": "string", 42 | "name": "Goodreads Book" 43 | } 44 | ], 45 | "statistics": { 46 | "bookFileCount": 0, 47 | "bookCount": 1, 48 | "totalBookCount": 1, 49 | "sizeOnDisk": 0, 50 | "percentOfBooks": 0 51 | }, 52 | "added": "2022-05-25T09:35:57Z", 53 | "grabbed": false, 54 | "id": 997 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PyArr", 3 | "dockerComposeFile": [ 4 | "docker-compose.workspace.yml" 5 | ], 6 | "service": "pyarr-workspace", 7 | "workspaceFolder": "/workspaces", 8 | "forwardPorts": [ 9 | 8989, 10 | 7878, 11 | 8787, 12 | 8686 13 | ], 14 | "initializeCommand": "echo 'Hello World!'", 15 | "customizations": { 16 | "vscode": { 17 | "settings": { 18 | "[python]": { 19 | "diffEditor.ignoreTrimWhitespace": false, 20 | "editor.formatOnType": true, 21 | "editor.wordBasedSuggestions": "off", 22 | "editor.defaultFormatter": "ms-python.black-formatter", 23 | "autoDocstring.docstringFormat": "google", 24 | "editor.tabSize": 4 25 | }, 26 | "[yaml]": { 27 | "editor.insertSpaces": true, 28 | "editor.tabSize": 2, 29 | "editor.autoIndent": "advanced", 30 | "diffEditor.ignoreTrimWhitespace": false 31 | }, 32 | "files.eol": "\n", 33 | "terminal.integrated.profiles.linux": { 34 | "zsh": { 35 | "path": "/bin/zsh" 36 | } 37 | }, 38 | "terminal.integrated.defaultProfile.linux": "zsh", 39 | "editor.formatOnPaste": false, 40 | "editor.formatOnSave": true, 41 | "editor.formatOnType": true, 42 | "files.trimTrailingWhitespace": true 43 | }, 44 | "extensions": [ 45 | "sourcery.sourcery", 46 | "njpwerner.autodocstring", 47 | "ms-python.flake8", 48 | "dbaeumer.vscode-eslint", 49 | "ms-python.isort", 50 | "ms-python.python", 51 | "ms-python.vscode-pylance", 52 | "ms-python.black-formatter", 53 | "ms-toolsai.jupyter", 54 | "GitHub.vscode-github-actions", 55 | "yzhang.markdown-all-in-one" 56 | ], 57 | "runArgs": [ 58 | "--privileged", 59 | "--network=pyarr-dev" 60 | ] 61 | } 62 | }, 63 | "remoteUser": "vscode", 64 | "postCreateCommand": "zsh ./.devcontainer/post-install.sh", 65 | "features": { 66 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 67 | "version": "latest", 68 | "moby": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: '3' 4 | services: 5 | sonarr: 6 | image: lscr.io/linuxserver/sonarr:latest 7 | container_name: sonarr 8 | environment: 9 | - PUID=1001 10 | - PGID=123 11 | - TZ=Europe/London 12 | volumes: 13 | - ../tests/docker_configs/sonarr/config.xml:/config/config.xml:rw 14 | ports: 15 | - 8989:8989 16 | restart: unless-stopped 17 | radarr: 18 | image: lscr.io/linuxserver/radarr:latest 19 | container_name: radarr 20 | environment: 21 | - PUID=1001 22 | - PGID=123 23 | - TZ=Europe/London 24 | volumes: 25 | - ../tests/docker_configs/radarr/config.xml:/config/config.xml:rw 26 | ports: 27 | - 7878:7878 28 | restart: unless-stopped 29 | readarr: 30 | image: lscr.io/linuxserver/readarr:develop 31 | container_name: readarr 32 | environment: 33 | - PUID=1001 34 | - PGID=123 35 | - TZ=Europe/London 36 | volumes: 37 | - ../tests/docker_configs/readarr/config.xml:/config/config.xml:rw 38 | ports: 39 | - 8787:8787 40 | restart: unless-stopped 41 | lidarr: 42 | image: lscr.io/linuxserver/lidarr:latest 43 | container_name: lidarr 44 | environment: 45 | - PUID=1001 46 | - PGID=123 47 | - TZ=Europe/London 48 | volumes: 49 | - ../tests/docker_configs/lidarr/config.xml:/config/config.xml:rw 50 | ports: 51 | - 8686:8686 52 | restart: unless-stopped 53 | deluge: 54 | image: lscr.io/linuxserver/deluge:latest 55 | container_name: deluge 56 | environment: 57 | - PUID=1001 58 | - PGID=123 59 | - TZ=Europe/London 60 | volumes: 61 | - ../tests/docker_configs/deluge/config:/config:rw 62 | ports: 63 | - 8112:8112 64 | restart: unless-stopped 65 | jackett: 66 | image: lscr.io/linuxserver/jackett:latest 67 | container_name: jackett 68 | environment: 69 | - PUID=1001 70 | - PGID=123 71 | - TZ=Europe/London 72 | volumes: 73 | - ../tests/docker_configs/jackett/config/jackett:/config:rw 74 | ports: 75 | - 9117:9117 76 | restart: unless-stopped 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: ['type/feature'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **NOTE:** Before you start, the following should be completed. 9 | 10 | - Read [documentation](https://docs.totaldebug.uk/pyarr) to understand the current feature set. 11 | - Make sure no [similar feature requests(including closed ones)](https://github.com/totaldebug/pyarr/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Ffeature) exists. 12 | - Make sure the request is based on the latest code in the `master` branch. 13 | 14 | Thanks for taking the time to assist with improving this project! 15 | - type: checkboxes 16 | attributes: 17 | label: Is there an existing issue for this? 18 | description: Please search to see if an issue already exists for the feature you are requesting. 19 | options: 20 | - label: I have searched the existing issues 21 | required: true 22 | - type: textarea 23 | id: expected-feature 24 | attributes: 25 | label: Expected feature 26 | description: A concise description of what you expected the feature to do. 27 | placeholder: Tell us what you should see! 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Possible Solutions 33 | description: If you have an idea on how to implement this please let us know. 34 | validations: 35 | required: false 36 | - type: textarea 37 | attributes: 38 | label: Context / Reason 39 | description: Providing context helps us come up with a solution that is most useful in the real world. 40 | validations: 41 | required: true 42 | - type: checkboxes 43 | id: terms 44 | attributes: 45 | label: Code of Conduct 46 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/marksie1988/.github/blob/main/.github/CODE_OF_CONDUCT.md) 47 | options: 48 | - label: I agree to follow this project's Code of Conduct 49 | required: true 50 | -------------------------------------------------------------------------------- /pyarr/lib/alias_decorator.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from typing import Any, Callable, Dict, Optional, Set 3 | import warnings 4 | 5 | 6 | class FunctionWrapper: 7 | """Function wrapper""" 8 | 9 | def __init__(self, func: Callable[..., Any]) -> None: 10 | self.func = func 11 | self._aliases: Set[str] = set() 12 | 13 | 14 | class alias(object): 15 | """Add an alias to a function""" 16 | 17 | def __init__(self, *aliases: str, deprecated_version: str | None = None) -> None: 18 | """Constructor 19 | 20 | Args: 21 | deprecated_version (str, optional): Version number that deprecation will happen. Defaults to None. 22 | """ 23 | self.aliases: Set[str] = set(aliases) 24 | self.deprecated_version: Optional[str] = deprecated_version 25 | 26 | def __call__(self, f: Callable[..., Any]) -> FunctionWrapper: 27 | """call""" 28 | wrapped_func = FunctionWrapper(f) 29 | wrapped_func._aliases = self.aliases 30 | 31 | @functools.wraps(f) 32 | def wrapper(*args: Any, **kwargs: Any) -> Any: 33 | """Alias wrapper""" 34 | if self.deprecated_version: 35 | aliases_str = ", ".join(self.aliases) 36 | msg = f"{aliases_str} is deprecated and will be removed in version {self.deprecated_version}. Use {f.__name__} instead." 37 | warnings.warn(msg, DeprecationWarning) 38 | return f(*args, **kwargs) 39 | 40 | wrapped_func.func = wrapper # Assign wrapper directly to func attribute 41 | return wrapped_func 42 | 43 | 44 | def aliased(aliased_class: Any) -> Any: 45 | """Class has aliases""" 46 | original_methods: Dict[str, Any] = aliased_class.__dict__.copy() 47 | for name, method in original_methods.items(): 48 | if isinstance(method, FunctionWrapper) and hasattr(method, "_aliases"): 49 | for alias in method._aliases: 50 | setattr(aliased_class, alias, method.func) 51 | 52 | # Also replace the original method with the wrapped function 53 | setattr(aliased_class, name, method.func) 54 | 55 | return aliased_class 56 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/author.json: -------------------------------------------------------------------------------- 1 | { 2 | "authorMetadataId": 1, 3 | "status": "continuing", 4 | "ended": false, 5 | "authorName": "string", 6 | "authorNameLastFirst": "string", 7 | "foreignAuthorId": "string", 8 | "titleSlug": "string", 9 | "overview": "", 10 | "links": [ 11 | { 12 | "url": "string", 13 | "name": "Goodreads" 14 | } 15 | ], 16 | "lastBook": { 17 | "authorMetadataId": 1, 18 | "foreignBookId": "string", 19 | "titleSlug": "string", 20 | "title": "string", 21 | "releaseDate": "2020-11-22T00:00:00Z", 22 | "links": [ 23 | { 24 | "url": "string", 25 | "name": "Goodreads Editions" 26 | } 27 | ], 28 | "genres": [], 29 | "relatedBooks": [ 30 | 123456 31 | ], 32 | "ratings": { 33 | "votes": 98, 34 | "value": 4.54, 35 | "popularity": 444.92 36 | }, 37 | "cleanTitle": "", 38 | "monitored": true, 39 | "anyEditionOk": true, 40 | "lastInfoSync": "2022-09-16T11:12:53Z", 41 | "added": "2021-11-14T21:41:33Z", 42 | "addOptions": { 43 | "addType": "manual", 44 | "searchForNewBook": false 45 | }, 46 | "authorMetadata": null, 47 | "author": null, 48 | "editions": null, 49 | "bookFiles": null, 50 | "seriesLinks": null, 51 | "id": 392 52 | }, 53 | "images": [], 54 | "path": "string", 55 | "qualityProfileId": 1, 56 | "metadataProfileId": 1, 57 | "monitored": false, 58 | "monitorNewItems": "all", 59 | "rootFolderPath": "string", 60 | "genres": [], 61 | "cleanName": "string", 62 | "sortName": "string", 63 | "sortNameLastFirst": "string", 64 | "tags": [], 65 | "added": "2021-08-15T19:50:13Z", 66 | "ratings": { 67 | "votes": 110, 68 | "value": 4.57, 69 | "popularity": 502.70000000000005 70 | }, 71 | "statistics": { 72 | "bookFileCount": 0, 73 | "bookCount": 1, 74 | "availableBookCount": 0, 75 | "totalBookCount": 1, 76 | "sizeOnDisk": 0, 77 | "percentOfBooks": 0 78 | }, 79 | "id": 1 80 | } 81 | -------------------------------------------------------------------------------- /pyarr/models/lidarr.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | LidarrCommand = Literal[ 4 | "AlbumSearch", 5 | "ApplicationUpdateCheck", 6 | "ArtistSearch", 7 | "DownloadedAlbumsScan", 8 | "MissingAlbumSearch", 9 | "RefreshAlbum", 10 | "RefreshArtist", 11 | ] 12 | """ 13 | Lidarr commands. 14 | 15 | Note: 16 | The parameters are supplied as `**kwargs` within the `post_command` method. 17 | 18 | DownloadedAlbumsScan: 19 | Scans downloaded albums for state 20 | 21 | Args: 22 | path (str): path to files 23 | 24 | ArtistSearch: 25 | searches specified artist 26 | 27 | Args: 28 | artistId (int): ID of artist 29 | 30 | RefreshArtist: 31 | Refreshes all of the artists, or specific by ID 32 | 33 | Args: 34 | artistId (int, Optional): ID of Album 35 | 36 | RefreshAlbum: 37 | Refreshes all of the albums, or specific by ID 38 | 39 | Args: 40 | albumId (int, Optional): ID of Album 41 | 42 | ApplicationUpdateCheck: 43 | Checks for Application updates 44 | 45 | MissingAlbumSearch: 46 | Search for any missing albums 47 | 48 | AlbumSearch: 49 | Search for albums 50 | 51 | RssSync: 52 | Synchronise RSS Feeds 53 | 54 | Backup: 55 | Backup the server data 56 | 57 | """ 58 | 59 | #: Lidarr sort keys. 60 | LidarrSortKey = Literal[ 61 | "albums.title", 62 | "artistId", 63 | "date", 64 | "downloadClient", 65 | "id", 66 | "indexer", 67 | "message", 68 | "path", 69 | "progress", 70 | "protocol", 71 | "quality", 72 | "ratings", 73 | "albums.releaseDate", 74 | "sourcetitle", 75 | "status", 76 | "timeleft", 77 | "title", 78 | ] 79 | 80 | 81 | #: Lidarr Monitor types for an artist music 82 | LidarrArtistMonitor = Literal["all", "future", "missing", "existing", "first", "latest"] 83 | 84 | 85 | #: Import List schema implementations 86 | LidarrImportListSchema = Literal[ 87 | "LidarrImport", 88 | "HeadphonesImport", 89 | "LastFmTag", 90 | "LastFmUser", 91 | "LidarrLists", 92 | "MusicBrainzSeries", 93 | "SpotifyFollowedArtists", 94 | "SpotifyPlaylist", 95 | "SpotifySavedAlbums", 96 | ] 97 | 98 | #: Lidarr History Sort Keys 99 | LidarrHistorySortKey = Literal["sourceTitle", "status"] 100 | -------------------------------------------------------------------------------- /pyarr/models/readarr.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | ReadarrCommands = Literal[ 4 | "ApplicationUpdateCheck", 5 | "AuthorSearch", 6 | "BookSearch", 7 | "RefreshAuthor", 8 | "RefreshBook", 9 | "RenameAuthor", 10 | "RenameFiles", 11 | "RescanFolders", 12 | "RssSync", 13 | "Backup", 14 | "MissingBookSearch", 15 | ] 16 | """Readarr commands. 17 | 18 | Note: 19 | The parameters are supplied as `**kwargs` within the `post_command` method. 20 | 21 | ApplicationUpdateCheck: 22 | Checks for Application updates 23 | 24 | AuthorSearch: 25 | Search for specific author by ID 26 | 27 | Args: 28 | authorId (int): ID for Author 29 | 30 | BookSearch: 31 | Search for specific Book by ID 32 | 33 | Args: 34 | bookId (int): ID for Book 35 | 36 | RefreshAuthor: 37 | Refresh all Authors, or by specific ID 38 | 39 | Args: 40 | authorId (int, optional): ID for Author 41 | 42 | RefreshBook: 43 | Refresh all Books, or by specific ID 44 | 45 | Args: 46 | bookId (int, optional): ID for Book 47 | 48 | RenameAuthor: 49 | Rename all Authors, or by list of Ids 50 | 51 | Args: 52 | authorIds (list[int], optional): IDs for Authors 53 | 54 | RenameFiles: 55 | Rename all files, or by specific ID 56 | 57 | Args: 58 | authorId (int, optional): ID for Author 59 | files (str): ID of files 60 | 61 | RescanFolders: 62 | Rescans folders 63 | 64 | RssSync: 65 | Synchronise RSS Feeds 66 | 67 | Backup: 68 | Backup of the Database 69 | 70 | MissingBookSearch: 71 | Searches for any missing books 72 | """ 73 | 74 | #: Readarr sort keys. 75 | ReadarrSortKeys = Literal[ 76 | "authorId", 77 | "Books.Id", 78 | "books.releaseDate", 79 | "downloadClient", 80 | "id", 81 | "indexer", 82 | "message", 83 | "path", 84 | "progress", 85 | "protocol", 86 | "quality", 87 | "ratings", 88 | "size", 89 | "sourcetitle", 90 | "status", 91 | "timeleft", 92 | "title", 93 | ] 94 | 95 | 96 | #: Readarr search types. 97 | ReadarrSearchType = Literal["asin", "edition", "isbn", "author", "work"] 98 | 99 | 100 | #: Readarr author monitor options. 101 | ReadarrAuthorMonitor = Literal[ 102 | "all", "future", "missing", "existing", "first", "latest", "none" 103 | ] 104 | -------------------------------------------------------------------------------- /.github/workflows/release.yml.old: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | code-quality: 11 | name: 📊 Check code quality 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ["3.9", "3.10", "3.11"] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Run containers 24 | run: docker-compose -f .ci/docker-compose.yml up -d 25 | - name: Add hosts 26 | run: | 27 | sudo echo "127.0.0.1 sonarr readarr radarr lidarr prowlarr deluge jackett" | sudo tee -a /etc/hosts 28 | - name: sleep 30s for containers to start-up 29 | run: sleep 30s 30 | shell: bash 31 | - name: check ports are mapped 32 | run: docker ps 33 | - name: check one of the containers is up 34 | run: curl http://radarr:7878 35 | - name: 🧪 Check tests are passing 36 | run: | 37 | pip install poetry nox 38 | nox -s tests 39 | 40 | build-n-publish: 41 | name: Create release and publish 🐍 distribution 📦 to PyPI 42 | if: startsWith(github.ref, 'refs/tags/') 43 | needs: [code-quality] 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Getting your configuration from GitHub 47 | uses: actions/checkout@v2 48 | - name: 🛎️ Create release 49 | id: create_release 50 | uses: softprops/action-gh-release@v1 51 | with: 52 | generate_release_notes: true 53 | prerelease: ${{ contains(needs.tag_version.outputs.tag, '-rc') || contains(needs.tag_version.outputs.tag, '-b') || contains(needs.tag_version.outputs.tag, '-a') }} 54 | - name: 🏷️ Update latest tag 55 | uses: EndBug/latest-tag@latest 56 | 57 | # PyPi release steps 58 | - name: Set up Python 59 | uses: actions/setup-python@v1 60 | with: 61 | python-version: 3.11 62 | - name: Install dependencies 63 | run: | 64 | python -m pip install --upgrade pip 65 | python -m pip install poetry nox 66 | - name: Publish distribution 📦 to PyPI 67 | run: | 68 | nox -rs release -- "$PYPI_PASSWORD" 69 | env: 70 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 71 | -------------------------------------------------------------------------------- /pyarr/models/radarr.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | RadarrCommands = Literal[ 4 | "DownloadedMoviesScan", 5 | "MissingMoviesSearch", 6 | "MoviesSearch", 7 | "RefreshMovie", 8 | "RenameMovie", 9 | "RenameFiles", 10 | "Backup", 11 | ] 12 | """ 13 | Radarr commands. 14 | 15 | Note: 16 | The parameters are supplied as `**kwargs` within the `post_command` method. 17 | 18 | DownloadedMoviesScan: 19 | Scans downloaded episodes for state 20 | 21 | Args: 22 | path (str): path to files 23 | 24 | MissingMoviesSearch: 25 | Searches for any missing movies 26 | 27 | MoviesSearch: 28 | Searches for the specified movie or movies 29 | 30 | Args: 31 | movieIds (list[int]): ID of Movie or movies 32 | 33 | RefreshMovie: 34 | Refreshes all of the movies, or specific by ID 35 | 36 | Args: 37 | movieId (int, Optional): ID of Movie 38 | 39 | RenameMovie: 40 | Rename specific movie to correct format. 41 | 42 | Args: 43 | movieId (int): ID of Movie or movies 44 | movieIds (list[int]): ID of Movie or movies 45 | 46 | RescanMovie: 47 | Rescans specific movie 48 | 49 | Args: 50 | movieId (int): ID of Movie 51 | 52 | RenameFiles: 53 | Rename files to correct format 54 | 55 | Args: 56 | movieId (int): ID of Movie 57 | files (int): ID of files 58 | 59 | RssSync: 60 | Synchronise RSS Feeds 61 | 62 | Backup: 63 | Backup the server data 64 | """ 65 | 66 | #: Radarr sort keys 67 | RadarrSortKey = Literal[ 68 | "date", 69 | "downloadClient", 70 | "id", 71 | "indexer", 72 | "languages", 73 | "message", 74 | "modieId", 75 | "movies.sortTitle", 76 | "path", 77 | "progress", 78 | "protocol", 79 | "quality", 80 | "ratings", 81 | "title", 82 | "size", 83 | "sourcetitle", 84 | "status", 85 | "timeleft", 86 | ] 87 | 88 | 89 | #: Radarr event types 90 | RadarrEventType = Literal[ 91 | "unknown", 92 | "grabbed", 93 | "downloadFolderImported", 94 | "downloadFailed", 95 | "movieFileDeleted", 96 | "movieFolderImported", 97 | "movieFileRenamed", 98 | "downloadIgnored", 99 | ] 100 | 101 | #: Radarr movie availability types 102 | RadarrMonitorType = Literal["movieOnly", "movieAndCollections", "none"] 103 | 104 | #: Radarr movie availability types 105 | RadarrAvailabilityType = Literal["announced", "inCinemas", "released"] 106 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/artist.json: -------------------------------------------------------------------------------- 1 | { 2 | "artistMetadataId": 6, 3 | "status": "ended", 4 | "ended": true, 5 | "artistName": "string", 6 | "foreignArtistId": "string", 7 | "tadbId": 0, 8 | "discogsId": 0, 9 | "overview": "string", 10 | "artistType": "Person", 11 | "disambiguation": "", 12 | "links": [ 13 | { 14 | "url": "string", 15 | "name": "string" 16 | } 17 | ], 18 | "lastAlbum": { 19 | "artistMetadataId": 6, 20 | "foreignAlbumId": "string", 21 | "oldForeignAlbumIds": [], 22 | "title": "string", 23 | "overview": "string", 24 | "disambiguation": "", 25 | "releaseDate": "1977-01-01T00:00:00Z", 26 | "images": [], 27 | "links": [ 28 | { 29 | "url": "string", 30 | "name": "string" 31 | } 32 | ], 33 | "genres": [ 34 | "string" 35 | ], 36 | "albumType": "Single", 37 | "secondaryTypes": [], 38 | "ratings": { 39 | "votes": 0, 40 | "value": 0 41 | }, 42 | "cleanTitle": "string", 43 | "profileId": 0, 44 | "monitored": true, 45 | "anyReleaseOk": true, 46 | "lastInfoSync": "2022-08-08T19:45:43Z", 47 | "added": "2022-03-06T19:32:33Z", 48 | "addOptions": { 49 | "addType": "manual", 50 | "searchForNewAlbum": false 51 | }, 52 | "artistMetadata": null, 53 | "albumReleases": null, 54 | "artist": null, 55 | "id": 12 56 | }, 57 | "images": [ 58 | { 59 | "url": "/MediaCover/3/poster.jpg?lastWrite=637927957518187917", 60 | "coverType": "poster", 61 | "extension": ".jpg", 62 | "remoteUrl": "string" 63 | } 64 | ], 65 | "path": "/music/string", 66 | "qualityProfileId": 1, 67 | "metadataProfileId": 2, 68 | "monitored": true, 69 | "monitorNewItems": "all", 70 | "rootFolderPath": "/music/", 71 | "genres": [ 72 | "string" 73 | ], 74 | "cleanName": "string", 75 | "sortName": "string", 76 | "tags": [], 77 | "added": "2022-03-06T19:32:33Z", 78 | "ratings": { 79 | "votes": 9, 80 | "value": 8.4 81 | }, 82 | "statistics": { 83 | "albumCount": 1, 84 | "trackFileCount": 0, 85 | "trackCount": 4, 86 | "totalTrackCount": 4, 87 | "sizeOnDisk": 0, 88 | "percentOfTracks": 0 89 | }, 90 | "id": 3 91 | } 92 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Semantic Release 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - beta 9 | workflow_dispatch: 10 | 11 | jobs: 12 | code-quality: 13 | name: 📊 Check code quality 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["3.10", "3.11", "3.12"] 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Run containers 26 | run: docker-compose -f .ci/docker-compose.yml up -d 27 | - name: Add hosts 28 | run: | 29 | sudo echo "127.0.0.1 sonarr readarr radarr lidarr prowlarr deluge jackett" | sudo tee -a /etc/hosts 30 | - name: sleep 30s for containers to start-up 31 | run: sleep 30s 32 | shell: bash 33 | - name: check ports are mapped 34 | run: docker ps 35 | - name: check one of the containers is up 36 | run: curl http://radarr:7878 37 | - name: 🧪 Check tests are passing 38 | run: | 39 | pip install poetry nox 40 | nox -s tests 41 | release: 42 | name: Create release and publish 🐍 distribution 📦 to PyPI 43 | needs: [code-quality] 44 | runs-on: ubuntu-latest 45 | concurrency: release 46 | permissions: 47 | id-token: write 48 | contents: write 49 | 50 | steps: 51 | - uses: actions/checkout@v3 52 | with: 53 | fetch-depth: 0 54 | 55 | - name: Setup Node.js 56 | uses: actions/setup-node@v4 57 | with: 58 | node-version: 20 59 | 60 | - uses: actions/setup-python@v5 61 | with: 62 | python-version: '3.12' 63 | 64 | - name: Install dependencies 65 | run: | 66 | python -m pip install --upgrade pip 67 | python -m pip install poetry nox 68 | npm install @semantic-release/changelog 69 | npm install @semantic-release/exec 70 | npm install @semantic-release/git 71 | npm install @semantic-release/github 72 | npm install conventional-changelog-conventionalcommits@7.0.2 73 | npm install semantic-release-pypi 74 | 75 | - name: Run Release 76 | run: | 77 | nox -rs release -- "$PYPI_PASSWORD" 78 | env: 79 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | - name: 🏷️ Update latest tag 83 | uses: EndBug/latest-tag@latest 84 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/track_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "artistId": 3, 4 | "foreignTrackId": "0b804321-1b87-3f5e-8f2b-b81026c8707a", 5 | "foreignRecordingId": "5ed4a636-2e39-446c-8895-9a1166744b2f", 6 | "trackFileId": 0, 7 | "albumId": 12, 8 | "explicit": false, 9 | "absoluteTrackNumber": 1, 10 | "trackNumber": "1", 11 | "title": "Bat Out of Hell (single version)", 12 | "duration": 442466, 13 | "mediumNumber": 1, 14 | "hasFile": false, 15 | "ratings": { 16 | "votes": 0, 17 | "value": 0 18 | }, 19 | "grabbed": false, 20 | "id": 6009 21 | }, 22 | { 23 | "artistId": 3, 24 | "foreignTrackId": "e755ed89-eda8-3b83-86af-0f37fb0aebab", 25 | "foreignRecordingId": "0604a84a-6611-49b9-91b9-d426f4212609", 26 | "trackFileId": 0, 27 | "albumId": 12, 28 | "explicit": false, 29 | "absoluteTrackNumber": 2, 30 | "trackNumber": "2", 31 | "title": "Read 'em and Weep", 32 | "duration": 327560, 33 | "mediumNumber": 1, 34 | "hasFile": false, 35 | "ratings": { 36 | "votes": 0, 37 | "value": 0 38 | }, 39 | "grabbed": false, 40 | "id": 6010 41 | }, 42 | { 43 | "artistId": 3, 44 | "foreignTrackId": "0e57afef-a6bf-3770-89e9-4e0e691c6bdf", 45 | "foreignRecordingId": "0140c7a2-edac-41c7-9747-7907d5a32d39", 46 | "trackFileId": 0, 47 | "albumId": 12, 48 | "explicit": false, 49 | "absoluteTrackNumber": 3, 50 | "trackNumber": "3", 51 | "title": "Lost Boys and Golden Girls", 52 | "duration": 278866, 53 | "mediumNumber": 1, 54 | "hasFile": false, 55 | "ratings": { 56 | "votes": 0, 57 | "value": 0 58 | }, 59 | "grabbed": false, 60 | "id": 6011 61 | }, 62 | { 63 | "artistId": 3, 64 | "foreignTrackId": "8dbed555-a6de-3887-bebd-088f03498da3", 65 | "foreignRecordingId": "83d088a5-2f6f-481e-9542-38d1be1c385f", 66 | "trackFileId": 0, 67 | "albumId": 12, 68 | "explicit": false, 69 | "absoluteTrackNumber": 4, 70 | "trackNumber": "4", 71 | "title": "Rock and Roll Dreams Come Through", 72 | "duration": 389200, 73 | "mediumNumber": 1, 74 | "hasFile": false, 75 | "ratings": { 76 | "votes": 0, 77 | "value": 0 78 | }, 79 | "grabbed": false, 80 | "id": 6012 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/author_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "authorMetadataId": 1, 4 | "status": "continuing", 5 | "ended": false, 6 | "authorName": "string", 7 | "authorNameLastFirst": "string", 8 | "foreignAuthorId": "string", 9 | "titleSlug": "string", 10 | "overview": "", 11 | "links": [ 12 | { 13 | "url": "string", 14 | "name": "Goodreads" 15 | } 16 | ], 17 | "lastBook": { 18 | "authorMetadataId": 1, 19 | "foreignBookId": "string", 20 | "titleSlug": "string", 21 | "title": "string", 22 | "releaseDate": "2020-11-22T00:00:00Z", 23 | "links": [ 24 | { 25 | "url": "string", 26 | "name": "Goodreads Editions" 27 | } 28 | ], 29 | "genres": [], 30 | "relatedBooks": [ 31 | 123456 32 | ], 33 | "ratings": { 34 | "votes": 98, 35 | "value": 4.54, 36 | "popularity": 444.92 37 | }, 38 | "cleanTitle": "", 39 | "monitored": true, 40 | "anyEditionOk": true, 41 | "lastInfoSync": "2022-09-16T11:12:53Z", 42 | "added": "2021-11-14T21:41:33Z", 43 | "addOptions": { 44 | "addType": "manual", 45 | "searchForNewBook": false 46 | }, 47 | "authorMetadata": null, 48 | "author": null, 49 | "editions": null, 50 | "bookFiles": null, 51 | "seriesLinks": null, 52 | "id": 392 53 | }, 54 | "images": [], 55 | "path": "string", 56 | "qualityProfileId": 1, 57 | "metadataProfileId": 1, 58 | "monitored": false, 59 | "monitorNewItems": "all", 60 | "rootFolderPath": "string", 61 | "genres": [], 62 | "cleanName": "string", 63 | "sortName": "string", 64 | "sortNameLastFirst": "string", 65 | "tags": [], 66 | "added": "2021-08-15T19:50:13Z", 67 | "ratings": { 68 | "votes": 110, 69 | "value": 4.57, 70 | "popularity": 502.70000000000005 71 | }, 72 | "statistics": { 73 | "bookFileCount": 0, 74 | "bookCount": 1, 75 | "availableBookCount": 0, 76 | "totalBookCount": 1, 77 | "sizeOnDisk": 0, 78 | "percentOfBooks": 0 79 | }, 80 | "id": 1 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /tests/fixtures/radarr/movie_blocklist.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "movieId": 176, 4 | "sourceTitle": "string", 5 | "languages": [ 6 | { 7 | "id": 1, 8 | "name": "English" 9 | } 10 | ], 11 | "quality": { 12 | "quality": { 13 | "id": 7, 14 | "name": "Bluray-1080p", 15 | "source": "bluray", 16 | "resolution": 1080, 17 | "modifier": "none" 18 | }, 19 | "revision": { 20 | "version": 1, 21 | "real": 0, 22 | "isRepack": false 23 | } 24 | }, 25 | "customFormats": [], 26 | "date": "2019-04-25T10: 45: 21Z", 27 | "protocol": "torrent", 28 | "indexer": "Jackett", 29 | "message": "Manually marked as failed", 30 | "id": 3 31 | }, 32 | { 33 | "movieId": 176, 34 | "sourceTitle": "string", 35 | "languages": [ 36 | { 37 | "id": 1, 38 | "name": "English" 39 | } 40 | ], 41 | "quality": { 42 | "quality": { 43 | "id": 7, 44 | "name": "Bluray-1080p", 45 | "source": "bluray", 46 | "resolution": 1080, 47 | "modifier": "none" 48 | }, 49 | "revision": { 50 | "version": 1, 51 | "real": 0, 52 | "isRepack": false 53 | } 54 | }, 55 | "customFormats": [], 56 | "date": "2019-04-25T10: 45: 54Z", 57 | "protocol": "torrent", 58 | "indexer": "Jackett", 59 | "message": "Manually marked as failed", 60 | "id": 4 61 | }, 62 | { 63 | "movieId": 176, 64 | "sourceTitle": "string", 65 | "languages": [ 66 | { 67 | "id": 1, 68 | "name": "English" 69 | } 70 | ], 71 | "quality": { 72 | "quality": { 73 | "id": 7, 74 | "name": "Bluray-1080p", 75 | "source": "bluray", 76 | "resolution": 1080, 77 | "modifier": "none" 78 | }, 79 | "revision": { 80 | "version": 1, 81 | "real": 0, 82 | "isRepack": false 83 | } 84 | }, 85 | "customFormats": [], 86 | "date": "2019-04-25T10: 45: 55Z", 87 | "protocol": "torrent", 88 | "indexer": "Jackett", 89 | "message": "Manually marked as failed", 90 | "id": 5 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "string", 3 | "authorTitle": "string", 4 | "seriesTitle": "", 5 | "disambiguation": "", 6 | "authorId": 24, 7 | "foreignBookId": "string", 8 | "titleSlug": "string", 9 | "monitored": true, 10 | "anyEditionOk": true, 11 | "ratings": { 12 | "votes": 0, 13 | "value": 0, 14 | "popularity": 0 15 | }, 16 | "releaseDate": "2022-11-29T00:00:00Z", 17 | "pageCount": 0, 18 | "genres": [ 19 | "string" 20 | ], 21 | "author": { 22 | "authorMetadataId": 12, 23 | "status": "continuing", 24 | "ended": false, 25 | "authorName": "string", 26 | "authorNameLastFirst": "string", 27 | "foreignAuthorId": "string", 28 | "titleSlug": "string", 29 | "overview": "string", 30 | "links": [ 31 | { 32 | "url": "string", 33 | "name": "Goodreads" 34 | } 35 | ], 36 | "images": [ 37 | { 38 | "url": "string", 39 | "coverType": "poster", 40 | "extension": ".jpg" 41 | } 42 | ], 43 | "path": "string", 44 | "qualityProfileId": 1, 45 | "metadataProfileId": 1, 46 | "monitored": false, 47 | "monitorNewItems": "all", 48 | "genres": [], 49 | "cleanName": "string", 50 | "sortName": "string", 51 | "sortNameLastFirst": "string", 52 | "tags": [], 53 | "added": "2022-05-26T09:19:35Z", 54 | "ratings": { 55 | "votes": 280381, 56 | "value": 4.03, 57 | "popularity": 1129935.4300000002 58 | }, 59 | "statistics": { 60 | "bookFileCount": 0, 61 | "bookCount": 0, 62 | "availableBookCount": 0, 63 | "totalBookCount": 0, 64 | "sizeOnDisk": 0, 65 | "percentOfBooks": 0 66 | }, 67 | "id": 24 68 | }, 69 | "images": [ 70 | { 71 | "url": "string", 72 | "coverType": "cover", 73 | "extension": ".jpg" 74 | } 75 | ], 76 | "links": [ 77 | { 78 | "url": "string", 79 | "name": "Goodreads Editions" 80 | }, 81 | { 82 | "url": "string", 83 | "name": "Goodreads Book" 84 | } 85 | ], 86 | "statistics": { 87 | "bookFileCount": 0, 88 | "bookCount": 0, 89 | "totalBookCount": 1, 90 | "sizeOnDisk": 0, 91 | "percentOfBooks": 0 92 | }, 93 | "added": "2022-09-10T16:37:29Z", 94 | "grabbed": false, 95 | "id": 1321 96 | } 97 | -------------------------------------------------------------------------------- /pyarr/models/sonarr.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | SonarrCommands = Literal[ 4 | "Backup", 5 | "DownloadedEpisodesScan", 6 | "EpisodeSearch", 7 | "missingEpisodeSearch", 8 | "RefreshSeries", 9 | "RenameSeries", 10 | "RenameFiles", 11 | "RescanSeries", 12 | "RssSync", 13 | "SeasonSearch", 14 | "SeriesSearch", 15 | ] 16 | """Sonarr commands. 17 | 18 | Note: 19 | The parameters are supplied as `**kwargs` within the `post_command` method. 20 | 21 | Backup: 22 | Backup of the Database 23 | 24 | DownloadedEpisodesScan: 25 | Scans downloaded episodes for state 26 | 27 | Args: 28 | path (str): path to files 29 | 30 | EpisodeSearch: 31 | Searches for all episondes, or specific ones in supplied list 32 | 33 | Args: 34 | episodeIds (lsit[int], optional): One or more episodeIds in a list 35 | 36 | missingEpisodeSearch: 37 | Searches for any missing episodes 38 | 39 | RefreshSeries: 40 | Refreshes all series, if a `seriesId` is provided only that series will be refreshed 41 | 42 | Args: 43 | seriesId (int, optional): ID of specific series to be refreshed. 44 | 45 | RenameSeries: 46 | Renames series to the expected naming format. 47 | 48 | Args: 49 | seriesIds (list[int]): List of Series IDs to rename. 50 | 51 | RenameFiles: 52 | Renames files to the expected naming format. 53 | 54 | Args: 55 | seriesId (int, optional): ID of series files relate to 56 | files (list[int]): List of File IDs to rename. 57 | 58 | RescanSeries: 59 | Re-scan all series, if `seriesId` is provided only that series will be Re-scanned. 60 | 61 | Args: 62 | seriesId (int, optional): ID of series to search for. 63 | 64 | RssSync: 65 | Synchronise RSS Feeds 66 | 67 | SeasonSearch: 68 | Search for specific season. 69 | 70 | Args: 71 | seriesId (int): Series in which the season resides. 72 | seasonNumber (int): Season to search for. 73 | 74 | SeriesSearch: 75 | Searches for specific series. 76 | 77 | Args: 78 | seriesId (int): ID of series to search for. 79 | """ 80 | 81 | 82 | #: Sonarr sort keys. 83 | SonarrSortKey = Literal[ 84 | "airDateUtc", 85 | "date", 86 | "downloadClient", 87 | "episode", 88 | "episodeId", 89 | "episode.title", 90 | "id", 91 | "indexer", 92 | "language", 93 | "message", 94 | "path", 95 | "progress", 96 | "protocol", 97 | "quality", 98 | "ratings", 99 | "seriesId", 100 | "series.sortTitle", 101 | "size", 102 | "sourcetitle", 103 | "status", 104 | "timeleft", 105 | ] 106 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/lookup_author.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "authorMetadataId": 0, 4 | "status": "continuing", 5 | "ended": false, 6 | "authorName": "string", 7 | "authorNameLastFirst": "string", 8 | "foreignAuthorId": "1234567", 9 | "titleSlug": "1234567", 10 | "overview": "string", 11 | "links": [ 12 | { 13 | "url": "string", 14 | "name": "Goodreads" 15 | } 16 | ], 17 | "images": [ 18 | { 19 | "url": "string", 20 | "coverType": "poster", 21 | "extension": ".jpg" 22 | } 23 | ], 24 | "remotePoster": "string", 25 | "qualityProfileId": 0, 26 | "metadataProfileId": 0, 27 | "monitored": false, 28 | "monitorNewItems": "all", 29 | "genres": [], 30 | "cleanName": "string", 31 | "sortName": "string", 32 | "sortNameLastFirst": "string", 33 | "tags": [], 34 | "added": "0001-01-01T00:01:00Z", 35 | "ratings": { 36 | "votes": 31221602, 37 | "value": 4.46, 38 | "popularity": 139248344.92 39 | }, 40 | "statistics": { 41 | "bookFileCount": 0, 42 | "bookCount": 0, 43 | "availableBookCount": 0, 44 | "totalBookCount": 0, 45 | "sizeOnDisk": 0, 46 | "percentOfBooks": 0 47 | } 48 | }, 49 | { 50 | "authorMetadataId": 0, 51 | "status": "continuing", 52 | "ended": false, 53 | "authorName": "string", 54 | "authorNameLastFirst": "string", 55 | "foreignAuthorId": "1234567", 56 | "titleSlug": "1234567", 57 | "overview": "", 58 | "links": [ 59 | { 60 | "url": "string", 61 | "name": "Goodreads" 62 | } 63 | ], 64 | "images": [], 65 | "qualityProfileId": 0, 66 | "metadataProfileId": 0, 67 | "monitored": false, 68 | "monitorNewItems": "all", 69 | "genres": [], 70 | "cleanName": "string", 71 | "sortName": "string", 72 | "sortNameLastFirst": "string", 73 | "tags": [], 74 | "added": "0001-01-01T00:01:00Z", 75 | "ratings": { 76 | "votes": 18032, 77 | "value": 4.72, 78 | "popularity": 85111.04 79 | }, 80 | "statistics": { 81 | "bookFileCount": 0, 82 | "bookCount": 0, 83 | "availableBookCount": 0, 84 | "totalBookCount": 0, 85 | "sizeOnDisk": 0, 86 | "percentOfBooks": 0 87 | } 88 | } 89 | ] 90 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/artist_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "artistMetadataId": 6, 4 | "status": "ended", 5 | "ended": true, 6 | "artistName": "string", 7 | "foreignArtistId": "string", 8 | "tadbId": 0, 9 | "discogsId": 0, 10 | "overview": "string", 11 | "artistType": "Person", 12 | "disambiguation": "", 13 | "links": [ 14 | { 15 | "url": "string", 16 | "name": "string" 17 | } 18 | ], 19 | "lastAlbum": { 20 | "artistMetadataId": 6, 21 | "foreignAlbumId": "string", 22 | "oldForeignAlbumIds": [], 23 | "title": "string", 24 | "overview": "string", 25 | "disambiguation": "", 26 | "releaseDate": "1977-01-01T00:00:00Z", 27 | "images": [], 28 | "links": [ 29 | { 30 | "url": "string", 31 | "name": "string" 32 | } 33 | ], 34 | "genres": [ 35 | "string" 36 | ], 37 | "albumType": "Single", 38 | "secondaryTypes": [], 39 | "ratings": { 40 | "votes": 0, 41 | "value": 0 42 | }, 43 | "cleanTitle": "string", 44 | "profileId": 0, 45 | "monitored": true, 46 | "anyReleaseOk": true, 47 | "lastInfoSync": "2022-08-08T19:45:43Z", 48 | "added": "2022-03-06T19:32:33Z", 49 | "addOptions": { 50 | "addType": "manual", 51 | "searchForNewAlbum": false 52 | }, 53 | "artistMetadata": null, 54 | "albumReleases": null, 55 | "artist": null, 56 | "id": 12 57 | }, 58 | "images": [ 59 | { 60 | "url": "/MediaCover/3/banner.jpg?lastWrite=637821919538014821", 61 | "coverType": "banner", 62 | "extension": ".jpg", 63 | "remoteUrl": "string" 64 | } 65 | ], 66 | "path": "/music/string", 67 | "qualityProfileId": 1, 68 | "metadataProfileId": 2, 69 | "monitored": true, 70 | "monitorNewItems": "all", 71 | "rootFolderPath": "/music/", 72 | "genres": [ 73 | "string" 74 | ], 75 | "cleanName": "string", 76 | "sortName": "string", 77 | "tags": [], 78 | "added": "2022-03-06T19:32:33Z", 79 | "ratings": { 80 | "votes": 9, 81 | "value": 8.4 82 | }, 83 | "statistics": { 84 | "albumCount": 1, 85 | "trackFileCount": 0, 86 | "trackCount": 4, 87 | "totalTrackCount": 4, 88 | "sizeOnDisk": 0, 89 | "percentOfTracks": 0 90 | }, 91 | "id": 3 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/book_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "string", 4 | "authorTitle": "string", 5 | "seriesTitle": "", 6 | "disambiguation": "", 7 | "authorId": 24, 8 | "foreignBookId": "string", 9 | "titleSlug": "string", 10 | "monitored": true, 11 | "anyEditionOk": true, 12 | "ratings": { 13 | "votes": 0, 14 | "value": 0, 15 | "popularity": 0 16 | }, 17 | "releaseDate": "2022-11-29T00:00:00Z", 18 | "pageCount": 0, 19 | "genres": [ 20 | "string" 21 | ], 22 | "author": { 23 | "authorMetadataId": 12, 24 | "status": "continuing", 25 | "ended": false, 26 | "authorName": "string", 27 | "authorNameLastFirst": "string", 28 | "foreignAuthorId": "string", 29 | "titleSlug": "string", 30 | "overview": "string", 31 | "links": [ 32 | { 33 | "url": "string", 34 | "name": "Goodreads" 35 | } 36 | ], 37 | "images": [ 38 | { 39 | "url": "string", 40 | "coverType": "poster", 41 | "extension": ".jpg" 42 | } 43 | ], 44 | "path": "string", 45 | "qualityProfileId": 1, 46 | "metadataProfileId": 1, 47 | "monitored": false, 48 | "monitorNewItems": "all", 49 | "genres": [], 50 | "cleanName": "string", 51 | "sortName": "string", 52 | "sortNameLastFirst": "string", 53 | "tags": [], 54 | "added": "2022-05-26T09:19:35Z", 55 | "ratings": { 56 | "votes": 280381, 57 | "value": 4.03, 58 | "popularity": 1129935.4300000002 59 | }, 60 | "statistics": { 61 | "bookFileCount": 0, 62 | "bookCount": 0, 63 | "availableBookCount": 0, 64 | "totalBookCount": 0, 65 | "sizeOnDisk": 0, 66 | "percentOfBooks": 0 67 | }, 68 | "id": 24 69 | }, 70 | "images": [ 71 | { 72 | "url": "string", 73 | "coverType": "cover", 74 | "extension": ".jpg" 75 | } 76 | ], 77 | "links": [ 78 | { 79 | "url": "string", 80 | "name": "Goodreads Editions" 81 | }, 82 | { 83 | "url": "string", 84 | "name": "Goodreads Book" 85 | } 86 | ], 87 | "statistics": { 88 | "bookFileCount": 0, 89 | "bookCount": 0, 90 | "totalBookCount": 1, 91 | "sizeOnDisk": 0, 92 | "percentOfBooks": 0 93 | }, 94 | "added": "2022-09-10T16:37:29Z", 95 | "grabbed": false, 96 | "id": 1321 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: 'type/bug' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **NOTE:** Before you start, the following should be completed. 9 | 10 | - Read [documentation](https://docs.totaldebug.uk/pyarr) to ensure the correct setup. 11 | - Make sure no [similar issues(including closed ones)](https://github.com/totaldebug/pyarr/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Fbug) exists. 12 | - Make sure the request is based on the latest code in the `master` branch. 13 | 14 | Thanks for taking the time to assist with improving this project! 15 | - type: checkboxes 16 | attributes: 17 | label: Is there an existing issue for this? 18 | description: Please search to see if an issue already exists for the bug you encountered. 19 | options: 20 | - label: I have searched the existing issues 21 | required: true 22 | - type: textarea 23 | id: current-behaviour 24 | attributes: 25 | label: Current Behaviour 26 | description: A concise description of what you're experiencing. 27 | placeholder: Tell us what you see! 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Steps To Reproduce 33 | description: Steps to reproduce the behavior. 34 | placeholder: | 35 | 1. In this environment... 36 | 2. With this config... 37 | 3. Run '...' 38 | 4. See error... 39 | validations: 40 | required: false 41 | - type: textarea 42 | id: expected-behaviour 43 | attributes: 44 | label: Expected behaviour 45 | description: A concise description of what you expected to happen. 46 | placeholder: Tell us what you should see! 47 | validations: 48 | required: true 49 | - type: input 50 | id: pyarr-version 51 | attributes: 52 | label: Pyarr Version 53 | description: The version of the Pyarr you have installed 54 | validations: 55 | required: true 56 | - type: input 57 | id: python-version 58 | attributes: 59 | label: Python Version 60 | description: The version of Python you have installed 61 | validations: 62 | required: true 63 | - type: textarea 64 | id: example 65 | attributes: 66 | label: Example Code 67 | description: Please copy and paste an example code. This will be automatically formatted into code, so no need for backticks. 68 | render: shell 69 | - type: textarea 70 | id: logs 71 | attributes: 72 | label: Relevant log output 73 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 74 | render: shell 75 | validations: 76 | required: false 77 | - type: checkboxes 78 | id: terms 79 | attributes: 80 | label: Code of Conduct 81 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/marksie1988/.github/blob/main/.github/CODE_OF_CONDUCT.md) 82 | options: 83 | - label: I agree to follow this project's Code of Conduct 84 | required: true 85 | -------------------------------------------------------------------------------- /tests/fixtures/radarr/moviefiles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "movieId": 17, 4 | "relativePath": "string", 5 | "path": "string", 6 | "size": 5536497109, 7 | "dateAdded": "2018-01-21T02:11:37Z", 8 | "sceneName": "string", 9 | "indexerFlags": 0, 10 | "quality": { 11 | "quality": { 12 | "id": 3, 13 | "name": "WEBDL-1080p", 14 | "source": "webdl", 15 | "resolution": 1080, 16 | "modifier": "none" 17 | }, 18 | "revision": { 19 | "version": 1, 20 | "real": 0, 21 | "isRepack": false 22 | } 23 | }, 24 | "customFormats": [], 25 | "mediaInfo": { 26 | "audioBitrate": 384000, 27 | "audioChannels": 5.1, 28 | "audioCodec": "AC3", 29 | "audioLanguages": "eng", 30 | "audioStreamCount": 1, 31 | "videoBitDepth": 8, 32 | "videoBitrate": 0, 33 | "videoCodec": "x264", 34 | "videoDynamicRangeType": "", 35 | "videoFps": 23.976, 36 | "resolution": "1920x800", 37 | "runTime": "2:10:44", 38 | "scanType": "Progressive", 39 | "subtitles": "eng" 40 | }, 41 | "qualityCutoffNotMet": false, 42 | "languages": [ 43 | { 44 | "id": 1, 45 | "name": "English" 46 | } 47 | ], 48 | "releaseGroup": "string", 49 | "edition": "", 50 | "id": 1 51 | }, 52 | { 53 | "movieId": 17, 54 | "relativePath": "string", 55 | "path": "string", 56 | "size": 5536497109, 57 | "dateAdded": "2018-01-21T02:11:37Z", 58 | "sceneName": "string", 59 | "indexerFlags": 0, 60 | "quality": { 61 | "quality": { 62 | "id": 3, 63 | "name": "WEBDL-1080p", 64 | "source": "webdl", 65 | "resolution": 1080, 66 | "modifier": "none" 67 | }, 68 | "revision": { 69 | "version": 1, 70 | "real": 0, 71 | "isRepack": false 72 | } 73 | }, 74 | "customFormats": [], 75 | "mediaInfo": { 76 | "audioBitrate": 384000, 77 | "audioChannels": 5.1, 78 | "audioCodec": "AC3", 79 | "audioLanguages": "eng", 80 | "audioStreamCount": 1, 81 | "videoBitDepth": 8, 82 | "videoBitrate": 0, 83 | "videoCodec": "x264", 84 | "videoDynamicRangeType": "", 85 | "videoFps": 23.976, 86 | "resolution": "1920x800", 87 | "runTime": "2:10:44", 88 | "scanType": "Progressive", 89 | "subtitles": "eng" 90 | }, 91 | "qualityCutoffNotMet": false, 92 | "languages": [ 93 | { 94 | "id": 1, 95 | "name": "English" 96 | } 97 | ], 98 | "releaseGroup": "string", 99 | "edition": "", 100 | "id": 2 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /tests/fixtures/common/blocklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 20, 4 | "sortKey": "date", 5 | "sortDirection": "descending", 6 | "totalRecords": 0, 7 | "records": [ 8 | { 9 | "movieId": 176, 10 | "sourceTitle": "string", 11 | "languages": [ 12 | { 13 | "id": 1, 14 | "name": "English" 15 | } 16 | ], 17 | "quality": { 18 | "quality": { 19 | "id": 7, 20 | "name": "Bluray-1080p", 21 | "source": "bluray", 22 | "resolution": 1080, 23 | "modifier": "none" 24 | }, 25 | "revision": { 26 | "version": 1, 27 | "real": 0, 28 | "isRepack": false 29 | } 30 | }, 31 | "customFormats": [], 32 | "date": "2019-04-25T10: 45: 21Z", 33 | "protocol": "torrent", 34 | "indexer": "Jackett", 35 | "message": "Manually marked as failed", 36 | "id": 3 37 | }, 38 | { 39 | "movieId": 176, 40 | "sourceTitle": "string", 41 | "languages": [ 42 | { 43 | "id": 1, 44 | "name": "English" 45 | } 46 | ], 47 | "quality": { 48 | "quality": { 49 | "id": 7, 50 | "name": "Bluray-1080p", 51 | "source": "bluray", 52 | "resolution": 1080, 53 | "modifier": "none" 54 | }, 55 | "revision": { 56 | "version": 1, 57 | "real": 0, 58 | "isRepack": false 59 | } 60 | }, 61 | "customFormats": [], 62 | "date": "2019-04-25T10: 45: 54Z", 63 | "protocol": "torrent", 64 | "indexer": "Jackett", 65 | "message": "Manually marked as failed", 66 | "id": 4 67 | }, 68 | { 69 | "movieId": 176, 70 | "sourceTitle": "string", 71 | "languages": [ 72 | { 73 | "id": 1, 74 | "name": "English" 75 | } 76 | ], 77 | "quality": { 78 | "quality": { 79 | "id": 7, 80 | "name": "Bluray-1080p", 81 | "source": "bluray", 82 | "resolution": 1080, 83 | "modifier": "none" 84 | }, 85 | "revision": { 86 | "version": 1, 87 | "real": 0, 88 | "isRepack": false 89 | } 90 | }, 91 | "customFormats": [], 92 | "date": "2019-04-25T10: 45: 55Z", 93 | "protocol": "torrent", 94 | "indexer": "Jackett", 95 | "message": "Manually marked as failed", 96 | "id": 5 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/album.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "string", 3 | "disambiguation": "", 4 | "overview": "", 5 | "artistId": 7, 6 | "foreignAlbumId": "string", 7 | "monitored": true, 8 | "anyReleaseOk": true, 9 | "profileId": 1, 10 | "duration": 8926398, 11 | "albumType": "Album", 12 | "secondaryTypes": [], 13 | "mediumCount": 2, 14 | "ratings": { 15 | "votes": 0, 16 | "value": 0 17 | }, 18 | "releaseDate": "2008-01-01T00:00:00Z", 19 | "releases": [ 20 | { 21 | "id": 2439, 22 | "albumId": 100, 23 | "foreignReleaseId": "string", 24 | "title": "string", 25 | "status": "Official", 26 | "duration": 8926398, 27 | "trackCount": 20, 28 | "media": [ 29 | { 30 | "mediumNumber": 2, 31 | "mediumName": "string", 32 | "mediumFormat": "CD" 33 | } 34 | ], 35 | "mediumCount": 2, 36 | "disambiguation": "string", 37 | "country": [ 38 | "string" 39 | ], 40 | "label": [ 41 | "string" 42 | ], 43 | "format": "2xCD", 44 | "monitored": true 45 | } 46 | ], 47 | "genres": [], 48 | "media": [ 49 | { 50 | "mediumNumber": 2, 51 | "mediumName": "string", 52 | "mediumFormat": "string" 53 | } 54 | ], 55 | "artist": { 56 | "artistMetadataId": 1, 57 | "status": "continuing", 58 | "ended": false, 59 | "artistName": "string", 60 | "foreignArtistId": "string", 61 | "tadbId": 0, 62 | "discogsId": 0, 63 | "overview": "string", 64 | "artistType": "Group", 65 | "disambiguation": "", 66 | "links": [ 67 | { 68 | "url": "string", 69 | "name": "string" 70 | } 71 | ], 72 | "images": [ 73 | { 74 | "url": "string", 75 | "coverType": "poster", 76 | "extension": ".jpg" 77 | } 78 | ], 79 | "path": "/music/string", 80 | "qualityProfileId": 1, 81 | "metadataProfileId": 1, 82 | "monitored": true, 83 | "monitorNewItems": "all", 84 | "genres": [ 85 | "string" 86 | ], 87 | "cleanName": "string", 88 | "sortName": "string", 89 | "tags": [], 90 | "added": "2022-03-07T13:55:42Z", 91 | "ratings": { 92 | "votes": 66, 93 | "value": 8.3 94 | }, 95 | "statistics": { 96 | "albumCount": 0, 97 | "trackFileCount": 0, 98 | "trackCount": 0, 99 | "totalTrackCount": 0, 100 | "sizeOnDisk": 0, 101 | "percentOfTracks": 0 102 | }, 103 | "id": 7 104 | }, 105 | "images": [], 106 | "links": [], 107 | "statistics": { 108 | "trackFileCount": 0, 109 | "trackCount": 20, 110 | "totalTrackCount": 20, 111 | "sizeOnDisk": 0, 112 | "percentOfTracks": 0 113 | }, 114 | "grabbed": false, 115 | "id": 100 116 | } 117 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from os.path import abspath, dirname, join 4 | import re 5 | import sys 6 | 7 | import toml 8 | 9 | path = dirname(dirname(abspath(__file__))) 10 | sys.path.append(path) 11 | sys.path.append(join(path, "pyarr")) 12 | 13 | project = "pyarr" 14 | slug = re.sub(r"\W+", "-", project.lower()) 15 | copyright = "2021, Steven Marks, TotalDebug" 16 | author = "Steven Marks, TotalDebug" 17 | 18 | 19 | # The short X.Y version 20 | def get_version(): 21 | with open("../pyproject.toml") as f: 22 | config = toml.load(f) 23 | return config["tool"]["poetry"]["version"] 24 | 25 | 26 | version = get_version() 27 | # The full version, including alpha/beta/rc tags 28 | release = "" 29 | 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.coverage", 33 | "sphinx.ext.viewcode", 34 | "sphinx_rtd_theme", 35 | "sphinx.ext.napoleon", 36 | "sphinx.ext.autosectionlabel", 37 | "myst_parser", 38 | ] 39 | 40 | # -- Napoleon Settings ----------------------------------------------------- 41 | napoleon_google_docstring = True 42 | napoleon_numpy_docstring = False 43 | napoleon_include_init_with_doc = False 44 | napoleon_include_private_with_doc = False 45 | napoleon_include_special_with_doc = False 46 | napoleon_use_admonition_for_examples = False 47 | napoleon_use_admonition_for_notes = False 48 | napoleon_use_admonition_for_references = False 49 | napoleon_use_ivar = True 50 | napoleon_use_param = True 51 | napoleon_use_rtype = True 52 | napoleon_use_keyword = True 53 | autodoc_member_order = "bysource" 54 | 55 | templates_path = ["_templates"] 56 | source_suffix = ".rst" 57 | 58 | master_doc = "index" 59 | language = "en" 60 | gettext_compact = False 61 | 62 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 63 | 64 | pygments_style = "default" 65 | 66 | 67 | html_theme = "sphinx_rtd_theme" 68 | html_theme_options = { 69 | "logo_only": True, 70 | "navigation_depth": 5, 71 | } 72 | 73 | htmlhelp_basename = slug 74 | 75 | 76 | # -- Options for LaTeX output ------------------------------------------------ 77 | 78 | latex_documents = [ 79 | ("index", "{0}.tex".format(slug), project, author, "manual"), 80 | ] 81 | 82 | 83 | man_pages = [("index", slug, project, [author], 1)] 84 | 85 | 86 | # -- Options for Texinfo output ---------------------------------------------- 87 | 88 | # Grouping the document tree into Texinfo files. List of tuples 89 | # (source start file, target name, title, author, 90 | # dir menu entry, description, category) 91 | texinfo_documents = [ 92 | ( 93 | master_doc, 94 | "PyArr", 95 | "PyArr Documentation", 96 | author, 97 | "PyArr", 98 | "One line description of project.", 99 | "Miscellaneous", 100 | ), 101 | ] 102 | 103 | 104 | # -- Options for Epub output ------------------------------------------------- 105 | 106 | # Bibliographic Dublin Core info. 107 | epub_title = project 108 | 109 | # The unique identifier of the text. This can be a ISBN number 110 | # or the project homepage. 111 | # 112 | # epub_identifier = '' 113 | 114 | # A unique identification for the text. 115 | # 116 | # epub_uid = '' 117 | 118 | # A list of files that should not be packed into the epub file. 119 | epub_exclude_files = ["search.html"] 120 | 121 | 122 | # -- Extension configuration ------------------------------------------------- 123 | -------------------------------------------------------------------------------- /pyarr/models/common.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | #: Pyarr sort direction 4 | PyarrSortDirection = Literal["ascending", "default", "descending"] 5 | 6 | 7 | PyarrLogSortKey = Literal[ 8 | "Id", "level", "time", "logger", "message", "exception", "exceptionType" 9 | ] 10 | """Log Sort Keys 11 | 12 | Note: 13 | There may be more, but these are not well documented 14 | within Arr api docs. 15 | """ 16 | 17 | 18 | PyarrBlocklistSortKey = Literal["date"] 19 | """Block list sort keys 20 | 21 | Note: 22 | There may be more, but these are not well documented 23 | within Arr api docs. 24 | """ 25 | 26 | PyarrHistorySortKey = Literal[ 27 | "id", 28 | "date", 29 | "eventType", 30 | "series.title", 31 | "movieFile.relativePath", 32 | "sourceTitle", 33 | "status", 34 | ] 35 | """History sort keys 36 | 37 | series.title (Sonarr) 38 | episode.title (Sonarr) 39 | status (Lidarr only) 40 | 41 | Note: 42 | There may be more, but these are not well documented 43 | within Arr api docs. 44 | """ 45 | 46 | PyarrTaskSortKey = Literal["timeleft"] 47 | """Task sort keys 48 | 49 | Note: 50 | There may be more, but these are not well documented 51 | within Arr api docs. 52 | """ 53 | 54 | PyarrLogFilterKey = Literal["level"] 55 | """Log filter keys 56 | 57 | Note: 58 | There may be more, but these are not well documented 59 | within Arr api docs. 60 | """ 61 | 62 | PyarrLogFilterValue = Literal["all", "info", "warn", "error"] 63 | """Log filter values 64 | 65 | Note: 66 | There may be more, but these are not well documented 67 | within Arr api docs. 68 | """ 69 | 70 | 71 | #: Notification schema implementations 72 | PyarrNotificationSchema = Literal[ 73 | "Apprise", 74 | "CustomScript", 75 | "Discord", 76 | "Email", 77 | "MediaBrowser", 78 | "Gotify", 79 | "Join", 80 | "Xbmc", 81 | "MailGun", 82 | "Notifiarr", 83 | "Ntfy", 84 | "PlexServer", 85 | "Prowl", 86 | "PushBullet", 87 | "Pushcut", 88 | "Pushover", 89 | "SendGrid", 90 | "Signal", 91 | "Simplepush", 92 | "Slack", 93 | "SynologyIndexer", 94 | "Telegram", 95 | "Trakt", 96 | "Twitter", 97 | "Webhook", 98 | ] 99 | 100 | #: Download client schema implementations 101 | PyarrDownloadClientSchema = Literal[ 102 | "Aria2", 103 | "Deluge", 104 | "TorrentDownloadStation", 105 | "UsenetDownloadStation", 106 | "Flood", 107 | "TorrentFreeboxDownload", 108 | "Hadouken", 109 | "Nzbget", 110 | "NzbVortex", 111 | "Pneumatic", 112 | "QBittorrent", 113 | "RTorrent", 114 | "Sabnzbd", 115 | "TorrentBlackhole", 116 | "Transmission", 117 | "UsenetBlackhole", 118 | "UTorrent", 119 | "Vuze", 120 | ] 121 | 122 | #: Import List schema implementations 123 | PyarrImportListSchema = Literal[ 124 | "AniListImport", 125 | "CustomImport", 126 | "ImdbListImport", 127 | "MyAnimeListImport", 128 | "PlexImport", 129 | "PlexRssImport", 130 | "SimklUserImport", 131 | "SonarrImport", 132 | "TraktListImport", 133 | "TraktPopularImport", 134 | "TraktUserImport", 135 | ] 136 | 137 | #: Indexer schema implementations 138 | PyarrIndexerSchema = Literal[ 139 | "FileList", 140 | "HDBits", 141 | "IPTorrents", 142 | "Newznab", 143 | "Nyaa", 144 | "Omgwtfnzbs", 145 | "PassThePopcorn", 146 | "Rarbg", 147 | "TorrentRssIndexer", 148 | "TorrentPotato", 149 | "Torznab", 150 | ] 151 | -------------------------------------------------------------------------------- /tests/fixtures/sonarr/parse.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "string", 3 | "parsedEpisodeInfo": { 4 | "releaseTitle": "string", 5 | "seriesTitle": "string", 6 | "seriesTitleInfo": { 7 | "title": "string", 8 | "titleWithoutYear": "string", 9 | "year": 0 10 | }, 11 | "quality": { 12 | "quality": { 13 | "id": 0, 14 | "name": "string", 15 | "source": "string", 16 | "resolution": 0 17 | }, 18 | "revision": { 19 | "version": 0, 20 | "real": 0, 21 | "isRepack": false 22 | } 23 | }, 24 | "seasonNumber": 0, 25 | "episodeNumbers": [ 26 | 0 27 | ], 28 | "absoluteEpisodeNumbers": [ 29 | 0 30 | ], 31 | "specialAbsoluteEpisodeNumbers": [ 32 | 0 33 | ], 34 | "language": { 35 | "id": 0, 36 | "name": "string" 37 | }, 38 | "fullSeason": false, 39 | "isPartialSeason": false, 40 | "isMultiSeason": false, 41 | "isSeasonExtra": false, 42 | "special": false, 43 | "releaseHash": "string", 44 | "seasonPart": 0, 45 | "releaseTokens": "string", 46 | "isDaily": false, 47 | "isAbsoluteNumbering": false, 48 | "isPossibleSpecialEpisode": false, 49 | "isPossibleSceneSeasonSpecial": false 50 | }, 51 | "series": { 52 | "title": "string", 53 | "sortTitle": "string", 54 | "status": "string", 55 | "ended": true, 56 | "overview": "string", 57 | "network": "string", 58 | "airTime": "00:00", 59 | "images": [ 60 | { 61 | "coverType": "poster", 62 | "url": "string" 63 | } 64 | ], 65 | "seasons": [ 66 | { 67 | "seasonNumber": 0, 68 | "monitored": false 69 | } 70 | ], 71 | "year": 0, 72 | "path": "string", 73 | "qualityProfileId": 0, 74 | "languageProfileId": 0, 75 | "seasonFolder": true, 76 | "monitored": true, 77 | "useSceneNumbering": false, 78 | "runtime": 0, 79 | "tvdbId": 0, 80 | "tvRageId": 0, 81 | "tvMazeId": 0, 82 | "firstAired": "2011-09-26T00:00:00Z", 83 | "seriesType": "string", 84 | "cleanTitle": "string", 85 | "imdbId": "string", 86 | "titleSlug": "0", 87 | "certification": "string", 88 | "genres": [ 89 | "string" 90 | ], 91 | "tags": [ 92 | 0 93 | ], 94 | "added": "2020-05-19T05:33:31.868402Z", 95 | "ratings": { 96 | "votes": 0, 97 | "value": 0.0 98 | }, 99 | "id": 0 100 | }, 101 | "episodes": [ 102 | { 103 | "seriesId": 0, 104 | "episodeFileId": 0, 105 | "seasonNumber": 0, 106 | "episodeNumber": 0, 107 | "title": "string", 108 | "airDate": "2010-08-26", 109 | "airDateUtc": "2006-09-27T00:00:00Z", 110 | "overview": "string", 111 | "hasFile": true, 112 | "monitored": false, 113 | "absoluteEpisodeNumber": 0, 114 | "unverifiedSceneNumbering": false, 115 | "id": 0 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/lookup.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "foreignId": "guid", 4 | "artist": { 5 | "artistMetadataId": 0, 6 | "status": "string", 7 | "ended": true, 8 | "artistName": "string", 9 | "foreignArtistId": "guid", 10 | "tadbId": 0, 11 | "discogsId": 0, 12 | "overview": "string", 13 | "artistType": "Group", 14 | "disambiguation": "string", 15 | "links": [ 16 | { 17 | "url": "string", 18 | "name": "string" 19 | }, 20 | { 21 | "url": "string", 22 | "name": "string" 23 | }, 24 | { 25 | "url": "string", 26 | "name": "string" 27 | } 28 | ], 29 | "images": [], 30 | "qualityProfileId": 0, 31 | "metadataProfileId": 0, 32 | "monitored": false, 33 | "monitorNewItems": "all", 34 | "genres": [ 35 | "string" 36 | ], 37 | "tags": [], 38 | "added": "0001-01-01T00:01:00Z", 39 | "ratings": { 40 | "votes": 0, 41 | "value": 0 42 | }, 43 | "statistics": { 44 | "albumCount": 0, 45 | "trackFileCount": 0, 46 | "trackCount": 0, 47 | "totalTrackCount": 0, 48 | "sizeOnDisk": 0, 49 | "percentOfTracks": 0 50 | } 51 | }, 52 | "id": 1 53 | }, 54 | { 55 | "foreignId": "guid", 56 | "artist": { 57 | "artistMetadataId": 0, 58 | "status": "string", 59 | "ended": false, 60 | "artistName": "string", 61 | "foreignArtistId": "guid", 62 | "tadbId": 0, 63 | "discogsId": 0, 64 | "overview": "", 65 | "artistType": "Group", 66 | "disambiguation": "string", 67 | "links": [ 68 | { 69 | "url": "string", 70 | "name": "string" 71 | }, 72 | { 73 | "url": "string", 74 | "name": "string" 75 | }, 76 | { 77 | "url": "string", 78 | "name": "string" 79 | }, 80 | { 81 | "url": "string", 82 | "name": "string" 83 | }, 84 | { 85 | "url": "string", 86 | "name": "string" 87 | }, 88 | { 89 | "url": "string", 90 | "name": "string" 91 | } 92 | ], 93 | "images": [], 94 | "qualityProfileId": 0, 95 | "metadataProfileId": 0, 96 | "monitored": false, 97 | "monitorNewItems": "all", 98 | "genres": [], 99 | "tags": [], 100 | "added": "0001-01-01T00:01:00Z", 101 | "ratings": { 102 | "votes": 0, 103 | "value": 0 104 | }, 105 | "statistics": { 106 | "albumCount": 0, 107 | "trackFileCount": 0, 108 | "trackCount": 0, 109 | "totalTrackCount": 0, 110 | "sizeOnDisk": 0, 111 | "percentOfTracks": 0 112 | } 113 | }, 114 | "id": 2 115 | } 116 | ] 117 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pyarr" 3 | version = "5.2.0" 4 | description = "Synchronous Sonarr, Radarr, Lidarr and Readarr API's for Python" 5 | authors = ["Steven Marks "] 6 | license = "MIT" 7 | readme = "README.md" 8 | keywords = ["sonarr", "radarr", "readarr", "lidarr", "api", "wrapper", "plex"] 9 | homepage = "https://github.com/totaldebug/pyarr" 10 | repository = "https://github.com/totaldebug/pyarr" 11 | documentation = "https://docs.totaldebug.uk/pyarr" 12 | 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Natural Language :: English", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Topic :: Software Development :: Libraries :: Python Modules" 23 | ] 24 | packages = [ 25 | {include = "pyarr"}, 26 | {include = "pyarr/py.typed"} 27 | ] 28 | 29 | [tool.poetry.dependencies] 30 | python = "^3.10" 31 | requests = "^2.28.2" 32 | types-requests = "^2.28.11.17" 33 | overrides = "^7.3.1" 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | black = {version = "^24.3.0", allow-prereleases = true} 37 | flake8 = "^7.0.0" 38 | isort = "^5.12.0" 39 | mypy = "^1.10.0" 40 | pre-commit = "^3.7.1" 41 | interrogate = "^1.5.0" 42 | Sphinx = "^7.3.7" 43 | sphinx-argparse = "^0.4.0" 44 | sphinx-autobuild = "^2024.4.16" 45 | sphinx-rtd-theme = "^2.0.0" 46 | pytest = "^8.2.2" 47 | pytest-cov = "^5.0.0" 48 | pylint = "^3.2.2" 49 | responses = "^0.25.0" 50 | autoflake = "^2.3.1" 51 | ipykernel = "^6.22.0" 52 | sphinx-toolbox = "^3.4.0" 53 | enum-tools = "^0.12.0" 54 | pytest-rerunfailures = "^14.0" 55 | nox = "^2024.4.15" 56 | toml = "^0.10.2" 57 | commitizen = "^3.27.0" 58 | 59 | [tool.black] 60 | line-length = 88 61 | target_version = ['py310'] 62 | include = '\.pyi?$' 63 | exclude = ''' 64 | 65 | ( 66 | /( 67 | \.eggs # exclude a few common directories in the 68 | | \.git # root of the project 69 | | \.hg 70 | | \.mypy_cache 71 | | \.tox 72 | | \.nox 73 | | \.venv 74 | | \.cache 75 | | _build 76 | | buck-out 77 | | build 78 | | dist 79 | )/ 80 | ) 81 | ''' 82 | 83 | [tool.mypy] 84 | python_version = "3.10" 85 | warn_return_any = true 86 | warn_unused_configs = true 87 | 88 | [[tool.mypy.overrides]] 89 | module = "pyarr" 90 | disallow_untyped_defs = true 91 | 92 | [tool.isort] 93 | profile = "black" 94 | # will group `import x` and `from x import` of the same module. 95 | force_sort_within_sections = true 96 | known_first_party = [ 97 | "pyarr", 98 | "tests", 99 | ] 100 | forced_separate = [ 101 | "tests", 102 | ] 103 | skip = [".cache", ".nox"] 104 | combine_as_imports = true 105 | 106 | [tool.autoflake] 107 | check = true 108 | remove-unused-variables = true 109 | remove-all-unused-imports = true 110 | remove-duplicate-keys = true 111 | 112 | [tool.interrogate] 113 | ignore-init-method = true 114 | ignore-init-module = false 115 | ignore-magic = false 116 | ignore-semiprivate = false 117 | ignore-private = false 118 | ignore-property-decorators = false 119 | ignore-module = true 120 | ignore-nested-functions = false 121 | ignore-nested-classes = true 122 | ignore-setters = false 123 | fail-under = 100 124 | exclude = ["noxfile.py", "docs", "build", ".devcontainer", ".nox", ".cache", "tests"] 125 | ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] 126 | verbose = 0 127 | quiet = false 128 | color = true 129 | 130 | [build-system] 131 | requires = ["poetry-core"] 132 | build-backend = "poetry.core.masonry.api" 133 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/metadataprofile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Standard", 3 | "primaryAlbumTypes": [ 4 | { 5 | "albumType": { 6 | "id": 2, 7 | "name": "Single" 8 | }, 9 | "allowed": false 10 | }, 11 | { 12 | "albumType": { 13 | "id": 4, 14 | "name": "Other" 15 | }, 16 | "allowed": false 17 | }, 18 | { 19 | "albumType": { 20 | "id": 1, 21 | "name": "EP" 22 | }, 23 | "allowed": false 24 | }, 25 | { 26 | "albumType": { 27 | "id": 3, 28 | "name": "Broadcast" 29 | }, 30 | "allowed": false 31 | }, 32 | { 33 | "albumType": { 34 | "id": 0, 35 | "name": "Album" 36 | }, 37 | "allowed": true 38 | } 39 | ], 40 | "secondaryAlbumTypes": [ 41 | { 42 | "albumType": { 43 | "id": 0, 44 | "name": "Studio" 45 | }, 46 | "allowed": true 47 | }, 48 | { 49 | "albumType": { 50 | "id": 3, 51 | "name": "Spokenword" 52 | }, 53 | "allowed": false 54 | }, 55 | { 56 | "albumType": { 57 | "id": 2, 58 | "name": "Soundtrack" 59 | }, 60 | "allowed": false 61 | }, 62 | { 63 | "albumType": { 64 | "id": 7, 65 | "name": "Remix" 66 | }, 67 | "allowed": false 68 | }, 69 | { 70 | "albumType": { 71 | "id": 9, 72 | "name": "Mixtape/Street" 73 | }, 74 | "allowed": false 75 | }, 76 | { 77 | "albumType": { 78 | "id": 6, 79 | "name": "Live" 80 | }, 81 | "allowed": false 82 | }, 83 | { 84 | "albumType": { 85 | "id": 4, 86 | "name": "Interview" 87 | }, 88 | "allowed": false 89 | }, 90 | { 91 | "albumType": { 92 | "id": 8, 93 | "name": "DJ-mix" 94 | }, 95 | "allowed": false 96 | }, 97 | { 98 | "albumType": { 99 | "id": 10, 100 | "name": "Demo" 101 | }, 102 | "allowed": false 103 | }, 104 | { 105 | "albumType": { 106 | "id": 1, 107 | "name": "Compilation" 108 | }, 109 | "allowed": false 110 | } 111 | ], 112 | "releaseStatuses": [ 113 | { 114 | "releaseStatus": { 115 | "id": 3, 116 | "name": "Pseudo-Release" 117 | }, 118 | "allowed": false 119 | }, 120 | { 121 | "releaseStatus": { 122 | "id": 1, 123 | "name": "Promotion" 124 | }, 125 | "allowed": false 126 | }, 127 | { 128 | "releaseStatus": { 129 | "id": 0, 130 | "name": "Official" 131 | }, 132 | "allowed": true 133 | }, 134 | { 135 | "releaseStatus": { 136 | "id": 2, 137 | "name": "Bootleg" 138 | }, 139 | "allowed": false 140 | } 141 | ], 142 | "id": 1 143 | } 144 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/metadataprofile_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Standard", 4 | "primaryAlbumTypes": [ 5 | { 6 | "albumType": { 7 | "id": 2, 8 | "name": "Single" 9 | }, 10 | "allowed": false 11 | }, 12 | { 13 | "albumType": { 14 | "id": 4, 15 | "name": "Other" 16 | }, 17 | "allowed": false 18 | }, 19 | { 20 | "albumType": { 21 | "id": 1, 22 | "name": "EP" 23 | }, 24 | "allowed": false 25 | }, 26 | { 27 | "albumType": { 28 | "id": 3, 29 | "name": "Broadcast" 30 | }, 31 | "allowed": false 32 | }, 33 | { 34 | "albumType": { 35 | "id": 0, 36 | "name": "Album" 37 | }, 38 | "allowed": true 39 | } 40 | ], 41 | "secondaryAlbumTypes": [ 42 | { 43 | "albumType": { 44 | "id": 0, 45 | "name": "Studio" 46 | }, 47 | "allowed": true 48 | }, 49 | { 50 | "albumType": { 51 | "id": 3, 52 | "name": "Spokenword" 53 | }, 54 | "allowed": false 55 | }, 56 | { 57 | "albumType": { 58 | "id": 2, 59 | "name": "Soundtrack" 60 | }, 61 | "allowed": false 62 | }, 63 | { 64 | "albumType": { 65 | "id": 7, 66 | "name": "Remix" 67 | }, 68 | "allowed": false 69 | }, 70 | { 71 | "albumType": { 72 | "id": 9, 73 | "name": "Mixtape/Street" 74 | }, 75 | "allowed": false 76 | }, 77 | { 78 | "albumType": { 79 | "id": 6, 80 | "name": "Live" 81 | }, 82 | "allowed": false 83 | }, 84 | { 85 | "albumType": { 86 | "id": 4, 87 | "name": "Interview" 88 | }, 89 | "allowed": false 90 | }, 91 | { 92 | "albumType": { 93 | "id": 8, 94 | "name": "DJ-mix" 95 | }, 96 | "allowed": false 97 | }, 98 | { 99 | "albumType": { 100 | "id": 10, 101 | "name": "Demo" 102 | }, 103 | "allowed": false 104 | }, 105 | { 106 | "albumType": { 107 | "id": 1, 108 | "name": "Compilation" 109 | }, 110 | "allowed": false 111 | } 112 | ], 113 | "releaseStatuses": [ 114 | { 115 | "releaseStatus": { 116 | "id": 3, 117 | "name": "Pseudo-Release" 118 | }, 119 | "allowed": false 120 | }, 121 | { 122 | "releaseStatus": { 123 | "id": 1, 124 | "name": "Promotion" 125 | }, 126 | "allowed": false 127 | }, 128 | { 129 | "releaseStatus": { 130 | "id": 0, 131 | "name": "Official" 132 | }, 133 | "allowed": true 134 | }, 135 | { 136 | "releaseStatus": { 137 | "id": 2, 138 | "name": "Bootleg" 139 | }, 140 | "allowed": false 141 | } 142 | ], 143 | "id": 1 144 | } 145 | ] 146 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/wanted_missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "pageSize": 10, 4 | "sortKey": "Albums.Id", 5 | "sortDirection": "default", 6 | "totalRecords": 3, 7 | "records": [ 8 | { 9 | "title": "string", 10 | "disambiguation": "", 11 | "overview": "", 12 | "artistId": 7, 13 | "foreignAlbumId": "string", 14 | "monitored": true, 15 | "anyReleaseOk": true, 16 | "profileId": 1, 17 | "duration": 8926398, 18 | "albumType": "Album", 19 | "secondaryTypes": [], 20 | "mediumCount": 2, 21 | "ratings": { 22 | "votes": 0, 23 | "value": 0 24 | }, 25 | "releaseDate": "2008-01-01T00:00:00Z", 26 | "releases": [ 27 | { 28 | "id": 2439, 29 | "albumId": 100, 30 | "foreignReleaseId": "string", 31 | "title": "string", 32 | "status": "Official", 33 | "duration": 8926398, 34 | "trackCount": 20, 35 | "media": [ 36 | { 37 | "mediumNumber": 2, 38 | "mediumName": "string", 39 | "mediumFormat": "CD" 40 | } 41 | ], 42 | "mediumCount": 2, 43 | "disambiguation": "string", 44 | "country": [ 45 | "string" 46 | ], 47 | "label": [ 48 | "string" 49 | ], 50 | "format": "2xCD", 51 | "monitored": true 52 | } 53 | ], 54 | "genres": [], 55 | "media": [ 56 | { 57 | "mediumNumber": 2, 58 | "mediumName": "string", 59 | "mediumFormat": "string" 60 | } 61 | ], 62 | "artist": { 63 | "artistMetadataId": 1, 64 | "status": "continuing", 65 | "ended": false, 66 | "artistName": "string", 67 | "foreignArtistId": "string", 68 | "tadbId": 0, 69 | "discogsId": 0, 70 | "overview": "string", 71 | "artistType": "Group", 72 | "disambiguation": "", 73 | "links": [ 74 | { 75 | "url": "string", 76 | "name": "string" 77 | } 78 | ], 79 | "images": [ 80 | { 81 | "url": "string", 82 | "coverType": "poster", 83 | "extension": ".jpg" 84 | } 85 | ], 86 | "path": "/music/string", 87 | "qualityProfileId": 1, 88 | "metadataProfileId": 1, 89 | "monitored": true, 90 | "monitorNewItems": "all", 91 | "genres": [ 92 | "string" 93 | ], 94 | "cleanName": "string", 95 | "sortName": "string", 96 | "tags": [], 97 | "added": "2022-03-07T13:55:42Z", 98 | "ratings": { 99 | "votes": 66, 100 | "value": 8.3 101 | }, 102 | "statistics": { 103 | "albumCount": 0, 104 | "trackFileCount": 0, 105 | "trackCount": 0, 106 | "totalTrackCount": 0, 107 | "sizeOnDisk": 0, 108 | "percentOfTracks": 0 109 | }, 110 | "id": 7 111 | }, 112 | "images": [], 113 | "links": [], 114 | "statistics": { 115 | "trackFileCount": 0, 116 | "trackCount": 20, 117 | "totalTrackCount": 20, 118 | "sizeOnDisk": 0, 119 | "percentOfTracks": 0 120 | }, 121 | "grabbed": false, 122 | "id": 100 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/qualityprofile.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "eBook", 4 | "upgradeAllowed": false, 5 | "cutoff": 2, 6 | "items": [ 7 | { 8 | "quality": { 9 | "id": 0, 10 | "name": "Unknown Text" 11 | }, 12 | "items": [], 13 | "allowed": false 14 | }, 15 | { 16 | "quality": { 17 | "id": 1, 18 | "name": "PDF" 19 | }, 20 | "items": [], 21 | "allowed": false 22 | }, 23 | { 24 | "quality": { 25 | "id": 2, 26 | "name": "MOBI" 27 | }, 28 | "items": [], 29 | "allowed": true 30 | }, 31 | { 32 | "quality": { 33 | "id": 3, 34 | "name": "EPUB" 35 | }, 36 | "items": [], 37 | "allowed": true 38 | }, 39 | { 40 | "quality": { 41 | "id": 4, 42 | "name": "AZW3" 43 | }, 44 | "items": [], 45 | "allowed": true 46 | }, 47 | { 48 | "quality": { 49 | "id": 13, 50 | "name": "Unknown Audio" 51 | }, 52 | "items": [], 53 | "allowed": false 54 | }, 55 | { 56 | "quality": { 57 | "id": 10, 58 | "name": "MP3" 59 | }, 60 | "items": [], 61 | "allowed": false 62 | }, 63 | { 64 | "quality": { 65 | "id": 12, 66 | "name": "M4B" 67 | }, 68 | "items": [], 69 | "allowed": false 70 | }, 71 | { 72 | "quality": { 73 | "id": 11, 74 | "name": "FLAC" 75 | }, 76 | "items": [], 77 | "allowed": false 78 | } 79 | ], 80 | "id": 1 81 | }, 82 | { 83 | "name": "Spoken", 84 | "upgradeAllowed": false, 85 | "cutoff": 10, 86 | "items": [ 87 | { 88 | "quality": { 89 | "id": 0, 90 | "name": "Unknown Text" 91 | }, 92 | "items": [], 93 | "allowed": false 94 | }, 95 | { 96 | "quality": { 97 | "id": 1, 98 | "name": "PDF" 99 | }, 100 | "items": [], 101 | "allowed": false 102 | }, 103 | { 104 | "quality": { 105 | "id": 2, 106 | "name": "MOBI" 107 | }, 108 | "items": [], 109 | "allowed": false 110 | }, 111 | { 112 | "quality": { 113 | "id": 3, 114 | "name": "EPUB" 115 | }, 116 | "items": [], 117 | "allowed": false 118 | }, 119 | { 120 | "quality": { 121 | "id": 4, 122 | "name": "AZW3" 123 | }, 124 | "items": [], 125 | "allowed": false 126 | }, 127 | { 128 | "quality": { 129 | "id": 13, 130 | "name": "Unknown Audio" 131 | }, 132 | "items": [], 133 | "allowed": true 134 | }, 135 | { 136 | "quality": { 137 | "id": 10, 138 | "name": "MP3" 139 | }, 140 | "items": [], 141 | "allowed": true 142 | }, 143 | { 144 | "quality": { 145 | "id": 12, 146 | "name": "M4B" 147 | }, 148 | "items": [], 149 | "allowed": true 150 | }, 151 | { 152 | "quality": { 153 | "id": 11, 154 | "name": "FLAC" 155 | }, 156 | "items": [], 157 | "allowed": true 158 | } 159 | ], 160 | "id": 2 161 | } 162 | ] 163 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | import nox 6 | from nox.sessions import Session 7 | 8 | 9 | @nox.session(reuse_venv=True) 10 | def format(session: Session) -> None: 11 | """Run automatic code formatters""" 12 | session.run("poetry", "install", external=True) 13 | session.run("black", ".") 14 | session.run("isort", ".") 15 | session.run("autoflake", "--in-place", ".") 16 | 17 | 18 | @nox.session(reuse_venv=True) 19 | def tests(session: Session) -> None: 20 | """Run the complete test suite""" 21 | if os.environ.get("GITHUB_ACTIONS") == "true": 22 | session.notify("test_types") 23 | session.notify("test_style") 24 | session.notify("test_suite") 25 | else: 26 | session.notify("docker_test") 27 | 28 | 29 | @nox.session(reuse_venv=True) 30 | def docker_test(session: Session) -> None: 31 | """Run the complete test suite""" 32 | session.notify("test_create_containers") 33 | session.notify("test_types") 34 | session.notify("test_style") 35 | session.notify("test_suite") 36 | session.notify("test_cleanup_containers") 37 | 38 | 39 | @nox.session(reuse_venv=True) 40 | def test_create_containers(session: Session) -> None: 41 | session.run( 42 | "sudo", 43 | "docker", 44 | "compose", 45 | "-f", 46 | ".devcontainer/docker-compose.yml", 47 | "pull", 48 | external=True, 49 | ) 50 | session.run( 51 | "sudo", 52 | "docker", 53 | "compose", 54 | "-f", 55 | ".devcontainer/docker-compose.yml", 56 | "up", 57 | "-d", 58 | external=True, 59 | ) 60 | 61 | 62 | @nox.session(reuse_venv=True) 63 | def test_cleanup_containers(session: Session) -> None: 64 | session.run( 65 | "sudo", 66 | "docker", 67 | "compose", 68 | "-f", 69 | ".devcontainer/docker-compose.yml", 70 | "down", 71 | external=True, 72 | ) 73 | session.run("git", "checkout", "--", "tests/docker_configs/", external=True) 74 | 75 | 76 | @nox.session(reuse_venv=True) 77 | def test_suite(session: Session) -> None: 78 | """Run the Python-based test suite""" 79 | session.run("poetry", "install", external=True) 80 | session.run( 81 | "pytest", 82 | "--showlocals", 83 | "--reruns", 84 | "3", 85 | "--reruns-delay", 86 | "5", 87 | "--cov=pyarr", 88 | "--cov-report", 89 | "xml", 90 | "--cov-report", 91 | "term-missing", 92 | "-vv", 93 | ) 94 | 95 | 96 | @nox.session(reuse_venv=True) 97 | def test_types(session: Session) -> None: 98 | """Check that typing is working as expected""" 99 | session.run("poetry", "install", external=True) 100 | session.run("mypy", "--show-error-codes", "pyarr") 101 | 102 | 103 | @nox.session(reuse_venv=True) 104 | def test_style(session: Session) -> None: 105 | """Check that style guidelines are being followed""" 106 | session.run("poetry", "install", external=True) 107 | session.run("flake8", "pyarr", "tests") 108 | session.run( 109 | "black", 110 | "pyarr", 111 | "--check", 112 | ) 113 | session.run("isort", "pyarr", "--check-only") 114 | session.run("autoflake", "-r", "pyarr") 115 | session.run("interrogate", "pyarr") 116 | 117 | 118 | @nox.session(reuse_venv=True) 119 | def serve_docs(session: Session) -> None: 120 | """Create local copy of docs for testing""" 121 | session.run("poetry", "install", external=True) 122 | session.run("sphinx-autobuild", "docs", "build") 123 | 124 | 125 | @nox.session(reuse_venv=True) 126 | def build_docs(session: Session) -> None: 127 | """Create local copy of docs for testing""" 128 | session.run("poetry", "install", external=True) 129 | session.run("sphinx-build", "-b", "html", "docs", "build") 130 | 131 | 132 | @nox.session(reuse_venv=True) 133 | def install_release(session: Session) -> None: 134 | session.run("npm", "install", "@semantic-release/changelog") 135 | session.run("npm", "install", "@semantic-release/exec") 136 | session.run("npm", "install", "@semantic-release/git") 137 | session.run("npm", "install", "@semantic-release/github") 138 | session.run("npm", "install", "conventional-changelog-conventionalcommits@7.0.2") 139 | session.run("npm", "install", "semantic-release-pypi") 140 | 141 | 142 | @nox.session(reuse_venv=True) 143 | def release(session: Session) -> None: 144 | """Release a new version of the package""" 145 | pypi_password = session.posargs[0] 146 | session.run("poetry", "install", external=True) 147 | session.notify("install_release") 148 | session.run("npx", "semantic-release", "--debug") 149 | session.run("poetry", "build", external=True) 150 | session.run( 151 | "poetry", "publish", "-u", "__token__", "-p", pypi_password, external=True 152 | ) 153 | -------------------------------------------------------------------------------- /tests/fixtures/readarr/lookup.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "foreignId": "1234567", 4 | "author": { 5 | "authorMetadataId": 0, 6 | "status": "continuing", 7 | "ended": false, 8 | "authorName": "string", 9 | "authorNameLastFirst": "string", 10 | "foreignAuthorId": "1234567", 11 | "titleSlug": "1234567", 12 | "overview": "string", 13 | "links": [ 14 | { 15 | "url": "string", 16 | "name": "Goodreads" 17 | } 18 | ], 19 | "images": [ 20 | { 21 | "url": "string", 22 | "coverType": "poster", 23 | "extension": ".jpg" 24 | } 25 | ], 26 | "remotePoster": "string", 27 | "qualityProfileId": 0, 28 | "metadataProfileId": 0, 29 | "monitored": false, 30 | "monitorNewItems": "all", 31 | "genres": [], 32 | "cleanName": "string", 33 | "sortName": "string", 34 | "sortNameLastFirst": "string", 35 | "tags": [], 36 | "added": "0001-01-01T00:01:00Z", 37 | "ratings": { 38 | "votes": 31221602, 39 | "value": 4.46, 40 | "popularity": 139248344.92 41 | }, 42 | "statistics": { 43 | "bookFileCount": 0, 44 | "bookCount": 0, 45 | "availableBookCount": 0, 46 | "totalBookCount": 0, 47 | "sizeOnDisk": 0, 48 | "percentOfBooks": 0 49 | } 50 | }, 51 | "id": 1 52 | }, 53 | { 54 | "foreignId": "1234567", 55 | "book": { 56 | "title": "string", 57 | "authorTitle": "string", 58 | "seriesTitle": "string", 59 | "disambiguation": "US Edition", 60 | "overview": "string", 61 | "authorId": 0, 62 | "foreignBookId": "1234567", 63 | "titleSlug": "1234567", 64 | "monitored": false, 65 | "anyEditionOk": true, 66 | "ratings": { 67 | "votes": 2745211, 68 | "value": 4.5, 69 | "popularity": 12353449.5 70 | }, 71 | "releaseDate": "2003-06-21T00:00:00Z", 72 | "pageCount": 870, 73 | "genres": [ 74 | "string" 75 | ], 76 | "author": { 77 | "authorMetadataId": 0, 78 | "status": "continuing", 79 | "ended": false, 80 | "authorName": "string", 81 | "authorNameLastFirst": "v", 82 | "foreignAuthorId": "1234567", 83 | "titleSlug": "1234567", 84 | "overview": "string", 85 | "links": [ 86 | { 87 | "url": "string", 88 | "name": "Goodreads" 89 | } 90 | ], 91 | "images": [ 92 | { 93 | "url": "string", 94 | "coverType": "poster", 95 | "extension": ".jpg" 96 | } 97 | ], 98 | "qualityProfileId": 0, 99 | "metadataProfileId": 0, 100 | "monitored": false, 101 | "monitorNewItems": "all", 102 | "genres": [], 103 | "cleanName": "string", 104 | "sortName": "string", 105 | "sortNameLastFirst": "string", 106 | "tags": [], 107 | "added": "0001-01-01T00:01:00Z", 108 | "ratings": { 109 | "votes": 31221602, 110 | "value": 4.46, 111 | "popularity": 139248344.92 112 | }, 113 | "statistics": { 114 | "bookFileCount": 0, 115 | "bookCount": 0, 116 | "availableBookCount": 0, 117 | "totalBookCount": 0, 118 | "sizeOnDisk": 0, 119 | "percentOfBooks": 0 120 | } 121 | }, 122 | "images": [ 123 | { 124 | "url": "string", 125 | "coverType": "cover", 126 | "extension": ".jpg" 127 | } 128 | ], 129 | "links": [ 130 | { 131 | "url": "string", 132 | "name": "Goodreads Editions" 133 | }, 134 | { 135 | "url": "string", 136 | "name": "Goodreads Book" 137 | } 138 | ], 139 | "added": "0001-01-01T00:01:00Z", 140 | "remoteCover": "string", 141 | "editions": [], 142 | "grabbed": false 143 | }, 144 | "id": 2 145 | } 146 | ] 147 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Contributing 3 | ############ 4 | 5 | It is recommended that contributions to this project are created within vscode, 6 | utilising the devcontainer functionality. 7 | 8 | This ensures that all developers are using the same environment and extentions, 9 | reducing the risk of additional bugs / formatting issues within the project. 10 | 11 | .. note:: 12 | The setup of VSCode devcontainer is outside of the scope of this document 13 | additional information can be found within the VSCode documentation. 14 | 15 | ********************** 16 | Setup your environment 17 | ********************** 18 | 19 | #. Fork the `repository `_ 20 | #. Open the repository in VSCode 21 | #. Copy the ``.devcontainer/recommended-devcontainer.json`` files and rename the copy to ``devcontainer.json``. 22 | #. Modify the ``mounts`` section as required for your environment 23 | #. Copy the ``.devcontainer/recommended-docker-compose.yml`` files and rename the copy to ``docker.compose.yml`` 24 | #. You may need to modify the ``volumes`` for your environment 25 | #. Press ``ctrl + shift + p`` and select ``Remote-Container: Reopen in Container`` 26 | #. Once loaded you can begin modification of the module or Documentation 27 | 28 | ********************* 29 | Updating PyArr module 30 | ********************* 31 | 32 | Style & formatting 33 | ================== 34 | 35 | It is highly recommended to use vsCode devcontainer as this automatically adds the 36 | required formatting checks on pre-commit, this will allow for resolution of issues 37 | prior to the pull request being submitted. 38 | 39 | If you are not using devcontainer please register the pre-commit-config: 40 | 41 | .. code:: bash 42 | 43 | poetry run pre-commit install 44 | 45 | A few guidelines for approval: 46 | 47 | - Must follow PEP8 / Black formatting. (devcontainer is setup to reformat on save) 48 | - We recommend using `sourcery `_ to ensure code is most 49 | efficient, this will be checked when the pull request is opened. 50 | - All functions must use google docstring format, the devcontainer has an 51 | `autodocstring `_ 52 | plugin which will auto fill. 53 | - ``pyproject.toml`` must be updated with a new version, the new versions should 54 | follow `semver `_. 55 | - Each feature / bugfix etc. should have its own pull request. 56 | 57 | Testing 58 | ======= 59 | 60 | Tests can be run with the following command: ``nox -s tests``. This command will check 61 | code style and typing compliance and will then execute all required``tests`` 62 | 63 | If you are adding a new method to the library, a test must be added as well. This test should be 64 | against the live API, if a mock is required then reason for this should be added to the PR notes. 65 | 66 | ********************** 67 | Updating Documentation 68 | ********************** 69 | 70 | The documentation for this project utilises `sphinx `_. 71 | Sphinx allows for automatic documenting of all classes / functions via DocString. 72 | 73 | To Update static pages, you can amend the ``.rst`` files in the ``sphinx-docs`` folder. 74 | 75 | To test the documentation locally use command ``nox -s docs`` this will create all HTML files 76 | in the ``build`` directory. 77 | 78 | All Python Class / Function documentation is updated automatically by Github Actions and 79 | does not require any manual changes to be made. 80 | 81 | Sphinx documentation uses `reStructuredText `_ to format each of the pages. 82 | 83 | *********************** 84 | Pull Requests & Release 85 | *********************** 86 | 87 | Now that you have made the changes required for your enhancement, a pull request 88 | is required for the core team to review the changes, request amendments or approve 89 | the work that you have completed. 90 | 91 | Pull Requests 92 | ============= 93 | 94 | - Each feature / bugfix should have its own PR. This makes code review more efficient 95 | and allows for a clean changelog generation 96 | - All CI tests must be passing 97 | - If a Pull Request contains multiple changes, our core team may reject it 98 | - All information in the Pull Request template should be completed, when people look 99 | at what was done with this Pull Request it should be easy to tell from this template 100 | - It must state if the change is a Breaking Change, and what would break by implementing 101 | 102 | Release Changes 103 | ================= 104 | 105 | To release a new version of the module or documentation updates, the core team 106 | will take the following steps: 107 | 108 | #. Reviewing and testing the PR that has been submitted to ensure all 109 | requirements have been met. 110 | #. Tag the release in git: ``git tag $NEW_VERSION``. 111 | #. Push the tag to GitHub: ``git push --tags origin``. 112 | #. If all tests complete the package will be automatically released to PyPI 113 | #. Github Action will re-create the documentation with Sphinx 114 | 115 | If the only change is to documentation, the workflow ``Sphinx Documentation Update`` 116 | will be run to update the documentation. 117 | 118 | Documentation updates don't require the version to be updated in ``pyproject.toml`` 119 | and also don't require tagging. 120 | -------------------------------------------------------------------------------- /tests/fixtures/radarr/movie_import.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "string", 4 | "originalTitle": "string", 5 | "originalLanguage": { 6 | "id": 1, 7 | "name": "English" 8 | }, 9 | "alternateTitles": [ 10 | { 11 | "sourceType": "tmdb", 12 | "movieId": 0, 13 | "title": "string", 14 | "sourceId": 0, 15 | "votes": 0, 16 | "voteCount": 0, 17 | "language": { 18 | "id": 1, 19 | "name": "English" 20 | } 21 | }, 22 | { 23 | "sourceType": "tmdb", 24 | "movieId": 0, 25 | "title": "string", 26 | "sourceId": 0, 27 | "votes": 0, 28 | "voteCount": 0, 29 | "language": { 30 | "id": 11, 31 | "name": "Russian" 32 | } 33 | }, 34 | { 35 | "sourceType": "tmdb", 36 | "movieId": 0, 37 | "title": "string", 38 | "sourceId": 0, 39 | "votes": 0, 40 | "voteCount": 0, 41 | "language": { 42 | "id": 1, 43 | "name": "English" 44 | } 45 | }, 46 | { 47 | "sourceType": "tmdb", 48 | "movieId": 0, 49 | "title": "string", 50 | "sourceId": 0, 51 | "votes": 0, 52 | "voteCount": 0, 53 | "language": { 54 | "id": 1, 55 | "name": "English" 56 | } 57 | }, 58 | { 59 | "sourceType": "tmdb", 60 | "movieId": 0, 61 | "title": "string", 62 | "sourceId": 0, 63 | "votes": 0, 64 | "voteCount": 0, 65 | "language": { 66 | "id": 16, 67 | "name": "Finnish" 68 | } 69 | }, 70 | { 71 | "sourceType": "tmdb", 72 | "movieId": 0, 73 | "title": "string", 74 | "sourceId": 0, 75 | "votes": 0, 76 | "voteCount": 0, 77 | "language": { 78 | "id": 2, 79 | "name": "French" 80 | } 81 | }, 82 | { 83 | "sourceType": "tmdb", 84 | "movieId": 0, 85 | "title": "string", 86 | "sourceId": 0, 87 | "votes": 0, 88 | "voteCount": 0, 89 | "language": { 90 | "id": 18, 91 | "name": "Portuguese" 92 | } 93 | }, 94 | { 95 | "sourceType": "tmdb", 96 | "movieId": 0, 97 | "title": "string", 98 | "sourceId": 0, 99 | "votes": 0, 100 | "voteCount": 0, 101 | "language": { 102 | "id": 1, 103 | "name": "English" 104 | } 105 | }, 106 | { 107 | "sourceType": "tmdb", 108 | "movieId": 0, 109 | "title": "string", 110 | "sourceId": 0, 111 | "votes": 0, 112 | "voteCount": 0, 113 | "language": { 114 | "id": 1, 115 | "name": "English" 116 | } 117 | } 118 | ], 119 | "secondaryYearSourceId": 0, 120 | "sortTitle": "string", 121 | "sizeOnDisk": 0, 122 | "status": "released", 123 | "overview": "string", 124 | "inCinemas": "2002-03-10T00:00:00Z", 125 | "physicalRelease": "2002-09-26T00:00:00Z", 126 | "digitalRelease": "2005-04-22T00:00:00Z", 127 | "images": [ 128 | { 129 | "coverType": "poster", 130 | "url": "string" 131 | }, 132 | { 133 | "coverType": "fanart", 134 | "url": "string" 135 | } 136 | ], 137 | "website": "string", 138 | "year": 2002, 139 | "hasFile": false, 140 | "youTubeTrailerId": "string", 141 | "studio": "string", 142 | "path": "/movies/string", 143 | "qualityProfileId": 1, 144 | "monitored": true, 145 | "minimumAvailability": "announced", 146 | "isAvailable": true, 147 | "folderName": "/movies/string", 148 | "runtime": 81, 149 | "cleanTitle": "string", 150 | "imdbId": "string", 151 | "tmdbId": 425, 152 | "titleSlug": "425", 153 | "certification": "U", 154 | "genres": [ 155 | "Animation", 156 | "Comedy", 157 | "Family" 158 | ], 159 | "added": "2022-08-03T08:21:06Z", 160 | "addOptions": { 161 | "searchForMovie": false, 162 | "ignoreEpisodesWithFiles": false, 163 | "ignoreEpisodesWithoutFiles": false 164 | }, 165 | "ratings": { 166 | "imdb": { 167 | "votes": 476521, 168 | "value": 7.5, 169 | "type": "user" 170 | }, 171 | "tmdb": { 172 | "votes": 11238, 173 | "value": 7.334, 174 | "type": "user" 175 | }, 176 | "metacritic": { 177 | "votes": 0, 178 | "value": 61, 179 | "type": "user" 180 | }, 181 | "rottenTomatoes": { 182 | "votes": 0, 183 | "value": 77, 184 | "type": "user" 185 | } 186 | }, 187 | "collection": { 188 | "name": "string", 189 | "tmdbId": 8354, 190 | "images": [] 191 | }, 192 | "id": 313 193 | } 194 | ] 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Release][release-shield]][release-url] 4 | [![Stargazers][stars-shield]][stars-url] 5 | ![codecov][codecov-shield] 6 | 7 | ![GitHub last release date][gh-last-release-date] 8 | ![GitHub last commit][gh-last-commit] 9 | 10 | [![Contributors][contributors-shield]][contributors-url] 11 | [![Forks][forks-shield]][forks-url] 12 | [![Issues][issues-shield]][issues-url] 13 | 14 | [![Lines of code][lines]][lines-url] 15 | ![Code size][code-size] 16 | 17 | [![MIT License][license-shield]][license-url] 18 | [![LinkedIn][linkedin-shield]][linkedin-url] 19 | 20 | 21 |
22 |
23 | 24 | Logo 25 | 26 | 27 |

Pyarr

28 | 29 |

30 | A Python library for interacting with the `arr` API's 31 |

32 |
33 |
34 | Report Bug 35 | · 36 | Request Feature 37 | 38 |
39 | 40 | 41 |
42 | Table of Contents 43 |
    44 |
  1. 45 | About The Project 46 | 49 |
  2. 50 |
  3. Getting Started
  4. 51 |
  5. Features
  6. 52 |
  7. Compatibility
  8. 53 |
  9. Roadmap
  10. 54 |
  11. Sponsor
  12. 55 |
  13. Contributing
  14. 56 |
  15. License
  16. 57 |
  17. Contact
  18. 58 |
  19. Acknowledgments
  20. 59 |
60 |
61 | 62 | 63 | ## About The Project 64 | 65 | A Python library for the following `arr` API's: 66 | 67 | * Sonarr 68 | * Radarr 69 | * Readarr 70 | * Lidarr 71 | 72 | The library returns results in JSON format for ease of use, this also reduces the risk of failue when the arr APIs are updated. 73 | 74 |

(back to top)

75 | 76 | ### Built With 77 | 78 | [![python][python]][python-url] 79 | 80 |

(back to top)

81 | 82 | 83 | ## Getting Started 84 | 85 | * [QuickStart Guide](https://docs.totaldebug.uk/pyarr/quickstart.html) 86 | * [Full Documentation](https://docs.totaldebug.uk/pyarr) 87 | * [Release Notes](https://github.com/totaldebug/pyarr/releases) 88 | 89 |

(back to top)

90 | 91 | ## Features 92 | 93 | * Support for multiple Arr APIs 94 | * Sonarr 95 | * Radarr 96 | * Readarr 97 | * Lidarr 98 | * Type checking 99 | 100 | ## Compatibility 101 | 102 | The below versions are based on our last tests, This will be updated as tests fail and updates are published. 103 | 104 | | Version | Sonarr | Radarr | Readarr | Lidarr | 105 | | ------- | ------ | ------ | ------- | ------ | 106 | | v5.0.0 | from: v3.0.10.1567 | from: v4.3.2.6857 | from: v0.1.4.1596 | from: v1.0.2.2592 | 107 | 108 |

(back to top)

109 | 110 | 111 | ## Roadmap 112 | 113 | See the [feature requests](https://github.com/totaldebug/pyarr/labels/type%2Ffeature) for a full list of requested features. 114 | 115 |

(back to top)

116 | 117 | ## Sponsor 118 | 119 | My projects arent possible without the support of the community, please consider donating a small amount to keep these projects alive. 120 | 121 | [![Sponsor][Sponsor]][Sponsor-url] 122 | 123 | 124 | ## Contributing 125 | 126 | Got something you would like to add? check out the contributing guide in the [documentation](https://docs.totaldebug.uk/pyarr/contributing.html) 127 | 128 |

(back to top)

129 | 130 | 131 | ## License 132 | 133 | [![CC BY-NC-SA 4.0][license-shield]][license-url] 134 | 135 | * Copyright © [Total Debug](https://totaldebug.uk). 136 | 137 |

(back to top)

138 | 139 | 140 | ## Contact 141 | 142 | * [Discord](https://discord.gg/6fmekudc8Q) 143 | * [Discussions](https://github.com/totaldebug/pyarr/discussions) 144 | * [Project Link](https://github.com/totaldebug/pyarr) 145 | 146 |

(back to top)

147 | 148 | 149 | ## Acknowledgments 150 | 151 | Below are a list of resources that I used to assist with this project. 152 | 153 | * None at this time 154 | 155 |

(back to top)

156 | 157 | 158 | 159 | [release-shield]: https://img.shields.io/github/v/release/totaldebug/pyarr?color=ff7034&label=Release&sort=semver&style=flat-square 160 | [release-url]: https://github.com/totaldebug/pyarr/releases 161 | [contributors-shield]: https://img.shields.io/github/contributors/totaldebug/pyarr.svg?style=flat-square 162 | [contributors-url]: https://github.com/totaldebug/pyarr/graphs/contributors 163 | [forks-shield]: https://img.shields.io/github/forks/totaldebug/pyarr.svg?style=flat-square 164 | [forks-url]: https://github.com/totaldebug/pyarr/network/members 165 | [stars-shield]: https://img.shields.io/github/stars/totaldebug/pyarr.svg?style=flat-square 166 | [stars-url]: https://github.com/totaldebug/pyarr/stargazers 167 | [issues-shield]: https://img.shields.io/github/issues/totaldebug/pyarr.svg?style=flat-square 168 | [issues-url]: https://github.com/totaldebug/pyarr/issues 169 | [license-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-orange.svg?style=flat-square 170 | [license-url]: https://creativecommons.org/licenses/by-nc-sa/4.0/ 171 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555 172 | [linkedin-url]: https://linkedin.com/in/marksie1988 173 | [codecov-shield]: https://img.shields.io/codecov/c/github/totaldebug/pyarr?style=flat-square 174 | 175 | [gh-last-release-date]: https://img.shields.io/github/release-date/totaldebug/pyarr?style=flat-square&label=Last%20Release%20Date&logo=github&logoColor=white 176 | [gh-last-commit]: https://img.shields.io/github/last-commit/totaldebug/pyarr.svg?style=flat-square&logo=github&label=Last%20Commit&logoColor=white 177 | 178 | [lines]: https://img.shields.io/tokei/lines/github/totaldebug/pyarr?style=flat-square 179 | [lines-url]: https://github.com/totaldebug/pyarr 180 | [code-size]: https://img.shields.io/github/languages/code-size/totaldebug/pyarr?style=flat-square 181 | 182 | [Sponsor]: https://img.shields.io/badge/sponsor-000?style=flat-square&logo=githubsponsors&logoColor=red 183 | [Sponsor-url]: https://github.com/sponsors/marksie1988 184 | 185 | [python]: https://img.shields.io/badge/Python-blue?style=flat-square&logo=Python&logoColor=white 186 | [python-url]: https://www.python.org/ 187 | -------------------------------------------------------------------------------- /tests/fixtures/lidarr/album_all.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "string", 4 | "disambiguation": "", 5 | "overview": "", 6 | "artistId": 7, 7 | "foreignAlbumId": "string", 8 | "monitored": true, 9 | "anyReleaseOk": true, 10 | "profileId": 1, 11 | "duration": 8926398, 12 | "albumType": "Album", 13 | "secondaryTypes": [], 14 | "mediumCount": 2, 15 | "ratings": { 16 | "votes": 0, 17 | "value": 0 18 | }, 19 | "releaseDate": "2008-01-01T00:00:00Z", 20 | "releases": [ 21 | { 22 | "id": 2439, 23 | "albumId": 100, 24 | "foreignReleaseId": "string", 25 | "title": "string", 26 | "status": "Official", 27 | "duration": 8926398, 28 | "trackCount": 20, 29 | "media": [ 30 | { 31 | "mediumNumber": 2, 32 | "mediumName": "string", 33 | "mediumFormat": "CD" 34 | } 35 | ], 36 | "mediumCount": 2, 37 | "disambiguation": "string", 38 | "country": [ 39 | "string" 40 | ], 41 | "label": [ 42 | "string" 43 | ], 44 | "format": "2xCD", 45 | "monitored": true 46 | } 47 | ], 48 | "genres": [], 49 | "media": [ 50 | { 51 | "mediumNumber": 2, 52 | "mediumName": "string", 53 | "mediumFormat": "string" 54 | } 55 | ], 56 | "artist": { 57 | "artistMetadataId": 1, 58 | "status": "continuing", 59 | "ended": false, 60 | "artistName": "string", 61 | "foreignArtistId": "string", 62 | "tadbId": 0, 63 | "discogsId": 0, 64 | "overview": "string", 65 | "artistType": "Group", 66 | "disambiguation": "", 67 | "links": [ 68 | { 69 | "url": "string", 70 | "name": "string" 71 | } 72 | ], 73 | "images": [ 74 | { 75 | "url": "string", 76 | "coverType": "poster", 77 | "extension": ".jpg" 78 | } 79 | ], 80 | "path": "/music/string", 81 | "qualityProfileId": 1, 82 | "metadataProfileId": 1, 83 | "monitored": true, 84 | "monitorNewItems": "all", 85 | "genres": [ 86 | "string" 87 | ], 88 | "cleanName": "string", 89 | "sortName": "string", 90 | "tags": [], 91 | "added": "2022-03-07T13:55:42Z", 92 | "ratings": { 93 | "votes": 66, 94 | "value": 8.3 95 | }, 96 | "statistics": { 97 | "albumCount": 0, 98 | "trackFileCount": 0, 99 | "trackCount": 0, 100 | "totalTrackCount": 0, 101 | "sizeOnDisk": 0, 102 | "percentOfTracks": 0 103 | }, 104 | "id": 7 105 | }, 106 | "images": [], 107 | "links": [], 108 | "statistics": { 109 | "trackFileCount": 0, 110 | "trackCount": 20, 111 | "totalTrackCount": 20, 112 | "sizeOnDisk": 0, 113 | "percentOfTracks": 0 114 | }, 115 | "grabbed": false, 116 | "id": 100 117 | }, 118 | { 119 | "title": "string", 120 | "disambiguation": "", 121 | "overview": "", 122 | "artistId": 7, 123 | "foreignAlbumId": "string", 124 | "monitored": true, 125 | "anyReleaseOk": true, 126 | "profileId": 1, 127 | "duration": 8926398, 128 | "albumType": "Album", 129 | "secondaryTypes": [], 130 | "mediumCount": 2, 131 | "ratings": { 132 | "votes": 0, 133 | "value": 0 134 | }, 135 | "releaseDate": "2008-01-01T00:00:00Z", 136 | "releases": [ 137 | { 138 | "id": 2439, 139 | "albumId": 100, 140 | "foreignReleaseId": "string", 141 | "title": "string", 142 | "status": "Official", 143 | "duration": 8926398, 144 | "trackCount": 20, 145 | "media": [ 146 | { 147 | "mediumNumber": 2, 148 | "mediumName": "string", 149 | "mediumFormat": "CD" 150 | } 151 | ], 152 | "mediumCount": 2, 153 | "disambiguation": "string", 154 | "country": [ 155 | "string" 156 | ], 157 | "label": [ 158 | "string" 159 | ], 160 | "format": "2xCD", 161 | "monitored": true 162 | } 163 | ], 164 | "genres": [], 165 | "media": [ 166 | { 167 | "mediumNumber": 2, 168 | "mediumName": "string", 169 | "mediumFormat": "string" 170 | } 171 | ], 172 | "artist": { 173 | "artistMetadataId": 1, 174 | "status": "continuing", 175 | "ended": false, 176 | "artistName": "string", 177 | "foreignArtistId": "string", 178 | "tadbId": 0, 179 | "discogsId": 0, 180 | "overview": "string", 181 | "artistType": "Group", 182 | "disambiguation": "", 183 | "links": [ 184 | { 185 | "url": "string", 186 | "name": "string" 187 | } 188 | ], 189 | "images": [ 190 | { 191 | "url": "string", 192 | "coverType": "poster", 193 | "extension": ".jpg" 194 | } 195 | ], 196 | "path": "/music/string", 197 | "qualityProfileId": 1, 198 | "metadataProfileId": 1, 199 | "monitored": true, 200 | "monitorNewItems": "all", 201 | "genres": [ 202 | "string" 203 | ], 204 | "cleanName": "string", 205 | "sortName": "string", 206 | "tags": [], 207 | "added": "2022-03-07T13:55:42Z", 208 | "ratings": { 209 | "votes": 66, 210 | "value": 8.3 211 | }, 212 | "statistics": { 213 | "albumCount": 0, 214 | "trackFileCount": 0, 215 | "trackCount": 0, 216 | "totalTrackCount": 0, 217 | "sizeOnDisk": 0, 218 | "percentOfTracks": 0 219 | }, 220 | "id": 7 221 | }, 222 | "images": [], 223 | "links": [], 224 | "statistics": { 225 | "trackFileCount": 0, 226 | "trackCount": 20, 227 | "totalTrackCount": 20, 228 | "sizeOnDisk": 0, 229 | "percentOfTracks": 0 230 | }, 231 | "grabbed": false, 232 | "id": 101 233 | } 234 | ] 235 | -------------------------------------------------------------------------------- /tests/fixtures/common/qualityprofile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Any", 3 | "upgradeAllowed": false, 4 | "cutoff": 4, 5 | "items": [ 6 | { 7 | "quality": { 8 | "id": 0, 9 | "name": "Unknown", 10 | "source": "unknown", 11 | "resolution": 0 12 | }, 13 | "items": [], 14 | "allowed": false 15 | }, 16 | { 17 | "quality": { 18 | "id": 1, 19 | "name": "SDTV", 20 | "source": "television", 21 | "resolution": 480 22 | }, 23 | "items": [], 24 | "allowed": false 25 | }, 26 | { 27 | "name": "WEB 480p", 28 | "items": [ 29 | { 30 | "quality": { 31 | "id": 12, 32 | "name": "WEBRip-480p", 33 | "source": "webRip", 34 | "resolution": 480 35 | }, 36 | "items": [], 37 | "allowed": false 38 | }, 39 | { 40 | "quality": { 41 | "id": 8, 42 | "name": "WEBDL-480p", 43 | "source": "web", 44 | "resolution": 480 45 | }, 46 | "items": [], 47 | "allowed": false 48 | } 49 | ], 50 | "allowed": false, 51 | "id": 1000 52 | }, 53 | { 54 | "name": "DVD", 55 | "items": [ 56 | { 57 | "quality": { 58 | "id": 2, 59 | "name": "DVD", 60 | "source": "dvd", 61 | "resolution": 480 62 | }, 63 | "items": [], 64 | "allowed": true 65 | }, 66 | { 67 | "quality": { 68 | "id": 13, 69 | "name": "Bluray-480p", 70 | "source": "bluray", 71 | "resolution": 480 72 | }, 73 | "items": [], 74 | "allowed": true 75 | } 76 | ], 77 | "allowed": true, 78 | "id": 1001 79 | }, 80 | { 81 | "quality": { 82 | "id": 4, 83 | "name": "HDTV-720p", 84 | "source": "television", 85 | "resolution": 720 86 | }, 87 | "items": [], 88 | "allowed": true 89 | }, 90 | { 91 | "quality": { 92 | "id": 9, 93 | "name": "HDTV-1080p", 94 | "source": "television", 95 | "resolution": 1080 96 | }, 97 | "items": [], 98 | "allowed": true 99 | }, 100 | { 101 | "quality": { 102 | "id": 10, 103 | "name": "Raw-HD", 104 | "source": "televisionRaw", 105 | "resolution": 1080 106 | }, 107 | "items": [], 108 | "allowed": false 109 | }, 110 | { 111 | "name": "WEB 720p", 112 | "items": [ 113 | { 114 | "quality": { 115 | "id": 14, 116 | "name": "WEBRip-720p", 117 | "source": "webRip", 118 | "resolution": 720 119 | }, 120 | "items": [], 121 | "allowed": true 122 | }, 123 | { 124 | "quality": { 125 | "id": 5, 126 | "name": "WEBDL-720p", 127 | "source": "web", 128 | "resolution": 720 129 | }, 130 | "items": [], 131 | "allowed": true 132 | } 133 | ], 134 | "allowed": true, 135 | "id": 1002 136 | }, 137 | { 138 | "quality": { 139 | "id": 6, 140 | "name": "Bluray-720p", 141 | "source": "bluray", 142 | "resolution": 720 143 | }, 144 | "items": [], 145 | "allowed": true 146 | }, 147 | { 148 | "name": "WEB 1080p", 149 | "items": [ 150 | { 151 | "quality": { 152 | "id": 15, 153 | "name": "WEBRip-1080p", 154 | "source": "webRip", 155 | "resolution": 1080 156 | }, 157 | "items": [], 158 | "allowed": true 159 | }, 160 | { 161 | "quality": { 162 | "id": 3, 163 | "name": "WEBDL-1080p", 164 | "source": "web", 165 | "resolution": 1080 166 | }, 167 | "items": [], 168 | "allowed": true 169 | } 170 | ], 171 | "allowed": true, 172 | "id": 1003 173 | }, 174 | { 175 | "quality": { 176 | "id": 7, 177 | "name": "Bluray-1080p", 178 | "source": "bluray", 179 | "resolution": 1080 180 | }, 181 | "items": [], 182 | "allowed": true 183 | }, 184 | { 185 | "quality": { 186 | "id": 20, 187 | "name": "Bluray-1080p Remux", 188 | "source": "blurayRaw", 189 | "resolution": 1080 190 | }, 191 | "items": [], 192 | "allowed": false 193 | }, 194 | { 195 | "quality": { 196 | "id": 16, 197 | "name": "HDTV-2160p", 198 | "source": "television", 199 | "resolution": 2160 200 | }, 201 | "items": [], 202 | "allowed": false 203 | }, 204 | { 205 | "name": "WEB 2160p", 206 | "items": [ 207 | { 208 | "quality": { 209 | "id": 17, 210 | "name": "WEBRip-2160p", 211 | "source": "webRip", 212 | "resolution": 2160 213 | }, 214 | "items": [], 215 | "allowed": false 216 | }, 217 | { 218 | "quality": { 219 | "id": 18, 220 | "name": "WEBDL-2160p", 221 | "source": "web", 222 | "resolution": 2160 223 | }, 224 | "items": [], 225 | "allowed": false 226 | } 227 | ], 228 | "allowed": false, 229 | "id": 1004 230 | }, 231 | { 232 | "quality": { 233 | "id": 19, 234 | "name": "Bluray-2160p", 235 | "source": "bluray", 236 | "resolution": 2160 237 | }, 238 | "items": [], 239 | "allowed": false 240 | }, 241 | { 242 | "quality": { 243 | "id": 21, 244 | "name": "Bluray-2160p Remux", 245 | "source": "blurayRaw", 246 | "resolution": 2160 247 | }, 248 | "items": [], 249 | "allowed": false 250 | } 251 | ], 252 | "id": 1 253 | } 254 | -------------------------------------------------------------------------------- /pyarr/request_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | 3 | import requests 4 | from requests import Response 5 | from requests.auth import HTTPBasicAuth 6 | 7 | from .exceptions import ( 8 | PyarrAccessRestricted, 9 | PyarrBadGateway, 10 | PyarrBadRequest, 11 | PyarrConnectionError, 12 | PyarrMethodNotAllowed, 13 | PyarrResourceNotFound, 14 | PyarrServerError, 15 | PyarrUnauthorizedError, 16 | ) 17 | from .types import _ReturnType 18 | 19 | 20 | class RequestHandler: 21 | """Base class for API Wrappers""" 22 | 23 | def __init__( 24 | self, 25 | host_url: str, 26 | api_key: str, 27 | ): 28 | """Constructor for connection to Arr API 29 | 30 | Args: 31 | host_url (str): Host URL to Arr api 32 | api_key (str): API Key for Arr api 33 | """ 34 | self.host_url = host_url.rstrip("/") 35 | self.api_key = api_key 36 | self.session: requests.Session = requests.Session() 37 | self.auth: Union[HTTPBasicAuth, None] = None 38 | 39 | def _request_url(self, path: str, ver_uri: str) -> str: 40 | """Builds the URL for the request to use. 41 | 42 | Args: 43 | path (str): Destination for specific call 44 | ver_uri (str): API Version number 45 | 46 | Returns: 47 | str: string URL for API endpoint 48 | """ 49 | return f"{self.host_url}/api{ver_uri}/{path}" 50 | 51 | def basic_auth(self, username: str, password: str) -> Union[HTTPBasicAuth, None]: 52 | """If you have basic authentication setup you will need to pass your 53 | username and passwords to the HTTPBASICAUTH() method. 54 | 55 | Args: 56 | username (str): Username for basic auth. 57 | password (str): Password for basic auth. 58 | 59 | Returns: 60 | Object: HTTP Auth object 61 | """ 62 | return HTTPBasicAuth(username, password) 63 | 64 | def _get( 65 | self, 66 | path: str, 67 | ver_uri: str = "", 68 | params: Union[dict[str, Any], list[tuple[str, Any]], None] = None, 69 | ) -> _ReturnType: 70 | """Wrapper on any get requests 71 | 72 | Args: 73 | path (str): Path to API endpoint e.g. /api/manualimport 74 | params (dict, optional): URL Parameters to send with the request. Defaults to None. 75 | 76 | Returns: 77 | Object: Response object from requests 78 | """ 79 | headers = {"X-Api-Key": self.api_key} 80 | try: 81 | res = self.session.get( 82 | self._request_url(path, ver_uri), 83 | headers=headers, 84 | params=params, 85 | auth=self.auth, 86 | ) 87 | except requests.Timeout as exception: 88 | raise PyarrConnectionError( 89 | "Timeout occurred while connecting to API." 90 | ) from exception 91 | response = _process_response(res) 92 | return self._return(res, dict if isinstance(response, dict) else list) 93 | 94 | def _post( 95 | self, 96 | path: str, 97 | ver_uri: str = "", 98 | params: Union[dict, None] = None, 99 | data: Union[list[dict], dict, None] = None, 100 | ) -> _ReturnType: 101 | """Wrapper on any post requests 102 | 103 | Args: 104 | path (str): Path to API endpoint e.g. /api/manualimport 105 | params (dict, optional): URL Parameters to send with the request. Defaults to None. 106 | data (dict, optional): Payload to send with request. Defaults to None. 107 | 108 | Returns: 109 | Object: Response object from requests 110 | """ 111 | headers = {"X-Api-Key": self.api_key} 112 | try: 113 | res = self.session.post( 114 | self._request_url(path, ver_uri), 115 | headers=headers, 116 | params=params, 117 | json=data, 118 | auth=self.auth, 119 | ) 120 | 121 | except requests.Timeout as exception: 122 | raise PyarrConnectionError( 123 | "Timeout occurred while connecting to API." 124 | ) from exception 125 | response = _process_response(res) 126 | return self._return(res, dict if isinstance(response, dict) else list) 127 | 128 | def _put( 129 | self, 130 | path: str, 131 | ver_uri: str, 132 | params: Optional[dict] = None, 133 | data: Optional[Union[dict[str, Any], list[dict[str, Any]]]] = None, 134 | ) -> _ReturnType: 135 | """Wrapper on any put requests 136 | 137 | Args: 138 | path (str): Path to API endpoint e.g. /api/manualimport 139 | ver_uri (str): API Version 140 | params (dict, optional): URL Parameters to send with the request. Defaults to None. 141 | data (dict, optional): Payload to send with request. Defaults to None. 142 | 143 | Returns: 144 | Object: Response object from requests 145 | """ 146 | headers = {"X-Api-Key": self.api_key} 147 | try: 148 | res = self.session.put( 149 | self._request_url(path, ver_uri), 150 | headers=headers, 151 | params=params, 152 | json=data, 153 | auth=self.auth, 154 | ) 155 | except requests.Timeout as exception: 156 | raise PyarrConnectionError( 157 | "Timeout occurred while connecting to API." 158 | ) from exception 159 | 160 | response = _process_response(res) 161 | return self._return(res, dict if isinstance(response, dict) else list) 162 | 163 | def _delete( 164 | self, 165 | path: str, 166 | ver_uri: str = "", 167 | params: Union[dict, None] = None, 168 | data: Union[dict, None] = None, 169 | ) -> Union[Response, dict[str, Any], dict[Any, Any]]: 170 | """Wrapper on any delete requests 171 | 172 | Args: 173 | path (str): Path to API endpoint e.g. /api/manualimport 174 | params (dict, optional): URL Parameters to send with the request. Defaults to None. 175 | data (dict, optional): Payload to send with request. Defaults to None. 176 | 177 | Returns: 178 | Object: Response object from requests 179 | """ 180 | headers = {"X-Api-Key": self.api_key} 181 | try: 182 | res = self.session.delete( 183 | self._request_url(path, ver_uri), 184 | headers=headers, 185 | params=params, 186 | json=data, 187 | auth=self.auth, 188 | ) 189 | except requests.Timeout as exception: 190 | raise PyarrConnectionError( 191 | "Timeout occurred while connecting to API" 192 | ) from exception 193 | response = _process_response(res) 194 | if isinstance(response, dict): 195 | assert isinstance(response, dict) 196 | else: 197 | assert isinstance(response, Response) 198 | return response 199 | 200 | def _return(self, res: Response, arg1: type) -> Any: 201 | """Takes the response and asserts its type 202 | 203 | Args: 204 | res (Response): Response from request 205 | arg1 (type): The type that should be asserted 206 | 207 | Returns: 208 | Any: Many possible return types 209 | """ 210 | response = _process_response(res) 211 | assert isinstance(response, arg1) 212 | return response 213 | 214 | 215 | def _process_response( 216 | res: Response, 217 | ) -> Union[list[dict[str, Any]], Response, dict[str, Any], Any]: 218 | """Check the response status code and error or return results 219 | 220 | Args: 221 | res (str): JSON or Text response from API Call 222 | 223 | Raises: 224 | PyarrUnauthorizedError: Invalid API Key 225 | PyarrAccessRestricted: Invalid Permissions 226 | PyarrResourceNotFound: Incorrect Resource 227 | PyarrBadGateway: Bad Gateway 228 | 229 | Returns: 230 | JSON: Array 231 | """ 232 | if res.status_code == 400: 233 | raise PyarrBadRequest(f"Bad Request, possibly a bug. {str(res.content)}") 234 | 235 | if res.status_code == 401: 236 | raise PyarrUnauthorizedError( 237 | "Unauthorized. Please ensure valid API Key is used.", {} 238 | ) 239 | if res.status_code == 403: 240 | raise PyarrAccessRestricted( 241 | "Access restricted. Please ensure API Key has correct permissions", {} 242 | ) 243 | if res.status_code == 404: 244 | raise PyarrResourceNotFound("Resource not found") 245 | if res.status_code == 405: 246 | raise PyarrMethodNotAllowed(f"The endpoint {res.url} is not allowed") 247 | if res.status_code == 500: 248 | raise PyarrServerError( 249 | f"Internal Server Error: {res.json()['message']}", 250 | res.json(), 251 | ) 252 | if res.status_code == 502: 253 | raise PyarrBadGateway("Bad Gateway. Check your server is accessible.") 254 | 255 | content_type = res.headers.get("Content-Type", "") 256 | if "application/json" in content_type: 257 | return res.json() 258 | else: 259 | assert isinstance(res, Response) 260 | return res 261 | -------------------------------------------------------------------------------- /testing_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 6, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "text/plain": [ 11 | "{'page': 1,\n", 12 | " 'pageSize': 10,\n", 13 | " 'sortKey': 'date',\n", 14 | " 'sortDirection': 'descending',\n", 15 | " 'totalRecords': 0,\n", 16 | " 'records': []}" 17 | ] 18 | }, 19 | "execution_count": 6, 20 | "metadata": {}, 21 | "output_type": "execute_result" 22 | } 23 | ], 24 | "source": [ 25 | "from pyarr import SonarrAPI\n", 26 | "from tests import SONARR_API_KEY, SONARR_TVDB\n", 27 | "\n", 28 | "\n", 29 | "sonarr_client = SonarrAPI(host_url=\"http://localhost:8989\", api_key=SONARR_API_KEY)\n", 30 | "\n", 31 | "sonarr_client.get_history()\n" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 19, 37 | "metadata": {}, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "{'name': 'RescanMovie',\n", 43 | " 'commandName': 'Rescan Movie',\n", 44 | " 'body': {'movieId': 2,\n", 45 | " 'sendUpdatesToClient': True,\n", 46 | " 'updateScheduledTask': True,\n", 47 | " 'isLongRunning': False,\n", 48 | " 'requiresDiskAccess': False,\n", 49 | " 'isExclusive': False,\n", 50 | " 'isTypeExclusive': False,\n", 51 | " 'name': 'RescanMovie',\n", 52 | " 'trigger': 'manual',\n", 53 | " 'suppressMessages': False},\n", 54 | " 'priority': 'normal',\n", 55 | " 'status': 'completed',\n", 56 | " 'result': 'successful',\n", 57 | " 'queued': '2024-06-04T21:04:38Z',\n", 58 | " 'started': '2024-06-04T21:04:38Z',\n", 59 | " 'ended': '2024-06-04T21:04:38Z',\n", 60 | " 'duration': '00:00:00.0570204',\n", 61 | " 'trigger': 'manual',\n", 62 | " 'stateChangeTime': '2024-06-04T21:04:38Z',\n", 63 | " 'sendUpdatesToClient': True,\n", 64 | " 'updateScheduledTask': True,\n", 65 | " 'id': 35}" 66 | ] 67 | }, 68 | "execution_count": 19, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "from pyarr import RadarrAPI\n", 75 | "from tests import RADARR_API_KEY, RADARR_IMDB\n", 76 | "import time\n", 77 | "\n", 78 | "radarr_client = RadarrAPI(host_url=\"http://localhost:7878\", api_key=RADARR_API_KEY)\n", 79 | "quality_profiles = radarr_client.get_quality_profile()\n", 80 | "movie_imdb = radarr_client.lookup_movie(term=f\"imdb:{RADARR_IMDB}\")\n", 81 | "\n", 82 | "data = radarr_client.add_movie(\n", 83 | " movie=movie_imdb[0],\n", 84 | " root_dir=\"/defaults/\",\n", 85 | " quality_profile_id=quality_profiles[0][\"id\"],\n", 86 | " monitored=False,\n", 87 | " search_for_movie=False,\n", 88 | " monitor=\"movieOnly\",\n", 89 | " minimum_availability=\"announced\",\n", 90 | " )\n", 91 | "\n", 92 | "data = radarr_client.post_command(\n", 93 | " name=\"RescanMovie\", movieId=radarr_client.get_movie()[0][\"id\"]\n", 94 | " )\n", 95 | "time.sleep(5)\n", 96 | "radarr_client.get_command(id_=data[\"id\"])" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 15, 102 | "metadata": {}, 103 | "outputs": [ 104 | { 105 | "data": { 106 | "text/plain": [ 107 | "[{'title': 'The Life of the Bee',\n", 108 | " 'authorTitle': 'maeterlinck, maurice The Life of the Bee',\n", 109 | " 'seriesTitle': '',\n", 110 | " 'disambiguation': '',\n", 111 | " 'authorId': 3,\n", 112 | " 'foreignBookId': '1207892',\n", 113 | " 'foreignEditionId': '489521',\n", 114 | " 'titleSlug': '1207892',\n", 115 | " 'monitored': False,\n", 116 | " 'anyEditionOk': False,\n", 117 | " 'ratings': {'votes': 393, 'value': 3.9, 'popularity': 1532.7},\n", 118 | " 'releaseDate': '1901-01-01T00:00:00Z',\n", 119 | " 'pageCount': 192,\n", 120 | " 'genres': ['non-fiction',\n", 121 | " 'science',\n", 122 | " 'nature',\n", 123 | " 'philosophy',\n", 124 | " 'nobel-prize',\n", 125 | " 'classics',\n", 126 | " 'belgium',\n", 127 | " 'animals',\n", 128 | " 'belgian',\n", 129 | " 'biology'],\n", 130 | " 'images': [{'url': '/MediaCover/Books/14/cover.jpg',\n", 131 | " 'coverType': 'cover',\n", 132 | " 'extension': '.jpg'}],\n", 133 | " 'links': [{'url': 'https://www.goodreads.com/work/editions/1207892',\n", 134 | " 'name': 'Goodreads Editions'},\n", 135 | " {'url': 'https://www.goodreads.com/book/show/489521.The_Life_of_the_Bee',\n", 136 | " 'name': 'Goodreads Book'}],\n", 137 | " 'statistics': {'bookFileCount': 0,\n", 138 | " 'bookCount': 0,\n", 139 | " 'totalBookCount': 1,\n", 140 | " 'sizeOnDisk': 0,\n", 141 | " 'percentOfBooks': 0},\n", 142 | " 'added': '2024-06-04T20:54:54Z',\n", 143 | " 'grabbed': False,\n", 144 | " 'id': 14}]" 145 | ] 146 | }, 147 | "execution_count": 15, 148 | "metadata": {}, 149 | "output_type": "execute_result" 150 | } 151 | ], 152 | "source": [ 153 | "from pyarr import ReadarrAPI\n", 154 | "from tests import READARR_API_KEY, READARR_GOODREADS_ID\n", 155 | "\n", 156 | "\n", 157 | "readarr_client = ReadarrAPI(host_url=\"http://localhost:8787\", api_key=READARR_API_KEY)\n", 158 | "qual_profile = readarr_client.get_quality_profile()\n", 159 | "meta_profile = readarr_client.get_metadata_profile()\n", 160 | "\n", 161 | "items = readarr_client.lookup(f\"edition:{READARR_GOODREADS_ID}\")\n", 162 | "for item in items:\n", 163 | " if \"book\" in item:\n", 164 | " book = item[\"book\"]\n", 165 | " data = readarr_client.add_book(\n", 166 | " book=book,\n", 167 | " root_dir=\"/defaults/\",\n", 168 | " quality_profile_id=qual_profile[0][\"id\"],\n", 169 | " metadata_profile_id=meta_profile[0][\"id\"],\n", 170 | " )\n", 171 | " break\n", 172 | "books = readarr_client.get_book()\n", 173 | "book_ids = [d.get(\"id\") for d in books]\n", 174 | "\n", 175 | "readarr_client.upd_book_monitor(book_ids=book_ids, monitored=False)\n" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 17, 181 | "metadata": {}, 182 | "outputs": [ 183 | { 184 | "data": { 185 | "text/plain": [ 186 | "{'name': 'DownloadedAlbumsScan',\n", 187 | " 'commandName': 'Downloaded Albums Scan',\n", 188 | " 'message': 'Failed to import',\n", 189 | " 'body': {'path': '/defaults',\n", 190 | " 'importMode': 'auto',\n", 191 | " 'requiresDiskAccess': True,\n", 192 | " 'isLongRunning': True,\n", 193 | " 'sendUpdatesToClient': True,\n", 194 | " 'updateScheduledTask': True,\n", 195 | " 'isExclusive': False,\n", 196 | " 'isTypeExclusive': False,\n", 197 | " 'name': 'DownloadedAlbumsScan',\n", 198 | " 'trigger': 'manual',\n", 199 | " 'suppressMessages': True},\n", 200 | " 'priority': 'normal',\n", 201 | " 'status': 'completed',\n", 202 | " 'result': 'unsuccessful',\n", 203 | " 'queued': '2024-06-04T21:01:30Z',\n", 204 | " 'started': '2024-06-04T21:01:30Z',\n", 205 | " 'ended': '2024-06-04T21:01:30Z',\n", 206 | " 'duration': '00:00:00.0100103',\n", 207 | " 'trigger': 'manual',\n", 208 | " 'stateChangeTime': '2024-06-04T21:01:30Z',\n", 209 | " 'sendUpdatesToClient': True,\n", 210 | " 'updateScheduledTask': True,\n", 211 | " 'id': 38}" 212 | ] 213 | }, 214 | "execution_count": 17, 215 | "metadata": {}, 216 | "output_type": "execute_result" 217 | } 218 | ], 219 | "source": [ 220 | "from pyarr import LidarrAPI\n", 221 | "from tests import LIDARR_API_KEY, LIDARR_MUSICBRAINZ_ALBUM_ID\n", 222 | "import time\n", 223 | "\n", 224 | "lidarr_client = LidarrAPI(host_url=\"http://localhost:8686\", api_key=\"f0b398ba17c04645bea28ca934d003e0\")\n", 225 | "\n", 226 | "qual_profile = lidarr_client.get_quality_profile()\n", 227 | "meta_profile = lidarr_client.get_metadata_profile()\n", 228 | "data = lidarr_client.add_root_folder(\n", 229 | " name=\"test\",\n", 230 | " path=\"/defaults/\",\n", 231 | " default_quality_profile_id=qual_profile[0][\"id\"],\n", 232 | " default_metadata_profile_id=meta_profile[0][\"id\"],\n", 233 | " )\n", 234 | "data = lidarr_client.post_command(\n", 235 | " name=\"DownloadedAlbumsScan\", path=lidarr_client.get_root_folder()[0][\"path\"]\n", 236 | " )\n", 237 | "time.sleep(5)\n", 238 | "lidarr_client.get_command(id_=data[\"id\"])" 239 | ] 240 | } 241 | ], 242 | "metadata": { 243 | "kernelspec": { 244 | "display_name": "pyarr-lvwSnSoa-py3.12", 245 | "language": "python", 246 | "name": "python3" 247 | }, 248 | "language_info": { 249 | "codemirror_mode": { 250 | "name": "ipython", 251 | "version": 3 252 | }, 253 | "file_extension": ".py", 254 | "mimetype": "text/x-python", 255 | "name": "python", 256 | "nbconvert_exporter": "python", 257 | "pygments_lexer": "ipython3", 258 | "version": "3.12.3" 259 | } 260 | }, 261 | "nbformat": 4, 262 | "nbformat_minor": 2 263 | } 264 | --------------------------------------------------------------------------------