wi1-bot 1.4.9__py3-none-any.whl → 1.4.11__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 CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.4.9'
16
- __version_tuple__ = version_tuple = (1, 4, 9)
15
+ __version__ = version = '1.4.11'
16
+ __version_tuple__ = version_tuple = (1, 4, 11)
wi1_bot/arr/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from .radarr import Radarr
2
2
  from .sonarr import Sonarr
3
+ from .util import replace_remote_paths
3
4
 
4
- __all__ = ["Radarr", "Sonarr"]
5
+ __all__ = ["Radarr", "Sonarr", "replace_remote_paths"]
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):
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
- @commands.cooldown(1, 10) # type: ignore
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
- @commands.cooldown(1, 60, commands.BucketType.user) # type: ignore
92
- @bot.command(name="quota", help="see your used space on the plex")
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
- try:
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
- @commands.cooldown(1, 60) # type: ignore
118
- @bot.command(name="quotas", help="see everyone's used space on the plex")
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
- try:
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)
@@ -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}",
@@ -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}",
@@ -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
  },
@@ -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
 
@@ -96,6 +94,7 @@ class Transcoder:
96
94
  tmp_log_path = tmp_folder / "wi1_bot.transcoder.log"
97
95
 
98
96
  with open(tmp_log_path, "w") as ffmpeg_log_file:
97
+ ffmpeg_log_file.write(f"ffmpeg command: {shlex.join(command)}\n")
99
98
  assert proc.stdout is not None
100
99
  for line in proc.stdout:
101
100
  ffmpeg_log_file.write(line)
@@ -107,35 +106,25 @@ class Transcoder:
107
106
  try:
108
107
  transcode_to.unlink(missing_ok=True)
109
108
  except Exception:
110
- self.logger.debug(
111
- f"failed to delete transcoded file: {transcode_to}"
112
- )
109
+ self.logger.debug(f"failed to delete transcoded file: {transcode_to}")
113
110
 
114
111
  if (
115
112
  "Error opening input files" in last_output
116
113
  or "No such file or directory" in last_output
117
114
  ):
118
- self.logger.info(
119
- f"file does not exist: {path}, skipping transcoding"
120
- )
115
+ self.logger.info(f"file does not exist: {path}, skipping transcoding")
121
116
  return True
122
117
 
123
118
  if "File name too long" in last_output:
124
- self.logger.info(
125
- f"file name is too long: {path}, skipping transcoding"
126
- )
119
+ self.logger.info(f"file name is too long: {path}, skipping transcoding")
127
120
  return True
128
121
 
129
122
  if "received signal 15" in last_output:
130
- self.logger.info(
131
- f"transcoding interrupted by signal: {path}, will retry"
132
- )
123
+ self.logger.info(f"transcoding interrupted by signal: {path}, will retry")
133
124
  return False
134
125
 
135
126
  if "cannot open shared object file" in last_output:
136
- self.logger.error(
137
- "ffmpeg error: missing shared object file, will retry"
138
- )
127
+ self.logger.error("ffmpeg error: missing shared object file, will retry")
139
128
  push.send(f"ffmpeg error: {last_output}", title="ffmpeg error")
140
129
  return False
141
130
 
@@ -160,9 +149,7 @@ class Transcoder:
160
149
  return True
161
150
 
162
151
  if not path.exists():
163
- self.logger.debug(
164
- f"file doesn't exist: {item.path}, deleting transcoded file"
165
- )
152
+ self.logger.debug(f"file doesn't exist: {item.path}, deleting transcoded file")
166
153
 
167
154
  transcode_to.unlink(missing_ok=True)
168
155
  return True
@@ -171,24 +158,26 @@ class Transcoder:
171
158
  shutil.move(transcode_to, new_path)
172
159
  path.unlink()
173
160
 
174
- self._rescan_content(item, str(new_path))
161
+ self._rescan_content(item, new_path)
175
162
 
176
163
  self.logger.info(f"transcoded: {path.name} -> {new_path.name}")
177
164
  # push.send(f"{path.name} -> {new_path.name}", title="file transcoded")
178
165
 
179
166
  return True
180
167
 
181
- def _rescan_content(self, item: TranscodeItem, new_path: str) -> None:
168
+ def _rescan_content(self, item: TranscodeItem, new_path: pathlib.Path) -> None:
182
169
  if item.content_id is not None:
183
- if new_path.startswith(config["radarr"]["root_folder"]):
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
184
175
  self.radarr.rescan_movie(item.content_id)
185
- # radarr bug that it doesn't see the deleted file and the new file
186
- # in one rescan?
187
- # have to sleep in between to ensure initial command finishes
188
- # or use pyarr.get_command() to see command status
189
176
  sleep(5)
190
177
  self.radarr.rescan_movie(item.content_id)
191
- elif new_path.startswith(config["sonarr"]["root_folder"]):
178
+ elif new_path.is_relative_to(
179
+ replace_remote_paths(pathlib.Path(config["sonarr"]["root_folder"]))
180
+ ):
192
181
  self.sonarr.rescan_series(item.content_id)
193
182
 
194
183
  def _get_duration(self, path: str) -> timedelta:
@@ -211,20 +200,16 @@ class Transcoder:
211
200
 
