diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 7ad98012..00000000
--- a/.gitignore
+++ /dev/null
@@ -1,258 +0,0 @@
-
-# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,vscode
-# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,vscode
-
-### PyCharm ###
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-### PyCharm Patch ###
-# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
-
-# *.iml
-# modules.xml
-# .idea/misc.xml
-# *.ipr
-
-# Sonarlint plugin
-# https://plugins.jetbrains.com/plugin/7973-sonarlint
-.idea/**/sonarlint/
-
-# SonarQube Plugin
-# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
-.idea/**/sonarIssues.xml
-
-# Markdown Navigator plugin
-# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
-.idea/**/markdown-navigator.xml
-.idea/**/markdown-navigator-enh.xml
-.idea/**/markdown-navigator/
-
-# Cache file creation bug
-# See https://youtrack.jetbrains.com/issue/JBR-2257
-.idea/$CACHE_FILE$
-
-# CodeStream plugin
-# https://plugins.jetbrains.com/plugin/12206-codestream
-.idea/codestream.xml
-
-### Python ###
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-pytestdebug.log
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-doc/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-pythonenv*
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# profiling data
-.prof
-
-### vscode ###
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-*.code-workspace
-
-# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,vscode
-
-trash.yml
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d33521..00000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/TrashUpdater.iml b/.idea/TrashUpdater.iml
deleted file mode 100644
index 72a80507..00000000
--- a/.idea/TrashUpdater.iml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index a55e7a17..00000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index dd4c951e..00000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index ab4cd9b2..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index ebca9072..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.markdownlint.json b/.markdownlint.json
deleted file mode 100644
index 39e1e186..00000000
--- a/.markdownlint.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "default": true,
- "line-length": {
- "line_length": 100,
- "tables": false
- },
- "no-inline-html": {
- "allowed_elements": ["br"]
- }
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 99b087e5..00000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "python.linting.enabled": true
-}
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index a1060c4a..00000000
--- a/README.md
+++ /dev/null
@@ -1,179 +0,0 @@
-# TRaSH Guide Updater Script
-
-Automatically mirror TRaSH guides to your Sonarr/Radarr instance.
-
-> **NOTICE**: This is a work-in-progress Python script
-
-## Features
-
-Features list will continue to grow. See the limitations & roadmap section for more details!
-
-* Sonarr Release Profiles
- * Preferred, Must Not Contain, and Must Contain lists from guides are reflected completely in
- corresponding fields in release profiles in Sonarr.
- * "Include Preferred when Renaming" is properly checked/unchecked depending on explicit mention of
- this in the guides.
- * Profiles get created if they do not exist, or updated if they already exist. Profiles get a
- unique name based on the guide and this name is used to find them in subsequent runs.
- * Tags can be added to any updated or created profiles.
- * Ability to convert preferred with negative scores to "Must not contain" terms.
-* Sonarr Quality Definitions
- * Anime and Non-Anime quality definitions are now synced to Sonarr
-* Radarr Quality Definition can be synced (there's only one for now).
-* Configuration support using YAML
- * Many command line arguments can instead be provided in YAML configuration to reduce the
- redundancy of using the CLI.
-
-## Requirements
-
-* Python 3
-* The following packages installed with `pip`:
- * `requests`
- * `packaging`
- * `pyyaml`
-* For Sonarr updates, you must be running version `3.0.4.1098` or greater.
-
-To install all of the above required packages, here's a convenient copy & paste one-liner:
-
-```txt
-pip install requests packaging pyyaml
-```
-
-## Getting Started
-
-The only script you will need to be using is `src/trash.py`. If you've cloned my repository, simply
-`cd` to the `src` directory so you can run `trash.py` directly:
-
-```txt
-PS E:\code\TrashUpdater\src> .\trash.py -h
-usage: trash.py [-h] {profile,quality} ...
-
-Automatically mirror TRaSH guides to your Sonarr/Radarr instance.
-
-optional arguments:
- -h, --help show this help message and exit
-
-subcommands:
- Operations specific to different parts of the TRaSH guides
-
- {profile,quality}
- profile Pages of the guide that define profiles
- quality Pages in the guide that provide quality definitions
-```
-
-The command line is structured into a series of subcommands that each handle a different area of the
-guides. For example, you use a separate subcommand to sync quality definitions than you do release
-profiles. Simply run `trash.py [subcommand] -h` to get help for `[subcommand]`, which can be any
-supported subcommand listed in the top level help output.
-
-### Examples
-
-Some command line examples to show you how to use the script for various tasks. Note that most
-command line options were generated on a Windows environment, so you will see OS-specific syntax
-(e.g. backslashes). Obviously Python works on Linux systems too, so adjust the examples as needed
-for your platform.
-
-To preview what release profile information is parsed out of the Anime profile guide:
-
-```txt
-.\trash.py profile sonarr:anime --preview
-```
-
-To sync the anime release profiles to your Sonarr instance:
-
-```txt
-.\trash.py profile sonarr:anime --base-uri http://localhost:8989 --api-key a95cc792074644759fefe3ccab544f6e
-```
-
-To preview the Anime quality definition data parsed out of the Quality Definitions (file sizes) page
-of the TRaSH guides:
-
-```txt
-.\trash.py quality sonarr:anime --preview
-```
-
-Sync the non-anime quality definition to Sonarr:
-
-```txt
-.\trash.py quality sonarr:non-anime --base-uri http://localhost:8989 --api-key a95cc792074644759fefe3ccab544f6e
-```
-
-## Configuration File
-
-By default, `trash.py` will look for a configuration file named `trash.yml` in the same directory as
-the script itself. This configuration file may be used to store your Sonarr and Radarr Base URI and
-API Key, which should make using the command line interface a bit less clunky.
-
-```yml
-sonarr:
- base_uri: http://localhost:8989
- api_key: a95cc792074644759fefe3ccab544f6e
- profile:
- - type: anime
- tags:
- - anime
- - type: web-dl
- tags:
- - tv
-```
-
-Note that this file is not required to be present. If it is not present, then you will need to set
-respective parameters using the equivalent command line arguments (e.g. `--base-uri` and
-`--api-key`), as needed.
-
-Lastly, there's a `--config-file` argument you can use to point to your own YAML config file if you
-don't like the where the default one is located.
-
-### Profile Settings
-
-* **`profile`**
- Provide a list of settings used per each type of release profile supported in the guide (e.g.
- `web-dl`, `anime`).
-
- * **`type`**
- Type profile type to apply the settings to, such as adding new tags. The list of supported
- profile types can be found by doing `trash.py profile -h`. Each valid choice listed under the
- `type` argument can be used, just strip the `sonarr:` prefix.
-
- * **`tags`**
- A list of tags to apply to the profile. Functions exactly as it would if you used the `--tags`
- option to provide this list on the command line.
-
-## Important Notices
-
-Please be aware that this script relies on a deterministic and consistent structure of the TRaSH
-Guide markdown files. I'm in the process of creating a set of rules/guidelines to reduce the risk of
-the guide breaking this script, but in the meantime the script may stop working at any time due to
-guide updates. I will do my best to fix them in a timely manner. Reporting such issues would be
-appreciated and will help identify issues more quickly.
-
-### Limitations
-
-This script is a work in progress. At the moment, it only supports the following features and/or has
-the following limitations:
-
-* Radarr custom formats are not supported yet (coming soon).
-* Multiple scores on the same line are not supported. Only the first is used.
-
-### Roadmap
-
-In addition to the above limitations, the following items are planned for the future.
-
-* Better and more polished error handling (it's pretty minimal right now)
-* Implement some sort of guide versioning (e.g. to avoid updating a release profile if the guide did
- not change).
-
-## Development / Contributing
-
-### Prerequisites
-
-Some additional packages are required to run the unit tests. All can be installed via `pip`:
-
-* `pytest`
-* `pytest-mock`
-
-To install all of the above required packages, here's a convenient copy & paste one-liner:
-
-```txt
-pip install pytest pytest-mock
-```
diff --git a/sonarr_api_examples/qualityProfile.update.json b/sonarr_api_examples/qualityProfile.update.json
deleted file mode 100644
index a6d54c51..00000000
--- a/sonarr_api_examples/qualityProfile.update.json
+++ /dev/null
@@ -1,268 +0,0 @@
-[{
- "quality": {
- "id": 0,
- "name": "Unknown",
- "source": "unknown",
- "resolution": 0
- },
- "title": "Unknown",
- "weight": 1,
- "minSize": 1.0,
- "maxSize": 227.5,
- "id": 1
- },
- {
- "quality": {
- "id": 1,
- "name": "SDTV",
- "source": "television",
- "resolution": 480
- },
- "title": "SDTV",
- "weight": 2,
- "minSize": 2.0,
- "maxSize": 100.0,
- "id": 2
- },
- {
- "quality": {
- "id": 12,
- "name": "WEBRip-480p",
- "source": "webRip",
- "resolution": 480
- },
- "title": "WEBRip-480p",
- "weight": 3,
- "minSize": 2.0,
- "maxSize": 100.0,
- "id": 3
- },
- {
- "quality": {
- "id": 8,
- "name": "WEBDL-480p",
- "source": "web",
- "resolution": 480
- },
- "title": "WEBDL-480p",
- "weight": 3,
- "minSize": 2.0,
- "maxSize": 100.0,
- "id": 4
- },
- {
- "quality": {
- "id": 2,
- "name": "DVD",
- "source": "dvd",
- "resolution": 480
- },
- "title": "DVD",
- "weight": 4,
- "minSize": 2.0,
- "maxSize": 100.0,
- "id": 5
- },
- {
- "quality": {
- "id": 13,
- "name": "Bluray-480p",
- "source": "bluray",
- "resolution": 480
- },
- "title": "Bluray-480p",
- "weight": 5,
- "minSize": 2.0,
- "maxSize": 100.0,
- "id": 6
- },
- {
- "quality": {
- "id": 4,
- "name": "HDTV-720p",
- "source": "television",
- "resolution": 720
- },
- "title": "HDTV-720p",
- "weight": 6,
- "minSize": 3.0,
- "maxSize": 125.0,
- "id": 7
- },
- {
- "quality": {
- "id": 9,
- "name": "HDTV-1080p",
- "source": "television",
- "resolution": 1080
- },
- "title": "HDTV-1080p",
- "weight": 7,
- "minSize": 4.0,
- "maxSize": 125.0,
- "id": 8
- },
- {
- "quality": {
- "id": 10,
- "name": "Raw-HD",
- "source": "televisionRaw",
- "resolution": 1080
- },
- "title": "Raw-HD",
- "weight": 8,
- "minSize": 4.0,
- "id": 9
- },
- {
- "quality": {
- "id": 14,
- "name": "WEBRip-720p",
- "source": "webRip",
- "resolution": 720
- },
- "title": "WEBRip-720p",
- "weight": 9,
- "minSize": 3.0,
- "maxSize": 130.0,
- "id": 10
- },
- {
- "quality": {
- "id": 5,
- "name": "WEBDL-720p",
- "source": "web",
- "resolution": 720
- },
- "title": "WEBDL-720p",
- "weight": 9,
- "minSize": 3.0,
- "maxSize": 130.0,
- "id": 11
- },
- {
- "quality": {
- "id": 6,
- "name": "Bluray-720p",
- "source": "bluray",
- "resolution": 720
- },
- "title": "Bluray-720p",
- "weight": 10,
- "minSize": 4.0,
- "maxSize": 130.0,
- "id": 12
- },
- {
- "quality": {
- "id": 15,
- "name": "WEBRip-1080p",
- "source": "webRip",
- "resolution": 1080
- },
- "title": "WEBRip-1080p",
- "weight": 11,
- "minSize": 4.0,
- "maxSize": 130.0,
- "id": 13
- },
- {
- "quality": {
- "id": 3,
- "name": "WEBDL-1080p",
- "source": "web",
- "resolution": 1080
- },
- "title": "WEBDL-1080p",
- "weight": 11,
- "minSize": 4.0,
- "maxSize": 130.0,
- "id": 14
- },
- {
- "quality": {
- "id": 7,
- "name": "Bluray-1080p",
- "source": "bluray",
- "resolution": 1080
- },
- "title": "Bluray-1080p",
- "weight": 12,
- "minSize": 4.0,
- "maxSize": 155.0,
- "id": 15
- },
- {
- "quality": {
- "id": 20,
- "name": "Bluray-1080p Remux",
- "source": "blurayRaw",
- "resolution": 1080
- },
- "title": "Bluray-1080p Remux",
- "weight": 13,
- "minSize": 35.0,
- "id": 16
- },
- {
- "quality": {
- "id": 16,
- "name": "HDTV-2160p",
- "source": "television",
- "resolution": 2160
- },
- "title": "HDTV-2160p",
- "weight": 14,
- "minSize": 35.0,
- "maxSize": 199.9,
- "id": 17
- },
- {
- "quality": {
- "id": 17,
- "name": "WEBRip-2160p",
- "source": "webRip",
- "resolution": 2160
- },
- "title": "WEBRip-2160p",
- "weight": 15,
- "minSize": 59.0,
- "id": 18
- },
- {
- "quality": {
- "id": 18,
- "name": "WEBDL-2160p",
- "source": "web",
- "resolution": 2160
- },
- "title": "WEBDL-2160p",
- "weight": 15,
- "minSize": 59.0,
- "id": 19
- },
- {
- "quality": {
- "id": 19,
- "name": "Bluray-2160p",
- "source": "bluray",
- "resolution": 2160
- },
- "title": "Bluray-2160p",
- "weight": 16,
- "minSize": 59.0,
- "id": 20
- },
- {
- "quality": {
- "id": 21,
- "name": "Bluray-2160p Remux",
- "source": "blurayRaw",
- "resolution": 2160
- },
- "title": "Bluray-2160p Remux",
- "weight": 17,
- "minSize": 58.2,
- "id": 21
- }
-]
diff --git a/sonarr_api_examples/releaseprofile.json b/sonarr_api_examples/releaseprofile.json
deleted file mode 100644
index ff192585..00000000
--- a/sonarr_api_examples/releaseprofile.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "enabled": true,
- "required": "one,two,three",
- "ignored": "one,two,three",
- "preferred": [{
- "key": "/abc/",
- "value": "100"
- }, {
- "key": "/xyz/",
- "value": "200"
- }],
- "includePreferredWhenRenaming": true,
- "tags": [2],
- "indexerId": 0
-}
diff --git a/src/app/__init__.py b/src/app/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/app/api/__init__.py b/src/app/api/__init__.py
deleted file mode 100644
index 8dd7c734..00000000
--- a/src/app/api/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import server, sonarr
\ No newline at end of file
diff --git a/src/app/api/radarr.py b/src/app/api/radarr.py
deleted file mode 100644
index 71b5a4aa..00000000
--- a/src/app/api/radarr.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import requests
-import json
-from copy import deepcopy
-
-from app.api.server import Server, TrashHttpError
-from app.trash_error import TrashError
-
-class RadarrHttpError(TrashHttpError):
- @staticmethod
- def get_error_message(response: requests.Response):
- content = json.loads(response.content)
- if len(content) > 0:
- if type(content) is list:
- return content[0]['errorMessage']
- elif type(content) is dict and 'message' in content:
- return content['message']
- return None
-
- def __str__(self):
- msg = f'HTTP Response Error [Status Code {self.response.status_code}] [URI: {self.response.url}]'
- if error_msg := RadarrHttpError.get_error_message(self.response):
- msg += f'\n Response Message: {error_msg}'
- return msg
-
-class Radarr(Server):
- # --------------------------------------------------------------------------------------------------
- def __init__(self, args, logger):
- if not args.base_uri or not args.api_key:
- raise TrashError('--base-uri and --api-key are required arguments when not using --preview')
-
- self.logger = logger
-
- base_uri = f'{args.base_uri}/api/v3'
- key = f'?apikey={args.api_key}'
- super().__init__(base_uri, key, RadarrHttpError)
-
- # --------------------------------------------------------------------------------------------------
- # GET /qualitydefinition
- def get_quality_definition(self):
- return self.request('get', '/qualitydefinition')
-
- # --------------------------------------------------------------------------------------------------
- # PUT /qualityDefinition/update
- def update_quality_definition(self, server_definition, guide_definition):
- new_definition = []
- for quality, min_value, max_value, preferred in guide_definition:
- entry = self.find_quality_definition_entry(server_definition, quality)
- if not entry:
- print(f'WARN: Quality definition lacks entry for {quality}; it will be skipped.')
- continue
- entry = deepcopy(entry)
- entry['minSize'] = min_value
- entry['maxSize'] = max_value
- entry['preferredSize'] = preferred
- new_definition.append(entry)
-
- self.logger.debug('Setting Quality '
- f'[Name: {entry["quality"]["name"]}] '
- f'[Source: {entry["quality"]["source"]}] '
- f'[Min: {entry["minSize"]}] '
- f'[Max: {entry["maxSize"]}] '
- f'[Preferred: {entry["preferredSize"]}] '
- )
-
- self.request('put', '/qualityDefinition/update', new_definition)
-
- # --------------------------------------------------------------------------------------------------
- def find_quality_definition_entry(self, definition, quality):
- for entry in definition:
- if entry.get('quality').get('name') == quality:
- return entry
-
- return None
\ No newline at end of file
diff --git a/src/app/api/server.py b/src/app/api/server.py
deleted file mode 100644
index a7b2b9d2..00000000
--- a/src/app/api/server.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import json
-import requests
-
-from app.trash_error import TrashError
-
-class TrashHttpError(TrashError):
- def __init__(self, response):
- self.response = response
-
-class Server:
- dispatch = {
- 'put': requests.put,
- 'get': requests.get,
- 'post': requests.post,
- }
-
- def __init__(self, base_uri, apikey, exception_strategy):
- self.base_uri = base_uri
- self.apikey = apikey
- self.exception_strategy = exception_strategy
-
- def build_uri(self, endpoint):
- return self.base_uri + endpoint + self.apikey
-
- def request(self, method, endpoint, data=None):
- r = Server.dispatch.get(method)(self.build_uri(endpoint), json.dumps(data))
- if 400 <= r.status_code < 600:
- raise self.exception_strategy(r)
- return json.loads(r.content)
diff --git a/src/app/api/sonarr.py b/src/app/api/sonarr.py
deleted file mode 100644
index 288ad887..00000000
--- a/src/app/api/sonarr.py
+++ /dev/null
@@ -1,154 +0,0 @@
-import requests
-import json
-from packaging import version # pip install packaging
-from copy import deepcopy
-
-from app.api.server import Server, TrashHttpError
-from app.profile_data import ProfileData
-from app.trash_error import TrashError
-
-class SonarrHttpError(TrashHttpError):
- @staticmethod
- def get_error_message(response: requests.Response):
- content = json.loads(response.content)
- if len(content) > 0:
- if type(content) is list:
- return content[0]['errorMessage']
- elif type(content) is dict and 'message' in content:
- return content['message']
- return None
-
- def __str__(self):
- msg = f'HTTP Response Error [Status Code {self.response.status_code}] [URI: {self.response.url}]'
- if error_msg := SonarrHttpError.get_error_message(self.response):
- msg += f'\n Response Message: {error_msg}'
- return msg
-
-class Sonarr(Server):
- # --------------------------------------------------------------------------------------------------
- def __init__(self, args, logger):
- if not args.base_uri or not args.api_key:
- raise TrashError('--base-uri and --api-key are required arguments when not using --preview')
-
- self.logger = logger
-
- base_uri = f'{args.base_uri}/api/v3'
- key = f'?apikey={args.api_key}'
- super().__init__(base_uri, key, SonarrHttpError)
- self.do_version_check()
-
- # --------------------------------------------------------------------------------------------------
- def get_version(self):
- body = self.request('get', '/system/status')
- return version.parse(body['version'])
-
- # --------------------------------------------------------------------------------------------------
- def create_release_profile(self, profile_name: str, profile: ProfileData, tag_ids: list):
- json_preferred = []
- for score, terms in profile.preferred.items():
- for term in terms:
- json_preferred.append({"key": term, "value": score})
-
- data = {
- 'name': profile_name,
- 'enabled': True,
- 'required': ','.join(profile.required),
- 'ignored': ','.join(profile.ignored),
- 'preferred': json_preferred,
- 'includePreferredWhenRenaming': profile.include_preferred_when_renaming,
- 'tags': tag_ids,
- 'indexerId': 0
- }
-
- self.request('post', '/releaseprofile', data)
-
- # --------------------------------------------------------------------------------------------------
- def get_release_profiles(self):
- return self.request('get', '/releaseprofile')
-
- # --------------------------------------------------------------------------------------------------
- def update_existing_profile(self, existing_profile, profile, tag_ids: list):
- profile_id = existing_profile['id']
- self.logger.debug(f'update existing profile with id {profile_id}')
-
- # Create the release profile
- json_preferred = []
- for score, terms in profile.preferred.items():
- for term in terms:
- json_preferred.append({"key": term, "value": score})
-
- existing_profile['required'] = ','.join(profile.required)
- existing_profile['ignored'] = ','.join(profile.ignored)
- existing_profile['preferred'] = json_preferred
- existing_profile['includePreferredWhenRenaming'] = profile.include_preferred_when_renaming
-
- if len(tag_ids) > 0:
- existing_profile['tags'] = tag_ids
-
- self.request('put', f'/releaseprofile/{profile_id}', existing_profile)
-
- # --------------------------------------------------------------------------------------------------
- def get_tags(self):
- return self.request('get', '/tag')
-
- # --------------------------------------------------------------------------------------------------
- def create_missing_tags(self, current_tags_json, new_tags: list):
- for t in current_tags_json:
- try:
- new_tags.remove(t['label'])
- except ValueError:
- # The tag is not in the list specified by the user; ignore and continue
- pass
-
- # Anything still left in `new_tags` represents tags we need to add in Sonarr
- for t in new_tags:
- self.logger.debug(f'Creating tag: {t}')
- r = self.request('post', '/tag', {'label': t})
- current_tags_json.append(r)
-
- return current_tags_json
-
- # --------------------------------------------------------------------------------------------------
- def do_version_check(self):
- # Since this script requires a specific version of v3 Sonarr that implements name support for
- # release profiles, we perform that version check here and bail out if it does not meet a minimum
- # required version.
- minimum_version = version.parse('3.0.4.1098')
- sonarr_version = self.get_version()
- if sonarr_version < minimum_version:
- raise TrashError(f'Your Sonarr version ({sonarr_version}) does not meet the minimum required version of {minimum_version} to use this script.')
-
- # --------------------------------------------------------------------------------------------------
- # GET /qualitydefinition
- def get_quality_definition(self):
- return self.request('get', '/qualitydefinition')
-
- # --------------------------------------------------------------------------------------------------
- # PUT /qualityDefinition/update
- def update_quality_definition(self, server_definition, guide_definition):
- new_definition = []
- for quality, min_value, max_value in guide_definition:
- entry = self.find_quality_definition_entry(server_definition, quality)
- if not entry:
- print(f'WARN: Quality definition lacks entry for {quality}; it will be skipped.')
- continue
- entry = deepcopy(entry)
- entry['minSize'] = min_value
- entry['maxSize'] = max_value
- new_definition.append(entry)
-
- self.logger.debug('Setting Quality '
- f'[Name: {entry["quality"]["name"]}] '
- f'[Min: {entry["minSize"]}] '
- f'[Max: {entry["maxSize"]}] '
- )
-
- self.request('put', '/qualityDefinition/update', new_definition)
-
- # --------------------------------------------------------------------------------------------------
- def find_quality_definition_entry(self, definition, quality):
- for entry in definition:
- if entry.get('quality').get('name') == quality:
- return entry
-
- return None
\ No newline at end of file
diff --git a/src/app/cmd.py b/src/app/cmd.py
deleted file mode 100644
index b816a3f1..00000000
--- a/src/app/cmd.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import argparse
-
-from app.guide.profile_types import types as profile_types
-from app.guide.quality_types import types as quality_types
-
-# class args: pass
-class _NoAction(argparse.Action):
- def __init__(self, **kwargs):
- kwargs.setdefault('default', argparse.SUPPRESS)
- kwargs.setdefault('nargs', 0)
- super(_NoAction, self).__init__(**kwargs)
-
- def __call__(self, parser, namespace, values, option_string=None):
- pass
-
-def _add_choices_argument(parser, variable_name, help_text, choices: dict):
- parser.register('action', 'none', _NoAction)
- parser.add_argument(variable_name, help=help_text, metavar=variable_name.upper(), choices=choices.keys())
- group = parser.add_argument_group(title=f'Choices for {variable_name.upper()}')
- for choice,choice_help in choices.items():
- group.add_argument(choice, help=choice_help, action='none')
-
-def setup_and_parse_args(args_override=None):
- parent_p = argparse.ArgumentParser(add_help=False)
- parent_p.add_argument('--base-uri', help='The base URL for your Sonarr/Radarr instance, for example `http://localhost:8989`. Required if not doing --preview.')
- parent_p.add_argument('--api-key', help='Your API key. Required if not doing --preview.')
- parent_p.add_argument('--preview', help='Only display the processed markdown results and nothing else.',
- action='store_true', default=False)
- parent_p.add_argument('--debug', help='Display additional logs useful for development/debug purposes',
- action='store_true', default=False)
- parent_p.add_argument('--config', help='The configuration YAML file to use. If not specified, the script will look for `trash.yml` in the same directory as the `trash.py` script.')
-
- parser = argparse.ArgumentParser(description='Automatically mirror TRaSH guides to your Sonarr/Radarr instance.')
- subparsers = parser.add_subparsers(description='Operations specific to different parts of the TRaSH guides', dest='subcommand')
-
- # Subcommands for 'profile'
- profile_p = subparsers.add_parser('profile', help='Pages of the guide that define profiles',
- parents=[parent_p])
- _add_choices_argument(profile_p, 'type', 'The specific guide type/page to pull data from.',
- {type: data.get('cmd_help') for type, data in profile_types.items()})
- profile_p.add_argument('--tags', help='Tags to assign to the profiles that are created or updated. These tags will replace any existing tags when updating profiles.',
- nargs='+')
- profile_p.add_argument('--strict-negative-scores', help='Any negative scores get added to the list of "Must Not Contain" items',
- action='store_true')
-
- # Subcommands for 'quality'
- quality_p = subparsers.add_parser('quality', help='Pages in the guide that provide quality definitions',
- parents=[parent_p])
- _add_choices_argument(quality_p, 'type', 'The specific guide type/page to pull data from.',
- {type: data.get('cmd_help') for type, data in quality_types.items()})
- quality_p.add_argument('--preferred-percentage', help='A percentage value that determines the preferred quality, when needed. Default is 100. Value is interpolated between the min (0%%) and max (100%%) value for each table row.',
- type=int, default=100, metavar='[0-100]')
-
- args = parser.parse_args(args=args_override)
- if not args.subcommand:
- parser.print_help()
- exit(1)
-
- return args
diff --git a/src/app/guide/__init__.py b/src/app/guide/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/app/guide/profile_types.py b/src/app/guide/profile_types.py
deleted file mode 100644
index 6a8edfa6..00000000
--- a/src/app/guide/profile_types.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# This defines general information specific to guide types. Used across different modules as needed.
-types = {
- 'sonarr:anime': {
- 'cmd_help': 'The anime release profile for Sonarr v3',
- 'markdown_doc_name': 'Sonarr-Release-Profile-RegEx-Anime',
- 'profile_typename': 'Anime'
- },
- 'sonarr:web-dl': {
- 'cmd_help': 'The WEB-DL release profile for Sonarr v3',
- 'markdown_doc_name': 'Sonarr-Release-Profile-RegEx',
- 'profile_typename': 'WEB-DL'
- },
-}
diff --git a/src/app/guide/quality_types.py b/src/app/guide/quality_types.py
deleted file mode 100644
index 7b9504ec..00000000
--- a/src/app/guide/quality_types.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# This defines general information specific to quality definition types. Used across different modules as needed.
-types = {
- 'sonarr:anime': {
- 'cmd_help': 'Choose the Sonarr quality definition best fit for anime'
- },
- 'sonarr:non-anime': {
- 'cmd_help': 'Choose the Sonarr quality definition best fit for tv shows (non-anime)'
- },
- 'sonarr:hybrid': {
- 'cmd_help': 'The script will generate a Sonarr quality definition that works best for all show types'
- },
- 'radarr:movies': {
- 'cmd_help': 'Choose the Radarr quality definition used for movies.'
- },
-}
diff --git a/src/app/guide/radarr/quality.py b/src/app/guide/radarr/quality.py
deleted file mode 100644
index 025186d7..00000000
--- a/src/app/guide/radarr/quality.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import requests
-import re
-from collections import defaultdict
-
-header_regex = re.compile(r'^#+')
-table_row_regex = re.compile(r'\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|')
-
-# --------------------------------------------------------------------------------------------------
-def get_markdown():
- markdown_page_url = 'https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Radarr/V3/Radarr-Quality-Settings-File-Size.md'
- response = requests.get(markdown_page_url)
- return response.content.decode('utf8')
-
-# --------------------------------------------------------------------------------------------------
-def parse_markdown(args, logger, markdown_content):
- results = defaultdict(list)
- table = None
-
- # Convert from 0-100 to 0.0-1.0
- preferred_ratio = args.preferred_percentage / 100
-
- for line in markdown_content.splitlines():
- if not line:
- continue
-
- if header_regex.search(line):
- category = args.type
- table = results[category]
- if len(table) > 0:
- table = None
- elif (match := table_row_regex.search(line)) and table is not None:
- quality = match.group(1)
- min = float(match.group(2))
- max = float(match.group(3))
- # TODO: Support reading preferred from table data in the guide
- preferred = round(min + (max-min) * preferred_ratio, 1)
- table.append((quality, min, max, preferred))
-
- return results
diff --git a/src/app/guide/radarr/utils.py b/src/app/guide/radarr/utils.py
deleted file mode 100644
index 9601e48b..00000000
--- a/src/app/guide/radarr/utils.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# --------------------------------------------------------------------------------------------------
-# Filter out false-positive profiles that are empty.
-def filter_profiles(profiles):
- for name in list(profiles.keys()):
- profile = profiles[name]
- if not len(profile.required) and not len(profile.ignored) and not len(profile.preferred):
- del profiles[name]
-
-# --------------------------------------------------------------------------------------------------
-def print_terms_and_scores(profiles):
- for name, profile in profiles.items():
- print(name)
-
- if profile.include_preferred_when_renaming is not None:
- print(' Include Preferred when Renaming?')
- print(' ' + ('CHECKED' if profile.include_preferred_when_renaming else 'NOT CHECKED'))
- print('')
-
- if len(profile.required):
- print(' Must Contain:')
- for term in profile.required:
- print(f' {term}')
- print('')
-
- if len(profile.ignored):
- print(' Must Not Contain:')
- for term in profile.ignored:
- print(f' {term}')
- print('')
-
- if len(profile.preferred):
- print(' Preferred:')
- for score, terms in profile.preferred.items():
- for term in terms:
- print(f' {score:<10} {term}')
-
- print('')
-
-# --------------------------------------------------------------------------------------------------
-def find_existing_profile(profile_name, existing_profiles):
- for p in existing_profiles:
- if p.get('name') == profile_name:
- return p
- return None
-
-# --------------------------------------------------------------------------------------------------
-def quality_preview(definition):
- print('')
- formats = '{:<20} {:<10} {:<10} {:<10}'
- print(formats.format('Quality', 'Min', 'Max', 'Preferred'))
- print(formats.format('-------', '---', '---', '---'))
- for (quality, min, max, preferred) in definition:
- print(formats.format(quality, min, max, preferred))
- print('')
diff --git a/src/app/guide/sonarr/__init__.py b/src/app/guide/sonarr/__init__.py
deleted file mode 100644
index bbb676c0..00000000
--- a/src/app/guide/sonarr/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import profile, quality
\ No newline at end of file
diff --git a/src/app/guide/sonarr/profile.py b/src/app/guide/sonarr/profile.py
deleted file mode 100644
index dbdc351c..00000000
--- a/src/app/guide/sonarr/profile.py
+++ /dev/null
@@ -1,156 +0,0 @@
-import re
-from collections import defaultdict
-from enum import Enum
-import requests
-
-from app.profile_data import ProfileData
-
-TermCategory = Enum('TermCategory', 'Preferred Required Ignored')
-
-header_regex = re.compile(r'^(#+)\s([\w\s\d]+)\s*$')
-score_regex = re.compile(r'score.*?\[(-?[\d]+)\]', re.IGNORECASE)
-header_release_profile_regex = re.compile(r'release profile', re.IGNORECASE)
-category_regex = (
- (TermCategory.Required, re.compile(r'must contain', re.IGNORECASE)),
- (TermCategory.Ignored, re.compile(r'must not contain', re.IGNORECASE)),
- (TermCategory.Preferred, re.compile(r'preferred', re.IGNORECASE)),
-)
-
-class ParserState:
- def __init__(self):
- self.profile_name = None
- self.score = None
- self.current_category = TermCategory.Preferred
- self.bracket_depth = 0
- self.current_header_depth = -1
-
- def reset(self):
- self.__init__()
-
- def is_valid(self):
- return \
- self.profile_name is not None and \
- self.current_category is not None and \
- (self.current_category != TermCategory.Preferred or self.score is not None)
-
-# --------------------------------------------------------------------------------------------------
-def get_markdown(page):
- response = requests.get(f'https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr/V3/{page}.md')
- return response.content.decode('utf8')
-
-# --------------------------------------------------------------------------------------------------
-def parse_category(line):
- for rx in category_regex:
- if rx[1].search(line):
- return rx[0]
-
- return None
-
-# --------------------------------------------------------------------------------------------------
-def parse_markdown_outside_fence(args, logger, line, state, results):
- # Header processing
- if match := header_regex.search(line):
- header_depth = len(match.group(1))
- header_text = match.group(2)
- logger.debug(f'> Parsing Header [Text: {header_text}] [Depth: {header_depth}]')
-
- # Profile name (always reset previous state here)
- if header_release_profile_regex.search(header_text):
- state.reset()
- state.profile_name = header_text
- logger.debug(f' - New Profile [Text: {header_text}]')
- return
-
- elif header_depth <= state.current_header_depth:
- logger.debug(' - !! Non-nested, non-profile header found; resetting all state')
- state.reset()
- return
-
- # Until we find a header that defines a profile, we don't care about anything under it.
- if not state.profile_name:
- return
-
- # Check if we are enabling the "Include Preferred when Renaming" checkbox
- profile = results[state.profile_name]
- lower_line = line.lower()
- if 'include preferred' in lower_line:
- profile.include_preferred_when_renaming = 'not' not in lower_line
- logger.debug(f' - "Include Preferred" found [Value: {profile.include_preferred_when_renaming}] [Line: {line}]')
- return
-
- # Either we have a nested header or normal line at this point
- # We need to check if we're defining a new category.
- if category := parse_category(line):
- state.current_category = category
- logger.debug(f' - Category Set [Name: {category}] [Line: {line}]')
- # DO NOT RETURN HERE!
- # The category and score are sometimes in the same sentence (line); continue processing the line!!
- # return
-
- # Check this line for a score value. We do this even if our category may not be set to 'Preferred' yet.
- if match := score_regex.search(line):
- state.score = int(match.group(1))
- logger.debug(f' - Score [Value: {state.score}]')
- return
-
-# --------------------------------------------------------------------------------------------------
-def parse_markdown_inside_fence(args, logger, line, state, results):
- profile = results[state.profile_name]
-
- if state.current_category == TermCategory.Preferred:
- logger.debug(' + Capture Term '
- f'[Category: {state.current_category}] '
- f'[Score: {state.score}] '
- f'[Strict: {args.strict_negative_scores}] '
- f'[Term: {line}]')
-
- if args.strict_negative_scores and state.score < 0:
- profile.ignored.append(line)
- else:
- profile.preferred[state.score].append(line)
- return
-
- # Sometimes a comma is present at the end of these regexes, because when it's
- # pasted into Sonarr it acts as a delimiter. However, when using them with the
- # API we do not need them.
- line = line.rstrip(',')
-
- if state.current_category == TermCategory.Ignored:
- profile.ignored.append(line)
- logger.debug(f' + Capture Term [Category: {state.current_category}] [Term: {line}]')
- return
-
- if state.current_category == TermCategory.Required:
- profile.required.append(line)
- logger.debug(f' + Capture Term [Category: {state.current_category}] [Term: {line}]')
- return
-
-# --------------------------------------------------------------------------------------------------
-def parse_markdown(args, logger, markdown_content):
- results = defaultdict(ProfileData)
- state = ParserState()
-
- for line in markdown_content.splitlines():
- # Always check if we're starting a fenced code block. Whether we are inside one or not greatly affects
- # the logic we use.
- if line.startswith('```'):
- state.bracket_depth = 1 - state.bracket_depth
- continue
-
- # Not inside brackets
- if state.bracket_depth == 0:
- parse_markdown_outside_fence(args, logger, line, state, results)
- # Inside brackets
- elif state.bracket_depth == 1:
- if not state.is_valid():
- logger.debug(' - !! Inside bracket with invalid state; skipping! '
- f'[Profile Name: {state.profile_name}] '
- f'[Category: {state.current_category}] '
- f'[Score: {state.score}] '
- f'[Line: {line}] '
- )
- else:
- parse_markdown_inside_fence(args, logger, line, state, results)
-
- logger.debug('\n')
- return results
diff --git a/src/app/guide/sonarr/quality.py b/src/app/guide/sonarr/quality.py
deleted file mode 100644
index f8eaaf6e..00000000
--- a/src/app/guide/sonarr/quality.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import requests
-import re
-from collections import defaultdict
-
-header_regex = re.compile(r'^#+')
-table_row_regex = re.compile(r'\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|')
-
-# --------------------------------------------------------------------------------------------------
-def get_markdown():
- trash_anime_markdown_url = 'https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr/V3/Sonarr-Quality-Settings-File-Size.md'
- response = requests.get(trash_anime_markdown_url)
- return response.content.decode('utf8')
-
-# --------------------------------------------------------------------------------------------------
-def parse_markdown(logger, markdown_content):
- results = defaultdict(list)
- table = None
-
- for line in markdown_content.splitlines():
- if not line:
- continue
-
- if header_regex.search(line):
- category = 'sonarr:anime' if 'anime' in line.lower() else 'sonarr:non-anime'
- table = results[category]
- if len(table) > 0:
- table = None
- elif (match := table_row_regex.search(line)) and table is not None:
- table.append((match.group(1), float(match.group(2)), float(match.group(3))))
-
- return results
diff --git a/src/app/guide/sonarr/utils.py b/src/app/guide/sonarr/utils.py
deleted file mode 100644
index 4028e8b2..00000000
--- a/src/app/guide/sonarr/utils.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# --------------------------------------------------------------------------------------------------
-# Filter out false-positive profiles that are empty.
-def filter_profiles(profiles):
- for name in list(profiles.keys()):
- profile = profiles[name]
- if not len(profile.required) and not len(profile.ignored) and not len(profile.preferred):
- del profiles[name]
-
-# --------------------------------------------------------------------------------------------------
-def print_terms_and_scores(profiles):
- for name, profile in profiles.items():
- print(name)
-
- if profile.include_preferred_when_renaming is not None:
- print(' Include Preferred when Renaming?')
- print(' ' + ('CHECKED' if profile.include_preferred_when_renaming else 'NOT CHECKED'))
- print('')
-
- if len(profile.required):
- print(' Must Contain:')
- for term in profile.required:
- print(f' {term}')
- print('')
-
- if len(profile.ignored):
- print(' Must Not Contain:')
- for term in profile.ignored:
- print(f' {term}')
- print('')
-
- if len(profile.preferred):
- print(' Preferred:')
- for score, terms in profile.preferred.items():
- for term in terms:
- print(f' {score:<10} {term}')
-
- print('')
-
-# --------------------------------------------------------------------------------------------------
-def find_existing_profile(profile_name, existing_profiles):
- for p in existing_profiles:
- if p.get('name') == profile_name:
- return p
- return None
-
-# --------------------------------------------------------------------------------------------------
-def quality_preview(definition):
- print('')
- formats = '{:<20} {:<10} {:<10}'
- print(formats.format('Quality', 'Min', 'Max'))
- print(formats.format('-------', '---', '---'))
- for (quality, min, max) in definition:
- print(formats.format(quality, min, max))
- print('')
diff --git a/src/app/logger.py b/src/app/logger.py
deleted file mode 100644
index e6810b8a..00000000
--- a/src/app/logger.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from .orderedenum import OrderedEnum
-
-class Severity(OrderedEnum):
- Info = 1
- Debug = 2
-
-class Logger:
- def __init__(self, args):
- self.severity = Severity.Debug if args.debug else Severity.Info
-
- def info(self, msg):
- print(msg)
-
- def debug(self, msg):
- if self.severity >= Severity.Debug:
- print(msg)
\ No newline at end of file
diff --git a/src/app/logic/__init__.py b/src/app/logic/__init__.py
deleted file mode 100644
index 74a48b17..00000000
--- a/src/app/logic/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import config, main, sonarr, radarr
\ No newline at end of file
diff --git a/src/app/logic/config.py b/src/app/logic/config.py
deleted file mode 100644
index 6162b91a..00000000
--- a/src/app/logic/config.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from pathlib import Path
-import yaml
-
-# --------------------------------------------------------------------------------------------------
-def find_profile_by_name(config, profile_type):
- for profile in config['profile']:
- if profile['type'] == profile_type:
- return profile
- return None
-
-# --------------------------------------------------------------------------------------------------
-def load_config(args, logger, default_load_path: Path):
- if args.config:
- config_path = Path(args.config)
- else:
- # Look for `trash.yml` in the same directory as the main (entrypoint) python script.
- config_path = default_load_path / 'trash.yml'
-
- logger.debug(f'Using configuration file: {config_path}')
-
- if config_path.exists():
- with open(config_path, 'r') as f:
- config_yaml = f.read()
- load_config_string(args, logger, config_yaml)
- else:
- logger.debug('Config file could not be loaded because it does not exist')
-
-# --------------------------------------------------------------------------------------------------
-def _config_has_tags(profile):
- if profile is None or 'tags' not in profile:
- return False;
-
- tags = profile['tags']
- return tags is not None and len(tags) > 0
-
-# --------------------------------------------------------------------------------------------------
-def load_config_string(args, logger, config_yaml):
- config = yaml.load(config_yaml, Loader=yaml.Loader)
- if not config:
- return
-
- server_name, type_name = args.type.split(':')
- server_config = config[server_name]
-
- if not args.base_uri:
- args.base_uri = server_config['base_uri']
-
- if not args.api_key:
- args.api_key = server_config['api_key']
-
- if args.subcommand == 'profile':
- profile = find_profile_by_name(server_config, type_name)
- if _config_has_tags(profile):
- if args.tags is None:
- args.tags = []
- args.tags.extend(t for t in profile['tags'] if t not in args.tags)
diff --git a/src/app/logic/main.py b/src/app/logic/main.py
deleted file mode 100644
index 2611eefc..00000000
--- a/src/app/logic/main.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from pathlib import Path
-
-from app.logic import sonarr, config, radarr
-from app.cmd import setup_and_parse_args
-from app.logger import Logger
-from app.trash_error import TrashError
-
-# --------------------------------------------------------------------------------------------------
-def main(root_directory: Path):
- args = setup_and_parse_args()
- logger = Logger(args)
-
- config.load_config(args, logger, root_directory)
-
- subcommand_handlers = {
- ('sonarr', 'profile'): sonarr.process_profile,
- ('sonarr', 'quality'): sonarr.process_quality,
- ('radarr', 'quality'): radarr.process_quality,
- }
-
- server_name = args.type.split(':')[0]
-
- try:
- subcommand_handlers[server_name, args.subcommand](args, logger)
- except KeyError:
- raise TrashError(f'{args.subcommand} support in {server_name} is not implemented yet')
\ No newline at end of file
diff --git a/src/app/logic/radarr.py b/src/app/logic/radarr.py
deleted file mode 100644
index 70e366b1..00000000
--- a/src/app/logic/radarr.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from app.guide.radarr import quality, utils
-from app.api.radarr import Radarr
-from app.trash_error import TrashError
-
-# --------------------------------------------------------------------------------------------------
-def process_quality(args, logger):
- if 0 > args.preferred_percentage > 100:
- raise TrashError(f'Preferred percentage is out of range: {args.preferred_percentage}')
-
- guide_definitions = quality.parse_markdown(args, logger, quality.get_markdown())
- selected_definition = guide_definitions.get(args.type)
-
- if args.preview:
- utils.quality_preview(selected_definition)
- exit(0)
-
- print(f'Updating quality definition using {args.type}')
- server = Radarr(args, logger)
- definition = server.get_quality_definition()
- server.update_quality_definition(definition, selected_definition)
\ No newline at end of file
diff --git a/src/app/logic/sonarr.py b/src/app/logic/sonarr.py
deleted file mode 100644
index 790f50f2..00000000
--- a/src/app/logic/sonarr.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import re
-
-import app.guide.sonarr as guide
-from app.guide.sonarr import utils
-from app.guide.profile_types import types as profile_types
-from app.api.sonarr import Sonarr
-from app.trash_error import TrashError
-
-# --------------------------------------------------------------------------------------------------
-def process_profile(args, logger):
- page = profile_types.get(args.type).get('markdown_doc_name')
- logger.debug(f'Using markdown page: {page}')
- profiles = guide.profile.parse_markdown(args, logger, guide.profile.get_markdown(page))
-
- # A few false-positive profiles are added sometimes. We filter these out by checking if they
- # actually have meaningful data attached to them, such as preferred terms. If they are mostly empty,
- # we remove them here.
- utils.filter_profiles(profiles)
-
- if args.preview:
- utils.print_terms_and_scores(profiles)
- exit(0)
-
- sonarr = Sonarr(args, logger)
-
- # If tags were provided, ensure they exist. Tags that do not exist are added first, so that we
- # may specify them with the release profile request payload.
- tag_ids = []
- if args.tags:
- tags = sonarr.get_tags()
- tags = sonarr.create_missing_tags(tags, args.tags[:])
- logger.debug(f'Tags JSON: {tags}')
-
- # Get a list of IDs that we can pass along with the request to update/create release
- # profiles
- tag_ids = [t['id'] for t in tags if t['label'] in args.tags]
- logger.debug(f'Tag IDs: {tag_ids}')
-
- # Obtain all of the existing release profiles first. If any were previously created by our script
- # here, we favor replacing those instead of creating new ones, which would just be mostly duplicates
- # (but with some differences, since there have likely been updates since the last run).
- existing_profiles = sonarr.get_release_profiles()
-
- for name, profile in profiles.items():
- type_for_name = profile_types.get(args.type).get('profile_typename')
- new_profile_name = f'[Trash] {type_for_name} - {name}'
- profile_to_update = utils.find_existing_profile(new_profile_name, existing_profiles)
-
- if profile_to_update:
- logger.info(f'Updating existing profile: {new_profile_name}')
- sonarr.update_existing_profile(profile_to_update, profile, tag_ids)
- else:
- logger.info(f'Creating new profile: {new_profile_name}')
- sonarr.create_release_profile(new_profile_name, profile, tag_ids)
-
-# --------------------------------------------------------------------------------------------------
-def process_quality(args, logger):
- guide_definitions = guide.quality.parse_markdown(logger, guide.quality.get_markdown())
-
- if args.type == 'sonarr:hybrid':
- hybrid_quality_regex = re.compile(r'720|1080')
- anime = guide_definitions.get('sonarr:anime')
- nonanime = guide_definitions.get('sonarr:non-anime')
- if len(anime) != len(nonanime):
- raise TrashError('For some reason the anime and non-anime quality definitions are not the same length')
-
- logger.info(
- 'Notice: Hybrid only functions on 720/1080 qualities and uses non-anime values for the rest (e.g. 2160)')
-
- hybrid = []
- for i in range(len(nonanime)):
- left = nonanime[i]
- if not hybrid_quality_regex.search(left[0]):
- logger.debug('Ignored Quality: ' + left[0])
- hybrid.append(left)
- else:
- right = None
- for r in anime:
- if r[0] == left[0]:
- right = r
-
- if right is None:
- raise TrashError(f'Could not find matching anime quality for non-anime quality named: {left[0]}')
-
- hybrid.append((left[0], min(left[1], right[1]), max(left[2], right[2])))
-
- guide_definitions['sonarr:hybrid'] = hybrid
-
- selected_definition = guide_definitions.get(args.type)
-
- if args.preview:
- utils.quality_preview(selected_definition)
- exit(0)
-
- print(f'Updating quality definition using {args.type}')
- sonarr = Sonarr(args, logger)
- definition = sonarr.get_quality_definition()
- sonarr.update_quality_definition(definition, selected_definition)
\ No newline at end of file
diff --git a/src/app/orderedenum.py b/src/app/orderedenum.py
deleted file mode 100644
index 457fd718..00000000
--- a/src/app/orderedenum.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from enum import Enum
-
-
-class OrderedEnum(Enum):
- def __ge__(self, other):
- if self.__class__ is other.__class__:
- return self.value >= other.value
- return NotImplemented
-
- def __gt__(self, other):
- if self.__class__ is other.__class__:
- return self.value > other.value
- return NotImplemented
-
- def __le__(self, other):
- if self.__class__ is other.__class__:
- return self.value <= other.value
- return NotImplemented
-
- def __lt__(self, other):
- if self.__class__ is other.__class__:
- return self.value < other.value
- return NotImplemented
diff --git a/src/app/profile_data.py b/src/app/profile_data.py
deleted file mode 100644
index fd08fe23..00000000
--- a/src/app/profile_data.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from collections import defaultdict
-
-class ProfileData:
- def __init__(self):
- self.preferred = defaultdict(list)
- self.required = []
- self.ignored = []
- # We use 'none' here to represent no explicit mention of the "include preferred" string
- # found in the markdown. We use this to control whether or not the corresponding profile
- # section gets printed in the first place.
- self.include_preferred_when_renaming = None
diff --git a/src/app/trash_error.py b/src/app/trash_error.py
deleted file mode 100644
index b4d18b24..00000000
--- a/src/app/trash_error.py
+++ /dev/null
@@ -1,2 +0,0 @@
-class TrashError(Exception):
- pass
\ No newline at end of file
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/tests/guide/__init__.py b/src/tests/guide/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/tests/guide/sonarr/__init__.py b/src/tests/guide/sonarr/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/tests/guide/sonarr/data/test_parse_markdown_complete_doc.md b/src/tests/guide/sonarr/data/test_parse_markdown_complete_doc.md
deleted file mode 100644
index f5ff2902..00000000
--- a/src/tests/guide/sonarr/data/test_parse_markdown_complete_doc.md
+++ /dev/null
@@ -1,22 +0,0 @@
-### Release Profile 1
-
-The score is [100]
-
-```
-term1
-```
-
-This is another Score that should not be used [200]
-
-#### Must not contain
-
-```
-term2
-term3
-```
-
-#### Must contain
-
-```
-term4
-```
\ No newline at end of file
diff --git a/src/tests/guide/sonarr/data/test_parse_markdown_sonarr_quality_definitions.md b/src/tests/guide/sonarr/data/test_parse_markdown_sonarr_quality_definitions.md
deleted file mode 100644
index 5d184229..00000000
--- a/src/tests/guide/sonarr/data/test_parse_markdown_sonarr_quality_definitions.md
+++ /dev/null
@@ -1,39 +0,0 @@
-## Sonarr Quality Definitions
-
-| Quality | Minimum | Maximum |
-| ------------------ | ------- | ------- |
-| HDTV-720p | 17.9 | 67.5 |
-| HDTV-1080p | 20 | 137.3 |
-| WEBRip-720p | 20 | 137.3 |
-| WEBDL-720p | 20 | 137.3 |
-| Bluray-720p | 34.9 | 137.3 |
-| WEBRip-1080p | 22 | 137.3 |
-| WEBDL-1080p | 22 | 137.3 |
-| Bluray-1080p | 50.4 | 227 |
-| Bluray-1080p Remux | 69.1 | 400 |
-| HDTV-2160p | 84.5 | 350 |
-| WEBRip-2160p | 84.5 | 350 |
-| WEBDL-2160p | 84.5 | 350 |
-| Bluray-2160p | 94.6 | 400 |
-| Bluray-2160p Remux | 204.4 | 400 |
-
-------
-
-### Sonarr Quality Definitions - Anime (Work in Progress)
-
-| Quality | Minimum | Maximum |
-| ------------------ | ------- | ------- |
-| HDTV-720p | 2.3 | 51.4 |
-| HDTV-1080p | 2.3 | 100 |
-| WEBRip-720p | 4.3 | 100 |
-| WEBDL-720p | 4.3 | 51.4 |
-| Bluray-720p | 4.3 | 102.2 |
-| WEBRip-1080p | 4.5 | 257.4 |
-| WEBDL-1080p | 4.3 | 253.6 |
-| Bluray-1080p | 4.3 | 258.1 |
-| Bluray-1080p Remux | 0 | 400 |
-| HDTV-2160p | 84.5 | 350 |
-| WEBRip-2160p | 84.5 | 350 |
-| WEBDL-2160p | 84.5 | 350 |
-| Bluray-2160p | 94.6 | 400 |
-| Bluray-2160p Remux | 204.4 | 400 |
\ No newline at end of file
diff --git a/src/tests/guide/sonarr/test_profile.py b/src/tests/guide/sonarr/test_profile.py
deleted file mode 100644
index 4cdefca4..00000000
--- a/src/tests/guide/sonarr/test_profile.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from app.guide.sonarr import profile
-from pathlib import Path
-from tests.mock_logger import MockLogger
-
-data_files = Path(__file__).parent / 'data'
-
-def test_parse_markdown_complete_doc():
- md_file = data_files / 'test_parse_markdown_complete_doc.md'
- with open(md_file) as file:
- test_markdown = file.read()
-
- class args:
- strict_negative_scores = False
-
- results = profile.parse_markdown(args, MockLogger(), test_markdown)
-
- assert len(results) == 1
- test_profile = next(iter(results.values()))
-
- assert len(test_profile.ignored) == 2
- assert sorted(test_profile.ignored) == sorted(['term2', 'term3'])
-
- assert len(test_profile.required) == 1
- assert test_profile.required == ['term4']
-
- assert len(test_profile.preferred) == 1
- assert test_profile.preferred.get(100) == ['term1']
-
-
-def test_parse_markdown_strict_negative_scores():
- test_markdown = '''
-# Test Release Profile
-
-This score is negative [-1]
-
-```
-abc
-```
-
-This score is positive [0]
-
-```
-xyz
-```
-'''
-
- class args:
- strict_negative_scores = True
-
- results = profile.parse_markdown(args, MockLogger(), test_markdown)
- assert len(results['Test Release Profile'].required) == 0
- assert len(results['Test Release Profile'].ignored) == 1
- assert results['Test Release Profile'].ignored[0] == 'abc'
- assert len(results['Test Release Profile'].preferred) == 1
- assert results['Test Release Profile'].preferred[0] == ['xyz']
diff --git a/src/tests/guide/sonarr/test_quality.py b/src/tests/guide/sonarr/test_quality.py
deleted file mode 100644
index df9f3650..00000000
--- a/src/tests/guide/sonarr/test_quality.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import app.guide.sonarr.quality as quality
-from pathlib import Path
-from tests.mock_logger import MockLogger
-
-data_files = Path(__file__).parent / 'data'
-
-def test_parse_markdown_complete_doc():
- md_file = data_files / 'test_parse_markdown_sonarr_quality_definitions.md'
- with open(md_file) as file:
- test_markdown = file.read()
-
- results = quality.parse_markdown(MockLogger(), test_markdown)
-
- # Dictionary: Key (header name (anime or non-anime)), list (quality definitions table rows)
- assert len(results) == 2
-
- table = results.get('sonarr:anime')
- assert len(table) == 14
- table_expected = [
- ('HDTV-720p', 2.3, 51.4),
- ('HDTV-1080p', 2.3, 100.0),
- ('WEBRip-720p', 4.3, 100.0),
- ('WEBDL-720p', 4.3, 51.4),
- ('Bluray-720p', 4.3, 102.2),
- ('WEBRip-1080p', 4.5, 257.4),
- ('WEBDL-1080p', 4.3, 253.6),
- ('Bluray-1080p', 4.3, 258.1),
- ('Bluray-1080p Remux', 0.0, 400.0),
- ('HDTV-2160p', 84.5, 350.0),
- ('WEBRip-2160p', 84.5, 350.0),
- ('WEBDL-2160p', 84.5, 350.0),
- ('Bluray-2160p', 94.6, 400.0),
- ('Bluray-2160p Remux', 204.4, 400.0)
- ]
- assert sorted(table) == sorted(table_expected)
-
- table = results.get('sonarr:non-anime')
- assert len(table) == 14
- table_expected = [
- ('HDTV-720p', 17.9, 67.5),
- ('HDTV-1080p', 20.0, 137.3),
- ('WEBRip-720p', 20.0, 137.3),
- ('WEBDL-720p', 20.0, 137.3),
- ('Bluray-720p', 34.9, 137.3),
- ('WEBRip-1080p', 22.0, 137.3),
- ('WEBDL-1080p', 22.0, 137.3),
- ('Bluray-1080p', 50.4, 227.0),
- ('Bluray-1080p Remux', 69.1, 400.0),
- ('HDTV-2160p', 84.5, 350.0),
- ('WEBRip-2160p', 84.5, 350.0),
- ('WEBDL-2160p', 84.5, 350.0),
- ('Bluray-2160p', 94.6, 400.0),
- ('Bluray-2160p Remux', 204.4, 400.0)
- ]
- assert sorted(table) == sorted(table_expected)
diff --git a/src/tests/logic/test_config.py b/src/tests/logic/test_config.py
deleted file mode 100644
index b0e5b49e..00000000
--- a/src/tests/logic/test_config.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from inspect import cleandoc
-from pathlib import Path
-
-from app import cmd
-from app.logic import config
-from tests.mock_logger import MockLogger
-
-def test_config_load_from_file_default(mocker):
- mock_open = mocker.patch('app.logic.config.open', mocker.mock_open(read_data=''))
- mocker.patch.object(Path, 'exists', return_value=True)
-
- args = cmd.setup_and_parse_args(['profile', 'sonarr:anime'])
- default_root = Path(__file__).parent
- config.load_config(args, MockLogger(), default_root)
-
- mock_open.assert_called_once_with(default_root / 'trash.yml', 'r')
-
-def test_config_load_from_file_args(mocker):
- mock_open = mocker.patch('app.logic.config.open', mocker.mock_open(read_data=''))
- mocker.patch.object(Path, 'exists', return_value=True)
-
- expected_yml_path = Path(__file__).parent.parent / 'overridden_config.yml'
- args = cmd.setup_and_parse_args(['profile', 'sonarr:anime', '--config', str(expected_yml_path)])
- config.load_config(args, MockLogger(), '.')
-
- mock_open.assert_called_once_with(expected_yml_path, 'r')
-
-def test_config_tags():
- yaml = cleandoc('''
- sonarr:
- base_uri: http://localhost:8989
- api_key: a95cc792074644759fefe3ccab544f6e
- profile:
- - type: anime
- tags:
- - anime
- - type: web-dl
- tags:
- - tv
- - series
- ''')
-
- args = cmd.setup_and_parse_args(['profile', 'sonarr:anime'])
- config.load_config_string(args, MockLogger(), yaml)
- assert args.base_uri == 'http://localhost:8989'
- assert args.api_key == 'a95cc792074644759fefe3ccab544f6e'
- assert args.tags == ['anime']
-
- args = cmd.setup_and_parse_args(['profile', 'sonarr:web-dl'])
- config.load_config_string(args, MockLogger(), yaml)
- assert args.tags == ['tv', 'series']
diff --git a/src/tests/logic/test_main.py b/src/tests/logic/test_main.py
deleted file mode 100644
index 834a868e..00000000
--- a/src/tests/logic/test_main.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import sys
-from pathlib import Path
-
-from app.logic import main
-
-def test_main_sonarr_profile(mocker):
- test_args = ['trash.py', 'profile', 'sonarr:anime']
- mock_processor = mocker.patch('app.logic.sonarr.process_profile')
- mocker.patch.object(sys, 'argv', test_args)
-
- main.main(Path())
-
- mock_processor.assert_called_once()
-
-def test_main_sonarr_quality(mocker):
- test_args = ['trash.py', 'quality', 'sonarr:anime']
- mock_processor = mocker.patch('app.logic.sonarr.process_quality')
- mocker.patch.object(sys, 'argv', test_args)
-
- main.main(Path())
-
- mock_processor.assert_called_once()
\ No newline at end of file
diff --git a/src/tests/logic/test_radarr.py b/src/tests/logic/test_radarr.py
deleted file mode 100644
index a72a94f2..00000000
--- a/src/tests/logic/test_radarr.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import pytest
-
-from app import cmd
-from app.logic import radarr
-from app.trash_error import TrashError
-from tests.mock_logger import MockLogger
-
-@pytest.mark.parametrize('percentage', ['-1', '101'])
-def test_process_quality_bad_preferred_percentage(percentage):
- input_args = ['quality', 'radarr:movies', '--preferred-percentage', percentage]
- args = cmd.setup_and_parse_args(input_args)
- with pytest.raises(TrashError):
- radarr.process_quality(args, MockLogger())
\ No newline at end of file
diff --git a/src/tests/logic/test_sonarr.py b/src/tests/logic/test_sonarr.py
deleted file mode 100644
index 0e886317..00000000
--- a/src/tests/logic/test_sonarr.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import pytest
-
-from app.trash_error import TrashError
-from app.logic import sonarr as logic
-from app import cmd
-from tests.mock_logger import MockLogger
-
-class TestSonarrLogic:
- logger = MockLogger()
-
- @staticmethod
- def test_throw_without_required_arguments():
- with pytest.raises(TrashError):
- args = cmd.setup_and_parse_args(['profile', 'sonarr:anime', '--base-uri', 'value'])
- logic.process_profile(args, TestSonarrLogic.logger)
-
diff --git a/src/tests/mock_logger.py b/src/tests/mock_logger.py
deleted file mode 100644
index d7fcf9b5..00000000
--- a/src/tests/mock_logger.py
+++ /dev/null
@@ -1,3 +0,0 @@
-class MockLogger:
- def info(self, msg): pass
- def debug(self, msg): pass
\ No newline at end of file
diff --git a/src/trash.py b/src/trash.py
deleted file mode 100644
index f9f689a3..00000000
--- a/src/trash.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from pathlib import Path
-
-from app.logic.main import main
-from app.trash_error import TrashError
-
-# --------------------------------------------------------------------------------------------------
-if __name__ == '__main__':
- try:
- main(Path(__file__).parent)
- except TrashError as e:
- print(f'ERROR: {e}')
- exit(1)