wi1-bot 1.2.12__tar.gz → 1.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/.gitignore +3 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/.pre-commit-config.yaml +1 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/PKG-INFO +3 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/README.md +2 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/config.yaml.template +6 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/pyproject.toml +13 -16
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/tests/movie_downloaded.py +5 -5
- wi1-bot-1.3.1/wi1_bot/_version.py +4 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/arr/download.py +3 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/arr/episode.py +4 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/arr/movie.py +4 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/arr/radarr.py +32 -20
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/arr/sonarr.py +53 -21
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/config.py +2 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/discord/bot.py +41 -16
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/discord/cogs/movie.py +8 -3
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/discord/cogs/series.py +42 -15
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/scripts/start.py +4 -4
- wi1-bot-1.3.1/wi1_bot/scripts/transcode_item.py +43 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/transcoder/transcode_queue.py +12 -5
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/transcoder/transcoder.py +87 -69
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/webhook.py +18 -7
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot.egg-info/PKG-INFO +3 -1
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot.egg-info/SOURCES.txt +1 -14
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot.egg-info/entry_points.txt +0 -1
- wi1-bot-1.3.1/wi1_bot.egg-info/requires.txt +18 -0
- wi1-bot-1.2.12/dashboard/.gitignore +0 -23
- wi1-bot-1.2.12/dashboard/package-lock.json +0 -28006
- wi1-bot-1.2.12/dashboard/package.json +0 -43
- wi1-bot-1.2.12/dashboard/public/index.html +0 -16
- wi1-bot-1.2.12/dashboard/src/App.tsx +0 -7
- wi1-bot-1.2.12/dashboard/src/Log.css +0 -10
- wi1-bot-1.2.12/dashboard/src/Log.tsx +0 -49
- wi1-bot-1.2.12/dashboard/src/index.css +0 -47
- wi1-bot-1.2.12/dashboard/src/index.tsx +0 -15
- wi1-bot-1.2.12/dashboard/src/react-app-env.d.ts +0 -1
- wi1-bot-1.2.12/dashboard/tsconfig.json +0 -20
- wi1-bot-1.2.12/wi1_bot/_version.py +0 -5
- wi1-bot-1.2.12/wi1_bot/scripts/add_tag.py +0 -23
- wi1-bot-1.2.12/wi1_bot/scripts/transcode_item.py +0 -29
- wi1-bot-1.2.12/wi1_bot/transcoder/websocket.py +0 -47
- wi1-bot-1.2.12/wi1_bot.egg-info/requires.txt +0 -20
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/.flake8 +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/.github/workflows/pypi-publish.yml +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/LICENSE +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/setup.cfg +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/__init__.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/arr/__init__.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/discord/__init__.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/discord/cogs/__init__.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/discord/helpers.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/push.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/scripts/__init__.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot/transcoder/__init__.py +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot.egg-info/dependency_links.txt +0 -0
- {wi1-bot-1.2.12 → wi1-bot-1.3.1}/wi1_bot.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: wi1-bot
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.1
|
4
4
|
Summary: Discord bot for Radarr/Sonarr integration
|
5
5
|
Home-page: https://github.com/wthueb/wi1-bot
|
6
6
|
Author-email: William Huebner <wilhueb@gmail.com>
|
@@ -57,6 +57,8 @@ Requires Python >=3.10.
|
|
57
57
|
|
58
58
|
### TODO
|
59
59
|
|
60
|
+
- Use `mypy --strict`
|
61
|
+
- Use Discord slash commands instead of normal text commands
|
60
62
|
- Web dashboard for seeing transcode queue, transcode progress, quotas
|
61
63
|
- Enforce quotas
|
62
64
|
- !linktmdb
|
@@ -4,12 +4,16 @@ radarr:
|
|
4
4
|
url: http://localhost:7878
|
5
5
|
# radarr api key (Settings->General->Security->API Key)
|
6
6
|
api_key: XXX
|
7
|
+
# radarr root folder (absolute path)
|
8
|
+
root_folder: /full/path/movies
|
7
9
|
|
8
10
|
sonarr:
|
9
11
|
# sonarr url you use to get to the dashboard
|
10
12
|
url: http://localhost:8989
|
11
13
|
# sonarr api key (Settings->General->Security->API Key)
|
12
14
|
api_key: XXX
|
15
|
+
# sonarr root folder (absolute path)
|
16
|
+
root_folder: /full/path/shows
|
13
17
|
|
14
18
|
discord:
|
15
19
|
# discord bot token
|
@@ -45,6 +49,8 @@ transcoding:
|
|
45
49
|
profiles:
|
46
50
|
# name of profile must match name in radarr/sonarr
|
47
51
|
good:
|
52
|
+
# -map 0 in ffmpeg, optional
|
53
|
+
copy_all_streams: true
|
48
54
|
# -c:v in ffmpeg, optional
|
49
55
|
video_codec: hevc_nvenc
|
50
56
|
# -b:v in ffmpeg, optional
|
@@ -16,28 +16,26 @@ classifiers = [
|
|
16
16
|
dynamic = ["version"]
|
17
17
|
requires-python = ">=3.10"
|
18
18
|
dependencies = [
|
19
|
-
"discord.py==1.
|
19
|
+
"discord.py==2.1.0",
|
20
20
|
"Flask==2.1.2",
|
21
21
|
"mongoengine==0.24.1",
|
22
22
|
"pyarr==3.1.3",
|
23
|
-
"pymongo==4.1.1",
|
24
23
|
"python-pushover==0.4",
|
25
|
-
"PyYAML==6.0"
|
26
|
-
"websockets==10.3"
|
24
|
+
"PyYAML==6.0"
|
27
25
|
]
|
28
26
|
|
29
27
|
[project.optional-dependencies]
|
30
28
|
dev = [
|
31
|
-
"black==22.
|
32
|
-
"
|
33
|
-
"
|
34
|
-
"isort==5.10.1",
|
29
|
+
"black==22.12.0",
|
30
|
+
"flake8==6.0.0",
|
31
|
+
"isort==5.12.0",
|
35
32
|
"mongo-types==0.15.1",
|
36
|
-
"mypy==0.
|
37
|
-
"pre-commit==
|
38
|
-
"requests==2.
|
39
|
-
"types-
|
40
|
-
"types-
|
33
|
+
"mypy==0.991",
|
34
|
+
"pre-commit==3.0.2",
|
35
|
+
"requests==2.28.2",
|
36
|
+
"types-flask==1.1.6",
|
37
|
+
"types-PyYAML==6.0.12.3",
|
38
|
+
"types-requests==2.28.11.8"
|
41
39
|
]
|
42
40
|
|
43
41
|
[project.urls]
|
@@ -46,7 +44,6 @@ Homepage = "https://github.com/wthueb/wi1-bot"
|
|
46
44
|
[project.scripts]
|
47
45
|
wi1-bot = "wi1_bot.scripts.start:main"
|
48
46
|
transcode-item = "wi1_bot.scripts.transcode_item:main"
|
49
|
-
add-tag = "wi1_bot.scripts.add_tag:main"
|
50
47
|
|
51
48
|
[tool.setuptools]
|
52
49
|
packages = ["wi1_bot"]
|
@@ -55,13 +52,13 @@ packages = ["wi1_bot"]
|
|
55
52
|
write_to = "wi1_bot/_version.py"
|
56
53
|
|
57
54
|
[tool.mypy]
|
58
|
-
|
55
|
+
packages = "wi1_bot"
|
56
|
+
strict = true
|
59
57
|
|
60
58
|
[[tool.mypy.overrides]]
|
61
59
|
module = [
|
62
60
|
"pushover",
|
63
61
|
"pyarr",
|
64
|
-
"setuptools_scm"
|
65
62
|
]
|
66
63
|
ignore_missing_imports = true
|
67
64
|
|
@@ -1,13 +1,13 @@
|
|
1
|
+
import pathlib
|
1
2
|
import shutil
|
2
3
|
|
3
4
|
import requests
|
4
5
|
|
5
6
|
from wi1_bot.config import config
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
)
|
8
|
+
path = pathlib.Path("./tests/files/jellyfish-10-mbps-hd-h264.mkv")
|
9
|
+
|
10
|
+
shutil.copy(f"{path}.bak", path)
|
11
11
|
|
12
12
|
header = {
|
13
13
|
"X-Api-Key": config["radarr"]["api_key"],
|
@@ -19,7 +19,7 @@ data = {
|
|
19
19
|
"id": 1, # just to get quality profile
|
20
20
|
"folderPath": "./tests/files",
|
21
21
|
},
|
22
|
-
"movieFile": {"relativePath":
|
22
|
+
"movieFile": {"relativePath": path.name},
|
23
23
|
}
|
24
24
|
|
25
25
|
requests.post("http://localhost:9000/", json=data)
|
@@ -1,9 +1,11 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
1
3
|
from .episode import Episode
|
2
4
|
from .movie import Movie
|
3
5
|
|
4
6
|
|
5
7
|
class Download:
|
6
|
-
def __init__(self, data: dict) -> None:
|
8
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
7
9
|
self.content: Movie | Episode | str
|
8
10
|
|
9
11
|
if "movie" in data:
|
@@ -1,34 +1,37 @@
|
|
1
1
|
from shutil import rmtree
|
2
|
+
from typing import Any, cast
|
2
3
|
|
3
4
|
from pyarr import RadarrAPI
|
4
5
|
|
5
6
|
from .download import Download
|
6
7
|
from .movie import Movie
|
7
8
|
|
9
|
+
json = dict[str, Any]
|
10
|
+
|
8
11
|
|
9
12
|
class Radarr:
|
10
13
|
def __init__(self, url: str, api_key: str) -> None:
|
11
14
|
self._radarr = RadarrAPI(url, api_key)
|
12
15
|
|
13
16
|
def lookup_movie(self, query: str) -> list[Movie]:
|
14
|
-
possible_movies = self._radarr.lookup_movie(query)
|
17
|
+
possible_movies = cast(list[json], self._radarr.lookup_movie(query))
|
15
18
|
|
16
19
|
return [Movie(m) for m in possible_movies]
|
17
20
|
|
18
21
|
def lookup_library(self, query: str) -> list[Movie]:
|
19
|
-
possible_movies = self._radarr.lookup_movie(query)
|
22
|
+
possible_movies = cast(list[json], self._radarr.lookup_movie(query))
|
20
23
|
|
21
24
|
return [Movie(m) for m in possible_movies if "id" in m]
|
22
25
|
|
23
|
-
def
|
26
|
+
def lookup_user_library(self, query: str, user_id: int) -> list[Movie]:
|
24
27
|
try:
|
25
28
|
tag_id = self._get_tag_for_user_id(user_id)
|
26
29
|
except ValueError:
|
27
30
|
return []
|
28
31
|
|
29
|
-
tag_detail = self._radarr.get_tag_detail(tag_id)
|
32
|
+
tag_detail = cast(dict[str, Any], self._radarr.get_tag_detail(tag_id))
|
30
33
|
|
31
|
-
possible_movies = self._radarr.lookup_movie(query)
|
34
|
+
possible_movies = cast(list[json], self._radarr.lookup_movie(query))
|
32
35
|
|
33
36
|
user_movie_ids = tag_detail["movieIds"]
|
34
37
|
|
@@ -42,7 +45,7 @@ class Radarr:
|
|
42
45
|
|
43
46
|
quality_profile_id = self._get_quality_profile_id(profile)
|
44
47
|
|
45
|
-
root_folder = self._radarr.get_root_folder()[0]["path"]
|
48
|
+
root_folder = cast(list[json], self._radarr.get_root_folder())[0]["path"]
|
46
49
|
|
47
50
|
self._radarr.add_movie(
|
48
51
|
db_id=movie.tmdb_id,
|
@@ -53,7 +56,7 @@ class Radarr:
|
|
53
56
|
return True
|
54
57
|
|
55
58
|
def del_movie(self, movie: Movie) -> None:
|
56
|
-
potential = self._radarr.get_movie(movie.tmdb_id)
|
59
|
+
potential = cast(list[json], self._radarr.get_movie(movie.tmdb_id))
|
57
60
|
|
58
61
|
if not potential:
|
59
62
|
raise ValueError(f"{movie} is not in the library")
|
@@ -71,7 +74,7 @@ class Radarr:
|
|
71
74
|
pass
|
72
75
|
|
73
76
|
def movie_downloaded(self, movie: Movie) -> bool:
|
74
|
-
potential = self._radarr.get_movie(movie.tmdb_id)
|
77
|
+
potential = cast(list[json], self._radarr.get_movie(movie.tmdb_id))
|
75
78
|
|
76
79
|
if not potential:
|
77
80
|
return False
|
@@ -88,9 +91,12 @@ class Radarr:
|
|
88
91
|
|
89
92
|
def add_tag(self, movie: Movie | list[Movie], user_id: int) -> bool:
|
90
93
|
if isinstance(movie, Movie):
|
91
|
-
ids = [self._radarr.get_movie(movie.tmdb_id)[0]["id"]]
|
94
|
+
ids = [cast(list[json], self._radarr.get_movie(movie.tmdb_id))[0]["id"]]
|
92
95
|
else:
|
93
|
-
ids = [
|
96
|
+
ids = [
|
97
|
+
cast(list[json], self._radarr.get_movie(m.tmdb_id))[0]["id"]
|
98
|
+
for m in movie
|
99
|
+
]
|
94
100
|
|
95
101
|
try:
|
96
102
|
tag_id = self._get_tag_for_user_id(user_id)
|
@@ -106,7 +112,7 @@ class Radarr:
|
|
106
112
|
return True
|
107
113
|
|
108
114
|
def get_downloads(self) -> list[Download]:
|
109
|
-
queue = self._radarr.get_queue_details()
|
115
|
+
queue = cast(list[json], self._radarr.get_queue_details())
|
110
116
|
|
111
117
|
downloads = [Download(d) for d in queue]
|
112
118
|
|
@@ -120,46 +126,52 @@ class Radarr:
|
|
120
126
|
|
121
127
|
total = 0
|
122
128
|
|
123
|
-
for movie in self._radarr.get_movie():
|
129
|
+
for movie in cast(list[json], self._radarr.get_movie()):
|
124
130
|
if tag_id in movie["tags"]:
|
125
131
|
total += movie["sizeOnDisk"]
|
126
132
|
|
127
133
|
return total
|
128
134
|
|
129
|
-
def get_quality_profile_name(self, profile_id: int):
|
130
|
-
profiles = self._radarr.get_quality_profile()
|
135
|
+
def get_quality_profile_name(self, profile_id: int) -> str:
|
136
|
+
profiles = cast(list[json], self._radarr.get_quality_profile())
|
131
137
|
|
132
138
|
for profile in profiles:
|
133
139
|
if profile["id"] == profile_id:
|
134
|
-
return profile["name"]
|
140
|
+
return cast(str, profile["name"])
|
135
141
|
|
136
142
|
raise ValueError(f"no quality profile with the id {profile_id}")
|
137
143
|
|
138
144
|
def rescan_movie(self, movie_id: int) -> None:
|
139
|
-
self._radarr.post_command("RescanMovie",
|
145
|
+
self._radarr.post_command("RescanMovie", movieId=movie_id)
|
146
|
+
|
147
|
+
def refresh_movie(self, movie_id: int) -> None:
|
148
|
+
self._radarr.post_command("RefreshMovie", movieIds=[movie_id])
|
140
149
|
|
141
150
|
def search_missing(self) -> None:
|
142
151
|
self._radarr.post_command(name="MissingMoviesSearch")
|
143
152
|
|
144
153
|
def _get_quality_profile_id(self, name: str) -> int:
|
145
|
-
profiles = self._radarr.get_quality_profile()
|
154
|
+
profiles = cast(list[json], self._radarr.get_quality_profile())
|
146
155
|
|
147
156
|
for profile in profiles:
|
148
157
|
if profile["name"].lower() == name.lower():
|
149
|
-
return profile["id"]
|
158
|
+
return cast(int, profile["id"])
|
150
159
|
|
151
160
|
raise ValueError(f"no quality profile with the name {name}")
|
152
161
|
|
153
162
|
def _get_tag_for_user_id(self, user_id: int) -> int:
|
154
|
-
tags = self._radarr.get_tag()
|
163
|
+
tags = cast(list[json], self._radarr.get_tag())
|
155
164
|
|
156
165
|
for tag in tags:
|
157
166
|
if str(user_id) in tag["label"]:
|
158
|
-
return tag["id"]
|
167
|
+
return cast(int, tag["id"])
|
159
168
|
|
160
169
|
raise ValueError(f"no tag with the user id {user_id}")
|
161
170
|
|
162
171
|
|
172
|
+
__all__ = ["Movie"]
|
173
|
+
|
174
|
+
|
163
175
|
if __name__ == "__main__":
|
164
176
|
from wi1_bot.config import config
|
165
177
|
|
@@ -1,12 +1,15 @@
|
|
1
1
|
from shutil import rmtree
|
2
|
+
from typing import Any, cast
|
2
3
|
|
3
4
|
from pyarr import SonarrAPI
|
4
5
|
|
5
6
|
from .download import Download
|
6
7
|
|
8
|
+
json = dict[str, Any]
|
9
|
+
|
7
10
|
|
8
11
|
class Series:
|
9
|
-
def __init__(self, series_json: dict) -> None:
|
12
|
+
def __init__(self, series_json: dict[str, Any]) -> None:
|
10
13
|
self.title: str = series_json["title"]
|
11
14
|
self.year: int = series_json["year"]
|
12
15
|
self.tvdb_id: int = series_json["tvdbId"]
|
@@ -32,33 +35,59 @@ class Series:
|
|
32
35
|
return str(self.__dict__)
|
33
36
|
|
34
37
|
|
38
|
+
class SonarrError(Exception):
|
39
|
+
pass
|
40
|
+
|
41
|
+
|
35
42
|
class Sonarr:
|
36
43
|
def __init__(self, url: str, api_key: str) -> None:
|
37
44
|
self._sonarr = SonarrAPI(url, api_key)
|
38
45
|
|
39
46
|
def lookup_series(self, query: str) -> list[Series]:
|
40
|
-
possible_series =
|
47
|
+
possible_series = cast(
|
48
|
+
list[json] | dict[str, Any], self._sonarr.lookup_series(query)
|
49
|
+
)
|
50
|
+
|
51
|
+
if isinstance(possible_series, dict):
|
52
|
+
raise SonarrError(possible_series["message"])
|
41
53
|
|
42
54
|
return [Series(s) for s in possible_series]
|
43
55
|
|
44
56
|
def lookup_library(self, query: str) -> list[Series]:
|
45
|
-
possible_series = self._sonarr.lookup_series(query)
|
57
|
+
possible_series = cast(list[json], self._sonarr.lookup_series(query))
|
46
58
|
|
47
59
|
return [Series(s) for s in possible_series if "id" in s]
|
48
60
|
|
61
|
+
def lookup_user_library(self, query: str, user_id: int) -> list[Series]:
|
62
|
+
try:
|
63
|
+
tag_id = self._get_tag_for_user_id(user_id)
|
64
|
+
except ValueError:
|
65
|
+
return []
|
66
|
+
|
67
|
+
possible_series = cast(list[json], self._sonarr.lookup_series(query))
|
68
|
+
|
69
|
+
# self._sonarr.get_tag_detail is broken/not supported in v1 sonarr API so have
|
70
|
+
# to filter the tmdb lookup (probably slower but saves an API call)
|
71
|
+
user_series = [s for s in possible_series if tag_id in s["tags"]]
|
72
|
+
|
73
|
+
return [Series(s) for s in user_series]
|
74
|
+
|
49
75
|
def add_series(self, series: Series, profile: str = "good") -> bool:
|
50
76
|
if series.db_id is not None:
|
51
77
|
return False
|
52
78
|
|
53
79
|
quality_profile_id = self._get_quality_profile_id(profile)
|
54
80
|
|
55
|
-
root_folder = self._sonarr.get_root_folder()[0]["path"]
|
81
|
+
root_folder = cast(list[json], self._sonarr.get_root_folder())[0]["path"]
|
56
82
|
|
57
|
-
series_json =
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
83
|
+
series_json = cast(
|
84
|
+
json,
|
85
|
+
self._sonarr.add_series(
|
86
|
+
tvdb_id=series.tvdb_id,
|
87
|
+
quality_profile_id=quality_profile_id,
|
88
|
+
root_dir=root_folder,
|
89
|
+
search_for_missing_episodes=True,
|
90
|
+
),
|
62
91
|
)
|
63
92
|
|
64
93
|
series.db_id = series_json["id"]
|
@@ -69,7 +98,7 @@ class Sonarr:
|
|
69
98
|
if series.db_id is None:
|
70
99
|
raise ValueError(f"{series} is not in the library")
|
71
100
|
|
72
|
-
series_json = self._sonarr.get_series(series.db_id)
|
101
|
+
series_json = cast(json, self._sonarr.get_series(series.db_id))
|
73
102
|
|
74
103
|
self._sonarr.del_series(series.db_id, delete_files=True)
|
75
104
|
|
@@ -100,7 +129,7 @@ class Sonarr:
|
|
100
129
|
|
101
130
|
return False
|
102
131
|
|
103
|
-
series_json = self._sonarr.get_series(series.db_id)
|
132
|
+
series_json = cast(json, self._sonarr.get_series(series.db_id))
|
104
133
|
|
105
134
|
series_json["tags"].append(tag_id)
|
106
135
|
|
@@ -109,7 +138,7 @@ class Sonarr:
|
|
109
138
|
return True
|
110
139
|
|
111
140
|
def get_downloads(self) -> list[Download]:
|
112
|
-
queue = self._sonarr.get_queue()
|
141
|
+
queue = cast(list[json], self._sonarr.get_queue())
|
113
142
|
|
114
143
|
downloads = [Download(d) for d in queue]
|
115
144
|
|
@@ -123,18 +152,21 @@ class Sonarr:
|
|
123
152
|
|
124
153
|
total = 0
|
125
154
|
|
126
|
-
for series in self._sonarr.get_series():
|
155
|
+
for series in cast(list[json], self._sonarr.get_series()):
|
127
156
|
if tag_id in series["tags"]:
|
128
|
-
|
157
|
+
try:
|
158
|
+
total += series["sizeOnDisk"]
|
159
|
+
except KeyError:
|
160
|
+
continue
|
129
161
|
|
130
162
|
return total
|
131
163
|
|
132
|
-
def get_quality_profile_name(self, profile_id: int):
|
133
|
-
profiles = self._sonarr.get_quality_profile()
|
164
|
+
def get_quality_profile_name(self, profile_id: int) -> str:
|
165
|
+
profiles = cast(list[json], self._sonarr.get_quality_profile())
|
134
166
|
|
135
167
|
for profile in profiles:
|
136
168
|
if profile["id"] == profile_id:
|
137
|
-
return profile["name"]
|
169
|
+
return cast(str, profile["name"])
|
138
170
|
|
139
171
|
raise ValueError(f"no quality profile with the id {profile_id}")
|
140
172
|
|
@@ -142,19 +174,19 @@ class Sonarr:
|
|
142
174
|
self._sonarr.post_command("RescanSeries", seriesId=series_id)
|
143
175
|
|
144
176
|
def _get_quality_profile_id(self, name: str) -> int:
|
145
|
-
profiles = self._sonarr.get_quality_profile()
|
177
|
+
profiles = cast(list[json], self._sonarr.get_quality_profile())
|
146
178
|
|
147
179
|
for profile in profiles:
|
148
180
|
if profile["name"].lower() == name.lower():
|
149
|
-
return profile["id"]
|
181
|
+
return cast(int, profile["id"])
|
150
182
|
|
151
183
|
raise ValueError(f"no quality profile with the name {name}")
|
152
184
|
|
153
185
|
def _get_tag_for_user_id(self, user_id: int) -> int:
|
154
|
-
tags = self._sonarr.get_tag()
|
186
|
+
tags = cast(list[json], self._sonarr.get_tag())
|
155
187
|
|
156
188
|
for tag in tags:
|
157
189
|
if str(user_id) in tag["label"]:
|
158
|
-
return tag["id"]
|
190
|
+
return cast(int, tag["id"])
|
159
191
|
|
160
192
|
raise ValueError(f"no tag with the user id {user_id}")
|
@@ -26,6 +26,7 @@ if _config_path is None:
|
|
26
26
|
class ArrConfig(TypedDict):
|
27
27
|
url: str
|
28
28
|
api_key: str
|
29
|
+
root_folder: str
|
29
30
|
|
30
31
|
|
31
32
|
class PushoverConfig(TypedDict):
|
@@ -79,6 +80,7 @@ class DiscordConfig(DiscordConfigOptional):
|
|
79
80
|
|
80
81
|
|
81
82
|
class TranscodingProfile(TypedDict, total=False):
|
83
|
+
copy_all_streams: bool
|
82
84
|
video_codec: str
|
83
85
|
video_bitrate: int
|
84
86
|
audio_codec: str
|
@@ -1,4 +1,7 @@
|
|
1
|
+
import asyncio
|
1
2
|
import logging
|
3
|
+
import traceback
|
4
|
+
from typing import Any
|
2
5
|
|
3
6
|
import discord
|
4
7
|
from discord.ext import commands
|
@@ -18,23 +21,32 @@ sonarr = Sonarr(config["sonarr"]["url"], config["sonarr"]["api_key"])
|
|
18
21
|
|
19
22
|
|
20
23
|
@bot.check
|
21
|
-
async def check_channel(ctx: commands.Context) -> bool:
|
24
|
+
async def check_channel(ctx: commands.Context[Any]) -> bool:
|
22
25
|
return ctx.channel.id == config["discord"]["channel_id"]
|
23
26
|
|
24
27
|
|
25
28
|
@bot.event
|
26
|
-
async def on_command_error(
|
29
|
+
async def on_command_error(
|
30
|
+
ctx: commands.Context[Any], error: commands.CommandError
|
31
|
+
) -> None:
|
27
32
|
match error:
|
28
|
-
case commands.CommandNotFound():
|
33
|
+
case commands.CommandNotFound() | commands.CheckFailure():
|
29
34
|
pass
|
30
35
|
case commands.MissingRole():
|
31
36
|
await reply(ctx.message, "you don't have permission to do that")
|
32
37
|
case commands.MemberNotFound():
|
33
38
|
await reply(ctx.message, "that user doesn't exist")
|
39
|
+
case commands.CommandOnCooldown():
|
40
|
+
await reply(ctx.message, str(error))
|
34
41
|
case commands.MissingRequiredArgument():
|
35
42
|
await reply(ctx.message, str(error))
|
36
43
|
case _:
|
37
|
-
logger.error(
|
44
|
+
logger.error(
|
45
|
+
"".join(
|
46
|
+
traceback.format_exception(type(error), error, error.__traceback__)
|
47
|
+
)
|
48
|
+
)
|
49
|
+
|
38
50
|
await reply(
|
39
51
|
ctx.message,
|
40
52
|
f"something went wrong (<@!{config['discord']['admin_id']}>)",
|
@@ -55,15 +67,15 @@ async def on_ready() -> None:
|
|
55
67
|
|
56
68
|
|
57
69
|
@bot.before_invoke
|
58
|
-
async def before_invoke(ctx: commands.Context) -> None:
|
70
|
+
async def before_invoke(ctx: commands.Context[Any]) -> None:
|
59
71
|
logger.debug(f"got command from {ctx.message.author}: {ctx.message.content}")
|
60
72
|
|
61
73
|
|
62
|
-
@commands.cooldown(1, 10) #
|
74
|
+
@commands.cooldown(1, 10) # type: ignore
|
63
75
|
@bot.command(
|
64
76
|
name="downloads", aliases=["queue", "q"], help="see the status of movie downloads"
|
65
77
|
)
|
66
|
-
async def downloads_cmd(ctx: commands.Context) -> None:
|
78
|
+
async def downloads_cmd(ctx: commands.Context[Any]) -> None:
|
67
79
|
async with ctx.typing():
|
68
80
|
queue = radarr.get_downloads() + sonarr.get_downloads()
|
69
81
|
|
@@ -76,9 +88,9 @@ async def downloads_cmd(ctx: commands.Context) -> None:
|
|
76
88
|
await reply(ctx.message, "\n\n".join(map(str, queue)), title="download progress")
|
77
89
|
|
78
90
|
|
79
|
-
@commands.cooldown(1, 60, commands.BucketType.user)
|
91
|
+
@commands.cooldown(1, 60, commands.BucketType.user) # type: ignore
|
80
92
|
@bot.command(name="quota", help="see your used space on the plex")
|
81
|
-
async def quota_cmd(ctx: commands.Context) -> None:
|
93
|
+
async def quota_cmd(ctx: commands.Context[Any]) -> None:
|
82
94
|
async with ctx.typing():
|
83
95
|
used = (
|
84
96
|
radarr.get_quota_amount(ctx.message.author.id)
|
@@ -102,9 +114,9 @@ async def quota_cmd(ctx: commands.Context) -> None:
|
|
102
114
|
await reply(ctx.message, msg)
|
103
115
|
|
104
116
|
|
105
|
-
@commands.cooldown(1, 60)
|
117
|
+
@commands.cooldown(1, 60) # type: ignore
|
106
118
|
@bot.command(name="quotas", help="see everyone's used space on the plex")
|
107
|
-
async def quotas_cmd(ctx: commands.Context) -> None:
|
119
|
+
async def quotas_cmd(ctx: commands.Context[Any]) -> None:
|
108
120
|
try:
|
109
121
|
quotas = config["discord"]["quotas"]
|
110
122
|
except ValueError:
|
@@ -135,17 +147,30 @@ async def quotas_cmd(ctx: commands.Context) -> None:
|
|
135
147
|
)
|
136
148
|
|
137
149
|
|
138
|
-
bot.
|
139
|
-
|
150
|
+
@bot.command(name="addtag", help="add a user tag") # type: ignore
|
151
|
+
@commands.has_role("plex-admin")
|
152
|
+
async def addtag_cmd(
|
153
|
+
ctx: commands.Context[Any], name: str, user: discord.Member
|
154
|
+
) -> None:
|
155
|
+
tag = f"{name}: {user.id}"
|
140
156
|
|
157
|
+
radarr.create_tag(tag)
|
158
|
+
sonarr.create_tag(tag)
|
141
159
|
|
142
|
-
|
160
|
+
await reply(ctx.message, f"tag `{tag}` added for {user.display_name}")
|
161
|
+
|
162
|
+
|
163
|
+
async def run() -> None:
|
143
164
|
logger.debug("starting bot")
|
144
165
|
|
145
|
-
bot
|
166
|
+
async with bot:
|
167
|
+
await bot.add_cog(MovieCog(bot))
|
168
|
+
await bot.add_cog(SeriesCog(bot))
|
169
|
+
|
170
|
+
await bot.start(config["discord"]["bot_token"])
|
146
171
|
|
147
172
|
|
148
173
|
if __name__ == "__main__":
|
149
174
|
logger.addHandler(logging.StreamHandler())
|
150
175
|
|
151
|
-
|
176
|
+
asyncio.run(run())
|