212
201
  return duration
213
202
 
214
- def _build_ffmpeg_command(
215
- self, item: TranscodeItem, transcode_to: pathlib.Path
216
- ) -> list[str]:
203
+ def _build_ffmpeg_command(self, item: TranscodeItem, transcode_to: pathlib.Path) -> list[str]:
217
204
  command: list[Any] = [
218
205
  "ffmpeg",
219
206
  "-hide_banner",
220
207
  "-y",
221
208
  ]
222
209
 
223
- try:
210
+ if "transcoding" in config and "hwaccel" in config["transcoding"]:
224
211
  command.extend(["-hwaccel", config["transcoding"]["hwaccel"]])
225
212
  command.extend(["-hwaccel_output_format", config["transcoding"]["hwaccel"]])
226
- except KeyError:
227
- pass
228
213
 
229
214
  command.extend(["-probesize", "100M"])
230
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 RemotePathMapping, config
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
- try:
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.9
3
+ Version: 1.4.11
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 ==2.3.2
38
- Requires-Dist: Flask ==3.0.2
39
- Requires-Dist: mongoengine ==0.28.1
40
- Requires-Dist: pyarr ==5.2.0
41
- Requires-Dist: PyYAML ==6.0.1
42
- Requires-Dist: requests ==2.31.0
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 ==0.15.1 ; extra == 'dev'
45
- Requires-Dist: mypy ==1.3.0 ; extra == 'dev'
46
- Requires-Dist: pre-commit ==3.6.2 ; extra == 'dev'
47
- Requires-Dist: ruff ==0.3.0 ; extra == 'dev'
48
- Requires-Dist: types-PyYAML ==6.0.12.12 ; extra == 'dev'
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
 
@@ -68,6 +66,11 @@ Requires Python >=3.10.
68
66
 
69
67
  ### TODO
70
68
 
69
+ - https://github.com/kkroening/ffmpeg-python
70
+ - `ffmpeg -codecs`, `ffmpeg -hwaccels`
71
+ - ffmpeg filters for deinterlacing, scaling
72
+ - https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html#hwaccel-transcode-with-scaling
73
+ - ffmpeg remove bad subtitle streams
71
74
  - use sqlite
72
75
  - have config.discord.users be a dict with 'quotas' and 'name' for *arr tags
73
76
  - Better pushover notifications
@@ -98,5 +101,3 @@ Requires Python >=3.10.
98
101
  - Tautulli API (get_history) to show who has already seen the movie
99
102
  - User leaderboard
100
103
  - movies/shows added, Tautulli watch counts
101
-
102
-
@@ -0,0 +1,32 @@
1
+ wi1_bot/__init__.py,sha256=11ozJKiUsqDCZ3_mcAHhGYUyGK_Unl54djVSBBExFB4,59
2
+ wi1_bot/_version.py,sha256=CiEe7E8jVlrB3YZUumeEso6PXucVNqiDDQfL8kleYok,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.11.dist-info/LICENSE,sha256=6V4_mQoPoLJl77_WMsQRQMDG2mnIhDUdfCmMkqM9Qwc,1072
28
+ wi1_bot-1.4.11.dist-info/METADATA,sha256=qmX6VltiOGws7_0nK8G-jfPu0L_2BXzbwV2dmhcqLNA,4750
29
+ wi1_bot-1.4.11.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
30
+ wi1_bot-1.4.11.dist-info/entry_points.txt,sha256=pF5EawAQsUNMHs5exynpNdRq9g4dODg-RaPkx2MyYLU,184
31
+ wi1_bot-1.4.11.dist-info/top_level.txt,sha256=Q7mTnPLk80Td82YbjlBMO5tvXJoTFHhheHdmwc-c-7I,8
32
+ wi1_bot-1.4.11.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,31 +0,0 @@
1
- wi1_bot/__init__.py,sha256=11ozJKiUsqDCZ3_mcAHhGYUyGK_Unl54djVSBBExFB4,59
2
- wi1_bot/_version.py,sha256=7YfyzKekRBziu-K_N0hnrpsNnrC-U6NwEUBuUEQ_xUA,411
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=I8_SyUlbD-bSM-71adsZOoc_-yFdsC_SGXpJvZ5IrbM,5743
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=qqWiLnYbUJFTfS74bKZ3t2DlxAJLlf7J8Fh5QAB8glk,9327
26
- wi1_bot-1.4.9.dist-info/LICENSE,sha256=6V4_mQoPoLJl77_WMsQRQMDG2mnIhDUdfCmMkqM9Qwc,1072
27
- wi1_bot-1.4.9.dist-info/METADATA,sha256=L9EiHILLhO4MyQ0Vk7-AdfI1zaIiWzqynCIOhEm4h6E,4530
28
- wi1_bot-1.4.9.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
29
- wi1_bot-1.4.9.dist-info/entry_points.txt,sha256=pF5EawAQsUNMHs5exynpNdRq9g4dODg-RaPkx2MyYLU,184
30
- wi1_bot-1.4.9.dist-info/top_level.txt,sha256=Q7mTnPLk80Td82YbjlBMO5tvXJoTFHhheHdmwc-c-7I,8
31
- wi1_bot-1.4.9.dist-info/RECORD,,