wi1-bot 1.4.10__py3-none-any.whl → 1.4.12__py3-none-any.whl
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/_version.py +2 -2
- wi1_bot/arr/__init__.py +2 -1
- wi1_bot/arr/episode.py +1 -2
- wi1_bot/arr/radarr.py +3 -5
- wi1_bot/arr/util.py +31 -0
- wi1_bot/discord/bot.py +15 -25
- wi1_bot/discord/cogs/movie.py +10 -30
- wi1_bot/discord/cogs/series.py +8 -24
- wi1_bot/discord/helpers.py +1 -3
- wi1_bot/push.py +1 -3
- wi1_bot/scripts/rescan.py +1 -3
- wi1_bot/scripts/start.py +1 -2
- wi1_bot/transcoder/transcoder.py +20 -36
- wi1_bot/webhook.py +10 -49
- {wi1_bot-1.4.10.dist-info → wi1_bot-1.4.12.dist-info}/METADATA +12 -16
- wi1_bot-1.4.12.dist-info/RECORD +32 -0
- {wi1_bot-1.4.10.dist-info → wi1_bot-1.4.12.dist-info}/WHEEL +1 -1
- wi1_bot-1.4.10.dist-info/RECORD +0 -31
- {wi1_bot-1.4.10.dist-info → wi1_bot-1.4.12.dist-info}/LICENSE +0 -0
- {wi1_bot-1.4.10.dist-info → wi1_bot-1.4.12.dist-info}/entry_points.txt +0 -0
- {wi1_bot-1.4.10.dist-info → wi1_bot-1.4.12.dist-info}/top_level.txt +0 -0
wi1_bot/_version.py
CHANGED
wi1_bot/arr/__init__.py
CHANGED
wi1_bot/arr/episode.py
CHANGED
@@ -21,8 +21,7 @@ class Episode:
|
|
21
21
|
self.imdb_id: str = series_imdb_id
|
22
22
|
|
23
23
|
self.full_title: str = (
|
24
|
-
f"{self.series_title} S{self.season_num:02d}E{self.ep_num:02d} -"
|
25
|
-
f" {self.ep_title}"
|
24
|
+
f"{self.series_title} S{self.season_num:02d}E{self.ep_num:02d} - {self.ep_title}"
|
26
25
|
)
|
27
26
|
|
28
27
|
if self.imdb_id:
|
wi1_bot/arr/radarr.py
CHANGED
@@ -33,9 +33,7 @@ class Radarr:
|
|
33
33
|
|
34
34
|
user_movie_ids = tag_detail["movieIds"]
|
35
35
|
|
36
|
-
return [
|
37
|
-
Movie(m) for m in possible_movies if "id" in m and m["id"] in user_movie_ids
|
38
|
-
]
|
36
|
+
return [Movie(m) for m in possible_movies if "id" in m and m["id"] in user_movie_ids]
|
39
37
|
|
40
38
|
def add_movie(self, movie: Movie, profile: str = "good") -> bool:
|
41
39
|
if self._radarr.get_movie(movie.tmdb_id, tmdb=True):
|
@@ -149,10 +147,10 @@ class Radarr:
|
|
149
147
|
raise ValueError(f"no quality profile with the id {profile_id}")
|
150
148
|
|
151
149
|
def rescan_movie(self, movie_id: int) -> None:
|
152
|
-
self._radarr.post_command("RescanMovie", movieId=movie_id)
|
150
|
+
self._radarr.post_command("RescanMovie", movieId=movie_id) # type: ignore
|
153
151
|
|
154
152
|
def refresh_movie(self, movie_id: int) -> None:
|
155
|
-
self._radarr.post_command("RefreshMovie", movieIds=[movie_id])
|
153
|
+
self._radarr.post_command("RefreshMovie", movieIds=[movie_id]) # type: ignore
|
156
154
|
|
157
155
|
def search_missing(self) -> None:
|
158
156
|
self._radarr.post_command(name="MissingMoviesSearch")
|
wi1_bot/arr/util.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
import logging
|
2
|
+
import pathlib
|
3
|
+
|
4
|
+
from wi1_bot.config import RemotePathMapping, config
|
5
|
+
|
6
|
+
|
7
|
+
def replace_remote_paths(path: pathlib.Path) -> pathlib.Path:
|
8
|
+
if "general" not in config or "remote_path_mappings" not in config["general"]:
|
9
|
+
return path
|
10
|
+
|
11
|
+
mappings = config["general"]["remote_path_mappings"]
|
12
|
+
|
13
|
+
most_specific: RemotePathMapping | None = None
|
14
|
+
|
15
|
+
for mapping in mappings:
|
16
|
+
if path.is_relative_to(mapping["remote"]):
|
17
|
+
mapping_len = len(pathlib.Path(mapping["remote"]).parts)
|
18
|
+
most_specific_len = (
|
19
|
+
len(pathlib.Path(most_specific["remote"]).parts) if most_specific is not None else 0
|
20
|
+
)
|
21
|
+
|
22
|
+
if mapping_len > most_specific_len:
|
23
|
+
most_specific = mapping
|
24
|
+
|
25
|
+
if most_specific is not None:
|
26
|
+
remote_path = path
|
27
|
+
path = pathlib.Path(most_specific["local"]) / path.relative_to(most_specific["remote"])
|
28
|
+
|
29
|
+
logging.getLogger(__name__).debug(f"replaced remote path mapping: {remote_path} -> {path}")
|
30
|
+
|
31
|
+
return path
|
wi1_bot/discord/bot.py
CHANGED
@@ -26,9 +26,7 @@ async def check_channel(ctx: commands.Context[Any]) -> bool:
|
|
26
26
|
|
27
27
|
|
28
28
|
@bot.event
|
29
|
-
async def on_command_error(
|
30
|
-
ctx: commands.Context[Any], error: commands.CommandError
|
31
|
-
) -> None:
|
29
|
+
async def on_command_error(ctx: commands.Context[Any], error: commands.CommandError) -> None:
|
32
30
|
match error:
|
33
31
|
case commands.CommandNotFound() | commands.CheckFailure():
|
34
32
|
pass
|
@@ -42,9 +40,7 @@ async def on_command_error(
|
|
42
40
|
await reply(ctx.message, str(error))
|
43
41
|
case _:
|
44
42
|
logger.error(
|
45
|
-
"".join(
|
46
|
-
traceback.format_exception(type(error), error, error.__traceback__)
|
47
|
-
)
|
43
|
+
"".join(traceback.format_exception(type(error), error, error.__traceback__))
|
48
44
|
)
|
49
45
|
|
50
46
|
await reply(
|
@@ -71,10 +67,10 @@ async def before_invoke(ctx: commands.Context[Any]) -> None:
|
|
71
67
|
logger.info(f"got command from {ctx.message.author}: {ctx.message.content}")
|
72
68
|
|
73
69
|
|
74
|
-
@
|
75
|
-
@bot.command(
|
70
|
+
@bot.command( # type: ignore[arg-type]
|
76
71
|
name="downloads", aliases=["queue", "q"], help="see the status of movie downloads"
|
77
72
|
)
|
73
|
+
@commands.cooldown(1, 10)
|
78
74
|
async def downloads_cmd(ctx: commands.Context[Any]) -> None:
|
79
75
|
async with ctx.typing():
|
80
76
|
queue = radarr.get_downloads() + sonarr.get_downloads()
|
@@ -88,8 +84,8 @@ async def downloads_cmd(ctx: commands.Context[Any]) -> None:
|
|
88
84
|
await reply(ctx.message, "\n\n".join(map(str, queue)), title="download progress")
|
89
85
|
|
90
86
|
|
91
|
-
@
|
92
|
-
@
|
87
|
+
@bot.command(name="quota", help="see your used space on the plex") # type: ignore[arg-type]
|
88
|
+
@commands.cooldown(1, 60, commands.BucketType.user)
|
93
89
|
async def quota_cmd(ctx: commands.Context[Any]) -> None:
|
94
90
|
async with ctx.typing():
|
95
91
|
used = (
|
@@ -99,10 +95,8 @@ async def quota_cmd(ctx: commands.Context[Any]) -> None:
|
|
99
95
|
|
100
96
|
maximum: float = 0
|
101
97
|
|
102
|
-
|
98
|
+
if "quotas" in config["discord"]:
|
103
99
|
maximum = config["discord"]["quotas"][ctx.message.author.id]
|
104
|
-
except KeyError:
|
105
|
-
pass
|
106
100
|
|
107
101
|
pct = used / maximum * 100 if maximum != 0 else 100
|
108
102
|
|
@@ -114,15 +108,15 @@ async def quota_cmd(ctx: commands.Context[Any]) -> None:
|
|
114
108
|
await reply(ctx.message, msg)
|
115
109
|
|
116
110
|
|
117
|
-
@
|
118
|
-
@
|
111
|
+
@bot.command(name="quotas", help="see everyone's used space on the plex") # type: ignore[arg-type]
|
112
|
+
@commands.cooldown(1, 60)
|
119
113
|
async def quotas_cmd(ctx: commands.Context[Any]) -> None:
|
120
|
-
|
121
|
-
quotas = config["discord"]["quotas"]
|
122
|
-
except ValueError:
|
114
|
+
if "quotas" not in config["discord"]:
|
123
115
|
await reply(ctx.message, "quotas are not implemented here")
|
124
116
|
return
|
125
117
|
|
118
|
+
quotas = config["discord"]["quotas"]
|
119
|
+
|
126
120
|
if not quotas:
|
127
121
|
await reply(ctx.message, "quotas are not implemented here")
|
128
122
|
|
@@ -130,9 +124,7 @@ async def quotas_cmd(ctx: commands.Context[Any]) -> None:
|
|
130
124
|
msg = []
|
131
125
|
|
132
126
|
for user_id, total in quotas.items():
|
133
|
-
used = (
|
134
|
-
radarr.get_quota_amount(user_id) + sonarr.get_quota_amount(user_id)
|
135
|
-
) / 1024**3
|
127
|
+
used = (radarr.get_quota_amount(user_id) + sonarr.get_quota_amount(user_id)) / 1024**3
|
136
128
|
|
137
129
|
pct = used / total * 100 if total != 0 else 100
|
138
130
|
|
@@ -147,11 +139,9 @@ async def quotas_cmd(ctx: commands.Context[Any]) -> None:
|
|
147
139
|
)
|
148
140
|
|
149
141
|
|
150
|
-
@bot.command(name="addtag", help="add a user tag") # type: ignore
|
142
|
+
@bot.command(name="addtag", help="add a user tag") # type: ignore[arg-type]
|
151
143
|
@commands.has_role("plex-admin")
|
152
|
-
async def addtag_cmd(
|
153
|
-
ctx: commands.Context[Any], name: str, user: discord.Member
|
154
|
-
) -> None:
|
144
|
+
async def addtag_cmd(ctx: commands.Context[Any], name: str, user: discord.Member) -> None:
|
155
145
|
tag = f"{name}: {user.id}"
|
156
146
|
|
157
147
|
radarr.create_tag(tag)
|
wi1_bot/discord/cogs/movie.py
CHANGED
@@ -18,9 +18,7 @@ class MovieCog(commands.Cog):
|
|
18
18
|
self.radarr = Radarr(config["radarr"]["url"], config["radarr"]["api_key"])
|
19
19
|
|
20
20
|
@commands.command(name="addmovie", help="add a movie to the plex")
|
21
|
-
async def addmovie_cmd(
|
22
|
-
self, ctx: commands.Context[Any], *, query: str = ""
|
23
|
-
) -> None:
|
21
|
+
async def addmovie_cmd(self, ctx: commands.Context[Any], *, query: str = "") -> None:
|
24
22
|
if not query:
|
25
23
|
await reply(ctx.message, "usage: !addmovie KEYWORDS...")
|
26
24
|
return
|
@@ -36,9 +34,7 @@ class MovieCog(commands.Cog):
|
|
36
34
|
)
|
37
35
|
return
|
38
36
|
|
39
|
-
resp, to_add = await select_from_list(
|
40
|
-
self.bot, ctx.message, "addmovie", potential
|
41
|
-
)
|
37
|
+
resp, to_add = await select_from_list(self.bot, ctx.message, "addmovie", potential)
|
42
38
|
|
43
39
|
if not to_add:
|
44
40
|
return
|
@@ -48,16 +44,12 @@ class MovieCog(commands.Cog):
|
|
48
44
|
for movie in to_add:
|
49
45
|
if not self.radarr.add_movie(movie):
|
50
46
|
if self.radarr.movie_downloaded(movie):
|
51
|
-
await reply(
|
52
|
-
resp, f"{movie} is already DOWNLOADED on the plex (idiot)"
|
53
|
-
)
|
47
|
+
await reply(resp, f"{movie} is already DOWNLOADED on the plex (idiot)")
|
54
48
|
else:
|
55
49
|
await reply(resp, f"{movie} is already on the plex (idiot)")
|
56
50
|
continue
|
57
51
|
|
58
|
-
self.logger.info(
|
59
|
-
f"{ctx.message.author.name} has added the movie {movie.full_title}"
|
60
|
-
)
|
52
|
+
self.logger.info(f"{ctx.message.author.name} has added the movie {movie.full_title}")
|
61
53
|
|
62
54
|
push.send(
|
63
55
|
f"{ctx.message.author.name} has added the movie {movie.full_title}",
|
@@ -74,18 +66,12 @@ class MovieCog(commands.Cog):
|
|
74
66
|
await asyncio.sleep(10)
|
75
67
|
|
76
68
|
if not self.radarr.add_tag(added, ctx.message.author.id):
|
77
|
-
push.send(
|
78
|
-
f"get {ctx.message.author.name} a tag", title="tag needed", priority=1
|
79
|
-
)
|
69
|
+
push.send(f"get {ctx.message.author.name} a tag", title="tag needed", priority=1)
|
80
70
|
|
81
|
-
await ctx.send(
|
82
|
-
f"hey <@!{config['discord']['admin_id']}> get this guy a tag"
|
83
|
-
)
|
71
|
+
await ctx.send(f"hey <@!{config['discord']['admin_id']}> get this guy a tag")
|
84
72
|
|
85
73
|
@commands.command(name="delmovie", help="delete a movie from the plex")
|
86
|
-
async def delmovie_cmd(
|
87
|
-
self, ctx: commands.Context[Any], *, query: str = ""
|
88
|
-
) -> None:
|
74
|
+
async def delmovie_cmd(self, ctx: commands.Context[Any], *, query: str = "") -> None:
|
89
75
|
if not query:
|
90
76
|
await reply(ctx.message, "usage: !delmovie KEYWORDS...")
|
91
77
|
return
|
@@ -102,9 +88,7 @@ class MovieCog(commands.Cog):
|
|
102
88
|
)
|
103
89
|
return
|
104
90
|
else:
|
105
|
-
potential = self.radarr.lookup_user_library(
|
106
|
-
query, ctx.message.author.id
|
107
|
-
)[:50]
|
91
|
+
potential = self.radarr.lookup_user_library(query, ctx.message.author.id)[:50]
|
108
92
|
|
109
93
|
if not potential:
|
110
94
|
await reply(
|
@@ -114,9 +98,7 @@ class MovieCog(commands.Cog):
|
|
114
98
|
)
|
115
99
|
return
|
116
100
|
|
117
|
-
resp, to_delete = await select_from_list(
|
118
|
-
self.bot, ctx.message, "delmovie", potential
|
119
|
-
)
|
101
|
+
resp, to_delete = await select_from_list(self.bot, ctx.message, "delmovie", potential)
|
120
102
|
|
121
103
|
if not to_delete:
|
122
104
|
return
|
@@ -124,9 +106,7 @@ class MovieCog(commands.Cog):
|
|
124
106
|
for movie in to_delete:
|
125
107
|
self.radarr.del_movie(movie)
|
126
108
|
|
127
|
-
self.logger.info(
|
128
|
-
f"{ctx.message.author.name} has deleted the movie {movie.full_title}"
|
129
|
-
)
|
109
|
+
self.logger.info(f"{ctx.message.author.name} has deleted the movie {movie.full_title}")
|
130
110
|
|
131
111
|
push.send(
|
132
112
|
f"{ctx.message.author.name} has deleted the movie {movie.full_title}",
|
wi1_bot/discord/cogs/series.py
CHANGED
@@ -46,9 +46,7 @@ class SeriesCog(commands.Cog):
|
|
46
46
|
)
|
47
47
|
return
|
48
48
|
|
49
|
-
resp, to_add = await select_from_list(
|
50
|
-
self.bot, ctx.message, "addshow", potential
|
51
|
-
)
|
49
|
+
resp, to_add = await select_from_list(self.bot, ctx.message, "addshow", potential)
|
52
50
|
|
53
51
|
if not to_add:
|
54
52
|
return
|
@@ -58,16 +56,12 @@ class SeriesCog(commands.Cog):
|
|
58
56
|
for series in to_add:
|
59
57
|
if not self.sonarr.add_series(series):
|
60
58
|
if self.sonarr.series_downloaded(series):
|
61
|
-
await reply(
|
62
|
-
resp, f"{series} is already DOWNLOADED on the plex (idiot)"
|
63
|
-
)
|
59
|
+
await reply(resp, f"{series} is already DOWNLOADED on the plex (idiot)")
|
64
60
|
else:
|
65
61
|
await reply(resp, f"{series} is already on the plex (idiot)")
|
66
62
|
continue
|
67
63
|
|
68
|
-
self.logger.info(
|
69
|
-
f"{ctx.message.author.name} has added the show {series.full_title}"
|
70
|
-
)
|
64
|
+
self.logger.info(f"{ctx.message.author.name} has added the show {series.full_title}")
|
71
65
|
|
72
66
|
push.send(
|
73
67
|
f"{ctx.message.author.name} has added the show {series.full_title}",
|
@@ -91,17 +85,13 @@ class SeriesCog(commands.Cog):
|
|
91
85
|
priority=1,
|
92
86
|
)
|
93
87
|
|
94
|
-
await ctx.send(
|
95
|
-
f"hey <@!{config['discord']['admin_id']}> get this guy a tag"
|
96
|
-
)
|
88
|
+
await ctx.send(f"hey <@!{config['discord']['admin_id']}> get this guy a tag")
|
97
89
|
|
98
90
|
return
|
99
91
|
|
100
92
|
@commands.command(name="delshow", help="delete a show from the plex")
|
101
93
|
@commands.has_any_role("plex-admin", "plex-shows")
|
102
|
-
async def delshow_command(
|
103
|
-
self, ctx: commands.Context[Any], *, query: str = ""
|
104
|
-
) -> None:
|
94
|
+
async def delshow_command(self, ctx: commands.Context[Any], *, query: str = "") -> None:
|
105
95
|
if not query:
|
106
96
|
await reply(ctx.message, "usage: !delshow KEYWORDS...")
|
107
97
|
return
|
@@ -118,9 +108,7 @@ class SeriesCog(commands.Cog):
|
|
118
108
|
)
|
119
109
|
return
|
120
110
|
else:
|
121
|
-
potential = self.sonarr.lookup_user_library(
|
122
|
-
query, ctx.message.author.id
|
123
|
-
)[:50]
|
111
|
+
potential = self.sonarr.lookup_user_library(query, ctx.message.author.id)[:50]
|
124
112
|
|
125
113
|
if not potential:
|
126
114
|
await reply(
|
@@ -130,9 +118,7 @@ class SeriesCog(commands.Cog):
|
|
130
118
|
)
|
131
119
|
return
|
132
120
|
|
133
|
-
resp, to_delete = await select_from_list(
|
134
|
-
self.bot, ctx.message, "delshow", potential
|
135
|
-
)
|
121
|
+
resp, to_delete = await select_from_list(self.bot, ctx.message, "delshow", potential)
|
136
122
|
|
137
123
|
if not to_delete:
|
138
124
|
return
|
@@ -140,9 +126,7 @@ class SeriesCog(commands.Cog):
|
|
140
126
|
for series in to_delete:
|
141
127
|
self.sonarr.del_series(series)
|
142
128
|
|
143
|
-
self.logger.info(
|
144
|
-
f"{ctx.message.author.name} has deleted the show {series.full_title}"
|
145
|
-
)
|
129
|
+
self.logger.info(f"{ctx.message.author.name} has deleted the show {series.full_title}")
|
146
130
|
|
147
131
|
push.send(
|
148
132
|
f"{ctx.message.author.name} has deleted the show {series.full_title}",
|
wi1_bot/discord/helpers.py
CHANGED
@@ -13,9 +13,7 @@ async def member_has_role(member: discord.Member | discord.User, role: str) -> b
|
|
13
13
|
return False
|
14
14
|
|
15
15
|
|
16
|
-
async def reply(
|
17
|
-
msg: discord.Message, content: str, title: str = "", error: bool = False
|
18
|
-
) -> None:
|
16
|
+
async def reply(msg: discord.Message, content: str, title: str = "", error: bool = False) -> None:
|
19
17
|
if len(content) > 2048:
|
20
18
|
while len(content) > 2048 - len("\n..."):
|
21
19
|
content = content[: content.rfind("\n")].rstrip()
|
wi1_bot/push.py
CHANGED
@@ -3,9 +3,7 @@ import requests
|
|
3
3
|
from wi1_bot.config import config
|
4
4
|
|
5
5
|
|
6
|
-
def send(
|
7
|
-
msg: str, title: str | None = None, url: str | None = None, priority: int = 0
|
8
|
-
) -> None:
|
6
|
+
def send(msg: str, title: str | None = None, url: str | None = None, priority: int = 0) -> None:
|
9
7
|
if "pushover" not in config:
|
10
8
|
return
|
11
9
|
|
wi1_bot/scripts/rescan.py
CHANGED
@@ -38,9 +38,7 @@ def rescan_sonarr() -> None:
|
|
38
38
|
def main() -> None:
|
39
39
|
parser = argparse.ArgumentParser(description="Rescan all movies/shows")
|
40
40
|
|
41
|
-
parser.add_argument(
|
42
|
-
"service", nargs="?", choices=["radarr", "sonarr"], help="radarr or sonarr"
|
43
|
-
)
|
41
|
+
parser.add_argument("service", nargs="?", choices=["radarr", "sonarr"], help="radarr or sonarr")
|
44
42
|
|
45
43
|
args = parser.parse_args()
|
46
44
|
|
wi1_bot/scripts/start.py
CHANGED
@@ -25,8 +25,7 @@ def main() -> None:
|
|
25
25
|
"detailed": {
|
26
26
|
"class": "logging.Formatter",
|
27
27
|
"format": (
|
28
|
-
"[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d]"
|
29
|
-
" %(message)s"
|
28
|
+
"[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s"
|
30
29
|
),
|
31
30
|
},
|
32
31
|
},
|
wi1_bot/transcoder/transcoder.py
CHANGED
@@ -9,7 +9,7 @@ from time import sleep
|
|
9
9
|
from typing import Any
|
10
10
|
|
11
11
|
from wi1_bot import push
|
12
|
-
from wi1_bot.arr import Radarr, Sonarr
|
12
|
+
from wi1_bot.arr import Radarr, Sonarr, replace_remote_paths
|
13
13
|
from wi1_bot.config import config
|
14
14
|
|
15
15
|
from .transcode_queue import TranscodeItem, queue
|
@@ -51,9 +51,7 @@ class Transcoder:
|
|
51
51
|
if remove:
|
52
52
|
queue.remove(item)
|
53
53
|
except Exception:
|
54
|
-
self.logger.warning(
|
55
|
-
"got exception when trying to transcode", exc_info=True
|
56
|
-
)
|
54
|
+
self.logger.warning("got exception when trying to transcode", exc_info=True)
|
57
55
|
|
58
56
|
sleep(3)
|
59
57
|
|
@@ -108,35 +106,25 @@ class Transcoder:
|
|
108
106
|
try:
|
109
107
|
transcode_to.unlink(missing_ok=True)
|
110
108
|
except Exception:
|
111
|
-
self.logger.debug(
|
112
|
-
f"failed to delete transcoded file: {transcode_to}"
|
113
|
-
)
|
109
|
+
self.logger.debug(f"failed to delete transcoded file: {transcode_to}")
|
114
110
|
|
115
111
|
if (
|
116
112
|
"Error opening input files" in last_output
|
117
113
|
or "No such file or directory" in last_output
|
118
114
|
):
|
119
|
-
self.logger.info(
|
120
|
-
f"file does not exist: {path}, skipping transcoding"
|
121
|
-
)
|
115
|
+
self.logger.info(f"file does not exist: {path}, skipping transcoding")
|
122
116
|
return True
|
123
117
|
|
124
118
|
if "File name too long" in last_output:
|
125
|
-
self.logger.info(
|
126
|
-
f"file name is too long: {path}, skipping transcoding"
|
127
|
-
)
|
119
|
+
self.logger.info(f"file name is too long: {path}, skipping transcoding")
|
128
120
|
return True
|
129
121
|
|
130
122
|
if "received signal 15" in last_output:
|
131
|
-
self.logger.info(
|
132
|
-
f"transcoding interrupted by signal: {path}, will retry"
|
133
|
-
)
|
123
|
+
self.logger.info(f"transcoding interrupted by signal: {path}, will retry")
|
134
124
|
return False
|
135
125
|
|
136
126
|
if "cannot open shared object file" in last_output:
|
137
|
-
self.logger.error(
|
138
|
-
"ffmpeg error: missing shared object file, will retry"
|
139
|
-
)
|
127
|
+
self.logger.error("ffmpeg error: missing shared object file, will retry")
|
140
128
|
push.send(f"ffmpeg error: {last_output}", title="ffmpeg error")
|
141
129
|
return False
|
142
130
|
|
@@ -161,9 +149,7 @@ class Transcoder:
|
|
161
149
|
return True
|
162
150
|
|
163
151
|
if not path.exists():
|
164
|
-
self.logger.debug(
|
165
|
-
f"file doesn't exist: {item.path}, deleting transcoded file"
|
166
|
-
)
|
152
|
+
self.logger.debug(f"file doesn't exist: {item.path}, deleting transcoded file")
|
167
153
|
|
168
154
|
transcode_to.unlink(missing_ok=True)
|
169
155
|
return True
|
@@ -172,24 +158,26 @@ class Transcoder:
|
|
172
158
|
shutil.move(transcode_to, new_path)
|
173
159
|
path.unlink()
|
174
160
|
|
175
|
-
self._rescan_content(item,
|
161
|
+
self._rescan_content(item, new_path)
|
176
162
|
|
177
163
|
self.logger.info(f"transcoded: {path.name} -> {new_path.name}")
|
178
164
|
# push.send(f"{path.name} -> {new_path.name}", title="file transcoded")
|
179
165
|
|
180
166
|
return True
|
181
167
|
|
182
|
-
def _rescan_content(self, item: TranscodeItem, new_path:
|
168
|
+
def _rescan_content(self, item: TranscodeItem, new_path: pathlib.Path) -> None:
|
183
169
|
if item.content_id is not None:
|
184
|
-
if new_path.
|
170
|
+
if new_path.is_relative_to(
|
171
|
+
replace_remote_paths(pathlib.Path(config["radarr"]["root_folder"]))
|
172
|
+
):
|
173
|
+
# have to rescan the movie twice: Radarr/Radarr#7668
|
174
|
+
# TODO: create function that waits for comand to finish
|
185
175
|
self.radarr.rescan_movie(item.content_id)
|
186
|
-
# radarr bug that it doesn't see the deleted file and the new file
|
187
|
-
# in one rescan?
|
188
|
-
# have to sleep in between to ensure initial command finishes
|
189
|
-
# or use pyarr.get_command() to see command status
|
190
176
|
sleep(5)
|
191
177
|
self.radarr.rescan_movie(item.content_id)
|
192
|
-
elif new_path.
|
178
|
+
elif new_path.is_relative_to(
|
179
|
+
replace_remote_paths(pathlib.Path(config["sonarr"]["root_folder"]))
|
180
|
+
):
|
193
181
|
self.sonarr.rescan_series(item.content_id)
|
194
182
|
|
195
183
|
def _get_duration(self, path: str) -> timedelta:
|
@@ -212,20 +200,16 @@ class Transcoder:
|
|
212
200
|
|
213
201
|
return duration
|
214
202
|
|
215
|
-
def _build_ffmpeg_command(
|
216
|
-
self, item: TranscodeItem, transcode_to: pathlib.Path
|
217
|
-
) -> list[str]:
|
203
|
+
def _build_ffmpeg_command(self, item: TranscodeItem, transcode_to: pathlib.Path) -> list[str]:
|
218
204
|
command: list[Any] = [
|
219
205
|
"ffmpeg",
|
220
206
|
"-hide_banner",
|
221
207
|
"-y",
|
222
208
|
]
|
223
209
|
|
224
|
-
|
210
|
+
if "transcoding" in config and "hwaccel" in config["transcoding"]:
|
225
211
|
command.extend(["-hwaccel", config["transcoding"]["hwaccel"]])
|
226
212
|
command.extend(["-hwaccel_output_format", config["transcoding"]["hwaccel"]])
|
227
|
-
except KeyError:
|
228
|
-
pass
|
229
213
|
|
230
214
|
command.extend(["-probesize", "100M"])
|
231
215
|
command.extend(["-analyzeduration", "250M"])
|
wi1_bot/webhook.py
CHANGED
@@ -7,8 +7,8 @@ from typing import Any
|
|
7
7
|
from flask import Flask, request
|
8
8
|
|
9
9
|
from wi1_bot import push, transcoder
|
10
|
-
from wi1_bot.arr import Radarr, Sonarr
|
11
|
-
from wi1_bot.config import
|
10
|
+
from wi1_bot.arr import Radarr, Sonarr, replace_remote_paths
|
11
|
+
from wi1_bot.config import config
|
12
12
|
|
13
13
|
app = Flask(__name__)
|
14
14
|
|
@@ -21,40 +21,7 @@ sonarr = Sonarr(config["sonarr"]["url"], config["sonarr"]["api_key"])
|
|
21
21
|
|
22
22
|
|
23
23
|
def on_grab(req: dict[str, Any]) -> None:
|
24
|
-
push.send(
|
25
|
-
req["release"]["releaseTitle"], title=f"file grabbed ({req['downloadClient']})"
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
|
-
def replace_remote_paths(path: pathlib.Path) -> pathlib.Path:
|
30
|
-
if "general" not in config or "remote_path_mappings" not in config["general"]:
|
31
|
-
return path
|
32
|
-
|
33
|
-
mappings = config["general"]["remote_path_mappings"]
|
34
|
-
|
35
|
-
most_specific: RemotePathMapping | None = None
|
36
|
-
|
37
|
-
for mapping in mappings:
|
38
|
-
if path.is_relative_to(mapping["remote"]):
|
39
|
-
mapping_len = len(pathlib.Path(mapping["remote"]).parts)
|
40
|
-
most_specific_len = (
|
41
|
-
len(pathlib.Path(most_specific["remote"]).parts)
|
42
|
-
if most_specific is not None
|
43
|
-
else 0
|
44
|
-
)
|
45
|
-
|
46
|
-
if mapping_len > most_specific_len:
|
47
|
-
most_specific = mapping
|
48
|
-
|
49
|
-
if most_specific is not None:
|
50
|
-
remote_path = path
|
51
|
-
path = pathlib.Path(most_specific["local"]) / path.relative_to(
|
52
|
-
most_specific["remote"]
|
53
|
-
)
|
54
|
-
|
55
|
-
logger.debug(f"replaced remote path mapping: {remote_path} -> {path}")
|
56
|
-
|
57
|
-
return path
|
24
|
+
push.send(req["release"]["releaseTitle"], title=f"file grabbed ({req['downloadClient']})")
|
58
25
|
|
59
26
|
|
60
27
|
def on_download(req: dict[str, Any]) -> None:
|
@@ -66,9 +33,7 @@ def on_download(req: dict[str, Any]) -> None:
|
|
66
33
|
movie_json = radarr._radarr.get_movie(content_id)
|
67
34
|
assert isinstance(movie_json, dict)
|
68
35
|
|
69
|
-
quality_profile = radarr.get_quality_profile_name(
|
70
|
-
movie_json["qualityProfileId"]
|
71
|
-
)
|
36
|
+
quality_profile = radarr.get_quality_profile_name(movie_json["qualityProfileId"])
|
72
37
|
|
73
38
|
movie_folder = req["movie"]["folderPath"]
|
74
39
|
relative_path = req["movieFile"]["relativePath"]
|
@@ -82,9 +47,7 @@ def on_download(req: dict[str, Any]) -> None:
|
|
82
47
|
series_json = sonarr._sonarr.get_series(content_id)
|
83
48
|
assert isinstance(series_json, dict)
|
84
49
|
|
85
|
-
quality_profile = sonarr.get_quality_profile_name(
|
86
|
-
series_json["qualityProfileId"]
|
87
|
-
)
|
50
|
+
quality_profile = sonarr.get_quality_profile_name(series_json["qualityProfileId"])
|
88
51
|
|
89
52
|
series_folder = req["series"]["path"]
|
90
53
|
relative_path = req["episodeFile"]["relativePath"]
|
@@ -96,12 +59,12 @@ def on_download(req: dict[str, Any]) -> None:
|
|
96
59
|
else:
|
97
60
|
raise ValueError("unknown download request")
|
98
61
|
|
62
|
+
if "transcoding" not in config:
|
63
|
+
return
|
64
|
+
|
99
65
|
path = replace_remote_paths(path)
|
100
66
|
|
101
|
-
|
102
|
-
quality_options = config["transcoding"]["profiles"][quality_profile]
|
103
|
-
except KeyError:
|
104
|
-
return
|
67
|
+
quality_options = config["transcoding"]["profiles"][quality_profile]
|
105
68
|
|
106
69
|
# python 3.11: TranscodingProfile and typing.LiteralString
|
107
70
|
def get_key(d: Any, k: str) -> Any:
|
@@ -142,9 +105,7 @@ def index() -> Any:
|
|
142
105
|
if request.json["eventType"] == "Download":
|
143
106
|
on_download(request.json)
|
144
107
|
except Exception:
|
145
|
-
logger.warning(
|
146
|
-
f"error handling request: {request.data.decode()}", exc_info=True
|
147
|
-
)
|
108
|
+
logger.warning(f"error handling request: {request.data.decode()}", exc_info=True)
|
148
109
|
|
149
110
|
return "", 200
|
150
111
|
|
@@ -1,8 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: wi1-bot
|
3
|
-
Version: 1.4.
|
3
|
+
Version: 1.4.12
|
4
4
|
Summary: Discord bot for Radarr/Sonarr integration
|
5
|
-
Home-page: https://github.com/wthueb/wi1-bot
|
6
5
|
Author-email: William Huebner <wilhueb@gmail.com>
|
7
6
|
License: MIT License
|
8
7
|
|
@@ -27,25 +26,24 @@ License: MIT License
|
|
27
26
|
SOFTWARE.
|
28
27
|
|
29
28
|
Project-URL: Homepage, https://github.com/wthueb/wi1-bot
|
30
|
-
Platform: UNKNOWN
|
31
29
|
Classifier: Programming Language :: Python :: 3
|
32
30
|
Classifier: Programming Language :: Python :: 3 :: Only
|
33
31
|
Classifier: Programming Language :: Python :: 3.10
|
34
32
|
Requires-Python: >=3.10
|
35
33
|
Description-Content-Type: text/markdown
|
36
34
|
License-File: LICENSE
|
37
|
-
Requires-Dist: discord.py
|
38
|
-
Requires-Dist: Flask
|
39
|
-
Requires-Dist: mongoengine
|
40
|
-
Requires-Dist: pyarr
|
41
|
-
Requires-Dist: PyYAML
|
42
|
-
Requires-Dist: requests
|
35
|
+
Requires-Dist: discord.py==2.3.2
|
36
|
+
Requires-Dist: Flask==3.0.2
|
37
|
+
Requires-Dist: mongoengine==0.29.1
|
38
|
+
Requires-Dist: pyarr==5.2.0
|
39
|
+
Requires-Dist: PyYAML==6.0.1
|
40
|
+
Requires-Dist: requests==2.31.0
|
43
41
|
Provides-Extra: dev
|
44
|
-
Requires-Dist: mongo-types
|
45
|
-
Requires-Dist: mypy
|
46
|
-
Requires-Dist: pre-commit
|
47
|
-
Requires-Dist: ruff
|
48
|
-
Requires-Dist: types-PyYAML
|
42
|
+
Requires-Dist: mongo-types==0.15.1; extra == "dev"
|
43
|
+
Requires-Dist: mypy==1.3.0; extra == "dev"
|
44
|
+
Requires-Dist: pre-commit==3.6.2; extra == "dev"
|
45
|
+
Requires-Dist: ruff==0.3.0; extra == "dev"
|
46
|
+
Requires-Dist: types-PyYAML==6.0.12.12; extra == "dev"
|
49
47
|
|
50
48
|
# wi1-bot
|
51
49
|
|
@@ -103,5 +101,3 @@ Requires Python >=3.10.
|
|
103
101
|
- Tautulli API (get_history) to show who has already seen the movie
|
104
102
|
- User leaderboard
|
105
103
|
- movies/shows added, Tautulli watch counts
|
106
|
-
|
107
|
-
|
@@ -0,0 +1,32 @@
|
|
1
|
+
wi1_bot/__init__.py,sha256=11ozJKiUsqDCZ3_mcAHhGYUyGK_Unl54djVSBBExFB4,59
|
2
|
+
wi1_bot/_version.py,sha256=Zv4TpC5V6FOmlNNowuZivqOrr2E0lJ1Gpdzj7o5gDYw,413
|
3
|
+
wi1_bot/config.py,sha256=AtzOXvCeat_lb-w_Wy37XyWIpV3LmKh38OGigluJ2nM,3139
|
4
|
+
wi1_bot/push.py,sha256=6EwQ1eXcJnQimAi0dOm1_t7Am056zob-1-EOkytGZWg,783
|
5
|
+
wi1_bot/webhook.py,sha256=11rzmxhXnDeXWlgEnOLh26WP79rLeG71HaDvKUXuUcA,3665
|
6
|
+
wi1_bot/arr/__init__.py,sha256=-6UE81yY6OhD6-nbLindChE_HlwsNRuL8JijUmo_Q6k,149
|
7
|
+
wi1_bot/arr/download.py,sha256=02AYFglnFdWSG8xj_TaJc6l2wjybyhUW6F97CnoyUFw,1381
|
8
|
+
wi1_bot/arr/episode.py,sha256=0K_auUYwkBBd_Ludc4-4z7lSW8HRQ3mchFQ7FR10JSU,1041
|
9
|
+
wi1_bot/arr/movie.py,sha256=9cpJZcoW3r4op6X8Fkr4Cv0BsiWfqKD8MkBfEpTcT8k,741
|
10
|
+
wi1_bot/arr/radarr.py,sha256=jxgNuCZY7aGMWK3nTLt8KLntiYcLaAF7g9dJoThsCvc,5721
|
11
|
+
wi1_bot/arr/sonarr.py,sha256=OouuorRHBMFAOy5oDeJZ5H6WZ-95-N01EHtpOTVMTCo,5970
|
12
|
+
wi1_bot/arr/util.py,sha256=y4_Dotm-pSJJT8xvPWy7ZoGMGVI-YOU4zK2LNTSoTB0,1025
|
13
|
+
wi1_bot/discord/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
+
wi1_bot/discord/bot.py,sha256=KcluVIF1erA3jv5fqE9sLxZaNi_4Zrg9Mgx7OACNypM,5034
|
15
|
+
wi1_bot/discord/helpers.py,sha256=7Qr5Kr8H2CQoLiGG7Oqh388jqUCMJH0EBvF8fC-IbJo,2300
|
16
|
+
wi1_bot/discord/cogs/__init__.py,sha256=9nA47jEyuGG7s1UJYz5SJASJcs0Xa3M1rNz9BiyCico,95
|
17
|
+
wi1_bot/discord/cogs/movie.py,sha256=Gr8xbsdNDlKkOb7boiTE5n3YHz7FR0ajSRANV8EM3XY,3950
|
18
|
+
wi1_bot/discord/cogs/series.py,sha256=bPA3LtuK78qkYvyN38ZdNQ_6I3j4qVN9L5JHV8KX_GA,4597
|
19
|
+
wi1_bot/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
wi1_bot/scripts/add_tag.py,sha256=mWwo8egk2Y5XRiQCpfkA11-3rcxZoD0JOJKxV0LguLk,586
|
21
|
+
wi1_bot/scripts/rescan.py,sha256=97oGK1Gr3jBjfX-Eil4C0hDIwtpZ34E_rwTYCI8Z7r4,1474
|
22
|
+
wi1_bot/scripts/start.py,sha256=TyT29yZcWyQ4YqZeISYE8JB6fXv_y7siGdBX353GqqA,2454
|
23
|
+
wi1_bot/scripts/transcode_item.py,sha256=21MeeIZ9wIRhAY-FOEgOdPsg6YOLlSUj59r8NkTfixw,1155
|
24
|
+
wi1_bot/transcoder/__init__.py,sha256=B4xr82UtIFc3tyy_MEZdZKMukYW0yejPnfsGowaTIM0,105
|
25
|
+
wi1_bot/transcoder/transcode_queue.py,sha256=W7r2I7OD8QuxseCEb7_ddOvYXiBBLmKLvCs2IgJJ92I,1856
|
26
|
+
wi1_bot/transcoder/transcoder.py,sha256=2wIgDPkrrXcrSjA1HpYGd0RX5wonw1zfltJqGzCaorM,9163
|
27
|
+
wi1_bot-1.4.12.dist-info/LICENSE,sha256=6V4_mQoPoLJl77_WMsQRQMDG2mnIhDUdfCmMkqM9Qwc,1072
|
28
|
+
wi1_bot-1.4.12.dist-info/METADATA,sha256=twF68ewC8o9PcBMCUInNwSGz3SBnJwU9bHXWMBDHcWU,4750
|
29
|
+
wi1_bot-1.4.12.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
30
|
+
wi1_bot-1.4.12.dist-info/entry_points.txt,sha256=pF5EawAQsUNMHs5exynpNdRq9g4dODg-RaPkx2MyYLU,184
|
31
|
+
wi1_bot-1.4.12.dist-info/top_level.txt,sha256=Q7mTnPLk80Td82YbjlBMO5tvXJoTFHhheHdmwc-c-7I,8
|
32
|
+
wi1_bot-1.4.12.dist-info/RECORD,,
|
wi1_bot-1.4.10.dist-info/RECORD
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
wi1_bot/__init__.py,sha256=11ozJKiUsqDCZ3_mcAHhGYUyGK_Unl54djVSBBExFB4,59
|
2
|
-
wi1_bot/_version.py,sha256=PSfQzSIY-TCmDuz8UpHfMXvNhO-iyRNDmP7WVYUSfx8,413
|
3
|
-
wi1_bot/config.py,sha256=AtzOXvCeat_lb-w_Wy37XyWIpV3LmKh38OGigluJ2nM,3139
|
4
|
-
wi1_bot/push.py,sha256=-Az8c21R3IZAkJVRQmWsbNXMfvBzzpc9pFTWsDy1mJE,789
|
5
|
-
wi1_bot/webhook.py,sha256=cVp6USg1vbfL05TPQSDIUUYPHWDjuPGb1IGsqImao6s,4713
|
6
|
-
wi1_bot/arr/__init__.py,sha256=ZIgkW24GBdS4sJmaiEIhueHOB6s2L8y2s9ahgp1USRI,86
|
7
|
-
wi1_bot/arr/download.py,sha256=02AYFglnFdWSG8xj_TaJc6l2wjybyhUW6F97CnoyUFw,1381
|
8
|
-
wi1_bot/arr/episode.py,sha256=j25ljy9hyTXFAEBeUQ4iozKP2YXpZyzauphaXa2oqh8,1057
|
9
|
-
wi1_bot/arr/movie.py,sha256=9cpJZcoW3r4op6X8Fkr4Cv0BsiWfqKD8MkBfEpTcT8k,741
|
10
|
-
wi1_bot/arr/radarr.py,sha256=sZ2zVhSDir3T3THW2vvIgWPdsvoEsYZLualeY8UCIr4,5711
|
11
|
-
wi1_bot/arr/sonarr.py,sha256=OouuorRHBMFAOy5oDeJZ5H6WZ-95-N01EHtpOTVMTCo,5970
|
12
|
-
wi1_bot/discord/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
-
wi1_bot/discord/bot.py,sha256=puBlprcGqoC-m98DDZuM7Q7zhfTHSsqJIw2ptNrE4_M,5080
|
14
|
-
wi1_bot/discord/helpers.py,sha256=X8ym8CTmuN0TKXxWEmvXMOKVOLK7Fe-Fj6iiaDpWhT8,2306
|
15
|
-
wi1_bot/discord/cogs/__init__.py,sha256=9nA47jEyuGG7s1UJYz5SJASJcs0Xa3M1rNz9BiyCico,95
|
16
|
-
wi1_bot/discord/cogs/movie.py,sha256=J-1wahJLdIk9WhgQlva4zp9AK__2Vw1M8JoPvhcwzbw,4226
|
17
|
-
wi1_bot/discord/cogs/series.py,sha256=AfUfWuU-vUlID-gW7GWu9w-GiwWzZ4Cxm49_FjgvJik,4837
|
18
|
-
wi1_bot/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
-
wi1_bot/scripts/add_tag.py,sha256=mWwo8egk2Y5XRiQCpfkA11-3rcxZoD0JOJKxV0LguLk,586
|
20
|
-
wi1_bot/scripts/rescan.py,sha256=2V7a3GSP8owLxdJ0J6nG98M-PqPWfRRhfJuDOgWQgnU,1488
|
21
|
-
wi1_bot/scripts/start.py,sha256=vNa_iHkx10D5YWonyRW0f5nG8uE3_JtwJ-XZ-c0hWCs,2477
|
22
|
-
wi1_bot/scripts/transcode_item.py,sha256=21MeeIZ9wIRhAY-FOEgOdPsg6YOLlSUj59r8NkTfixw,1155
|
23
|
-
wi1_bot/transcoder/__init__.py,sha256=B4xr82UtIFc3tyy_MEZdZKMukYW0yejPnfsGowaTIM0,105
|
24
|
-
wi1_bot/transcoder/transcode_queue.py,sha256=W7r2I7OD8QuxseCEb7_ddOvYXiBBLmKLvCs2IgJJ92I,1856
|
25
|
-
wi1_bot/transcoder/transcoder.py,sha256=h1If_1FZz7aVvhpZ7G5tLwR88BojrO1EUuDy3lDdVb0,9409
|
26
|
-
wi1_bot-1.4.10.dist-info/LICENSE,sha256=6V4_mQoPoLJl77_WMsQRQMDG2mnIhDUdfCmMkqM9Qwc,1072
|
27
|
-
wi1_bot-1.4.10.dist-info/METADATA,sha256=wVcpUomwJ605vkSs5gUKWddBkIWwI7RbfKG2JNWZd5k,4831
|
28
|
-
wi1_bot-1.4.10.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
29
|
-
wi1_bot-1.4.10.dist-info/entry_points.txt,sha256=pF5EawAQsUNMHs5exynpNdRq9g4dODg-RaPkx2MyYLU,184
|
30
|
-
wi1_bot-1.4.10.dist-info/top_level.txt,sha256=Q7mTnPLk80Td82YbjlBMO5tvXJoTFHhheHdmwc-c-7I,8
|
31
|
-
wi1_bot-1.4.10.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|