-
-
Notifications
You must be signed in to change notification settings - Fork 31
Fix 403 forbidden error when following redirect #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3c548f2
9ae0922
07bb805
1a2e0f7
3253fcc
bbebdc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,7 @@ object ConfigurationUtils { | |
| sonarrBypassIgnored = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean) | ||
| sonarrSeasonMonitoring = configReader.getConfigOption(Keys.sonarrSeasonMonitoring).getOrElse("all") | ||
| radarrConfig <- getRadarrConfig(configReader, client) | ||
| (radarrBaseUrl, radarrApiKey, radarrQualityProfileId, radarrRootFolder, radarrTagIds) = radarrConfig | ||
| (radarrBaseUrl, radarrApiKey, radarrQualityProfileId, radarrRootFolder, radarrTagIds, radarrAvailability) = radarrConfig | ||
| radarrBypassIgnored = configReader.getConfigOption(Keys.radarrBypassIgnored).exists(_.toBoolean) | ||
| plexTokens = getPlexTokens(configReader) | ||
| skipFriendSync = configReader.getConfigOption(Keys.skipFriendSync).flatMap(_.toBooleanOption).getOrElse(false) | ||
|
|
@@ -64,6 +64,7 @@ object ConfigurationUtils { | |
| radarrBaseUrl, | ||
| radarrApiKey, | ||
| radarrQualityProfileId, | ||
| radarrAvailability, | ||
| radarrRootFolder, | ||
| radarrBypassIgnored, | ||
| radarrTagIds | ||
|
|
@@ -159,7 +160,7 @@ object ConfigurationUtils { | |
| private def getRadarrConfig( | ||
| configReader: ConfigurationReader, | ||
| client: HttpClient | ||
| ): IO[(Uri, String, Int, String, Set[Int])] = { | ||
| ): IO[(Uri, String, Int, String, Set[Int], Option[String])] = { | ||
| val apiKey = configReader.getConfigOption(Keys.radarrApiKey).getOrElse(throwError("Unable to find radarr API key")) | ||
| val configuredUrl = configReader.getConfigOption(Keys.radarrBaseUrl) | ||
| val possibleUrls: Seq[String] = | ||
|
|
@@ -187,7 +188,8 @@ object ConfigurationUtils { | |
| .getConfigOption(Keys.radarrTags) | ||
| .map(getTagIdsFromConfig(client, url, apiKey)) | ||
| .getOrElse(IO.pure(Set.empty[Int])) | ||
| } yield (url, apiKey, qualityProfileId, rootFolder, tagIds) | ||
| availability = configReader.getConfigOption(Keys.radarrAvailability).map(_.trim).filter(_.nonEmpty) | ||
| } yield (url, apiKey, qualityProfileId, rootFolder, tagIds, availability) | ||
|
Comment on lines
+191
to
+192
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
# Show the supported Radarr values.
curl -fsSL \
'https://raw.githubusercontent.com/Radarr/Radarr/develop/src/Radarr.Api.V3/openapi.json' |
jq -r '.components.schemas.MovieStatusType.enum[]'
# Trace validation and error-reporting paths.
rg -n -C4 --glob '*.scala' \
'radarrAvailability|minimumAvailability|Sent .* to Radarr|Received warning for sending' .Repository: nylonee/watchlistarr Length of output: 9455 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
# Inspect imports/dependencies and exact call sites relevant to validation and HTTP response handling.
rg -n --glob '*.scala' 'case class RadarrConfiguration|def postToArr|EitherT|Client|httpClient|raiseFailures' .
git ls-files '*build*' | sed -n '1,120p'
cat -n src/main/scala/configuration/ConfigurationUtils.scala | sed -n '160,200p'
cat -n src/main/scala/configuration/Configuration.scala | sed -n '1,80p'
if [ -f src/main/scala/radarr/RadarrUtils.scala ]; then
cat -n src/main/scala/radarr/RadarrUtils.scala | sed -n '1,70p'
fi
if [ -f src/main/scala/radarr/RadarrPost.scala ]; then
cat -n src/main/scala/radarr/RadarrPost.scala | sed -n '1,40p'
fi
# Check whether the repository has any existing enum-like helper for Radarr availability.
rg -n --glob '*.scala' 'tba|announced|inCinemas|released|deleted|MovieStatusType|RadarrAvailability|radarrAvailability' .Repository: nylonee/watchlistarr Length of output: 35372 Reject unsupported Radarr
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private def getTagIdsFromConfig(client: HttpClient, url: Uri, apiKey: String)(tags: String): IO[Set[Int]] = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| package radarr | ||
|
|
||
| private case class AddOptions(searchForMovie: Boolean = true) | ||
| case class AddOptions(searchForMovie: Boolean = true) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,32 @@ | ||
| package radarr | ||
|
|
||
| private case class RadarrPost( | ||
| import io.circe.generic.auto._ | ||
| import io.circe.syntax._ | ||
| import io.circe.{Encoder, Json, JsonObject} | ||
|
|
||
| case class RadarrPost( | ||
| title: String, | ||
| tmdbId: Long, | ||
| qualityProfileId: Int = 6, | ||
| rootFolderPath: String, | ||
| addOptions: AddOptions = AddOptions(), | ||
| tags: List[Int] = List.empty[Int] | ||
| minimumAvailability: Option[String] = None, | ||
| tags: List[Int] = List.empty[Int], | ||
| addOptions: AddOptions = AddOptions() | ||
| ) | ||
|
|
||
| object RadarrPost { | ||
| implicit val encoder: Encoder[RadarrPost] = Encoder.instance { post => | ||
| var fields = List[(String, Json)]( | ||
| "title" -> post.title.asJson, | ||
| "tmdbId" -> post.tmdbId.asJson, | ||
| "qualityProfileId" -> post.qualityProfileId.asJson, | ||
| "rootFolderPath" -> post.rootFolderPath.asJson, | ||
| "tags" -> post.tags.asJson | ||
| ) | ||
| post.minimumAvailability.foreach { avail => | ||
| fields = fields :+ ("minimumAvailability" -> avail.asJson) | ||
| } | ||
| fields = fields :+ ("addOptions" -> post.addOptions.asJson) | ||
| Json.fromJsonObject(JsonObject.fromIterable(fields)) | ||
| } | ||
| } | ||
|
Comment on lines
+17
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C4 --glob '*.scala' \
'AddOptions|addOptions|searchForMovie|RadarrPost' .
curl -fsSL \
'https://raw.githubusercontent.com/Radarr/Radarr/develop/src/Radarr.Api.V3/openapi.json' |
jq -e '.components.schemas.AddMovieOptions.properties.searchForMovie'Repository: nylonee/watchlistarr Length of output: 7623 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- RadarrPost.scala ---'
cat -n src/main/scala/radarr/RadarrPost.scala
printf '%s\n' '--- RadarrUtils.scala relevant section ---'
cat -n src/main/scala/radarr/RadarrUtils.scala
printf '%s\n' '--- Radarr delete post addOptions (context) ---'
cat -n src/main/scala/radarr/RadarrDelete.scala
printf '%s\n' '--- json output from current encoder ---'
python3 - <<'PY'
from pathlib import Path
src = Path('src/main/scala/radarr/RadarrPost.scala').read_text()
print('addOptions field in RadarrPost.scala:', 'addOptions' in src)
print('searchForMovie in RadarrPost.scala:', 'searchForMovie' in src)
PYRepository: nylonee/watchlistarr Length of output: 6063 🌐 Web query:
💡 Result: To trigger an automatic search when adding a movie via the Radarr API, you must include the addOptions object in your POST request body with the searchForMovie property set to true [1][2][3]. The correct JSON structure for the request body is as follows: { "title": "Movie Title", "tmdbId": 12345, "qualityProfileId": 1, "rootFolderPath": "/path/to/movies", "monitored": true, "addOptions": { "searchForMovie": true } } When adding a movie via the POST /api/v3/movie endpoint, the searchForMovie option is not set to true by default [1]. Including this option explicitly in the payload instructs Radarr to perform an immediate search for the movie upon addition [1][2]. This mechanism is widely used to replicate the behavior available in the Radarr Web UI where users can check an option to automatically search for the movie once it is added [1]. Citations:
Set
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -91,12 +91,14 @@ trait SonarrUtils extends SonarrConversions { | |||||||||||||||||||
|
|
||||||||||||||||||||
| private def postToArr[T: Decoder]( | ||||||||||||||||||||
| client: HttpClient | ||||||||||||||||||||
| )(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = | ||||||||||||||||||||
| )(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = { | ||||||||||||||||||||
| logger.info(s"Sending JSON to Sonarr: $payload") | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not log the full payload at INFO level. Line [95] writes the complete Sonarr JSON payload to application logs. The payload includes media metadata and configuration values such as titles, identifiers, root folders, profile IDs, and tags. Retained or shared logs can expose user library information and filesystem paths. Log only the endpoint and request status at INFO level. Use a redacted payload at DEBUG level only when detailed diagnostics are required. Proposed fix- logger.info(s"Sending JSON to Sonarr: $payload")
+ logger.info(s"Sending request to Sonarr endpoint: $endpoint")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify that the Sonarr log is sequenced inside the returned effect.
rg -n -C 8 'logger\.info\(s"Sending JSON to Sonarr|def postToArr|client\.httpRequest' src/main/scalaRepository: nylonee/watchlistarr Length of output: 17884 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== SonarrUtils context =="
sed -n '1,140p' src/main/scala/sonarr/SonarrUtils.scala
echo
echo "== postToArr call sites =="
rg -n -C 4 'postToArr\b|EitherT\(client\.httpRequest' src/main/scala/sonarr src/main/scala/radarr src/main/scala
echo
echo "== Cats IO/EitherT availability in project files =="
rg -n 'cats-effect|typelevel|cats' build.sbt project . --glob '!target/**' --glob '!node_modules/**' 2>/dev/null || trueRepository: nylonee/watchlistarr Length of output: 21767 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Use an embedded REPL-free behavioral probe that treats source text as data.
# It checks whether the INFO log is syntactically outside the IO/EitherT effect body.
python3 - <<'PY'
from pathlib import Path
import re
path = Path('src/main/scala/sonarr/SonarrUtils.scala')
src = path.read_text()
def extract_function_method(method_name: str):
# Find the declaration and then consume balanced braces, with support for string interpolation chars.
# For demonstration, the SonarrUtils method is simple enough that string literal balance is sufficient.
pattern = re.compile(f'\\bdef\\s+{re.escape(method_name)}\\b\\s*\\([^)]*\\)\\s*[^=]*=\\s*\\n')
m = pattern.search(src)
if not m:
return None
start = m.start()
i = src.find('{', m.end())
if i == -1:
return None
depth = 0
squote = dquote = raw = rawd = False
while i < len(src):
c = src[i]
cc = src[i:i+2]
if raw:
if c == '`': raw = False
elif rawd:
if src.startswith('"""', i): rawd = False; i += 2; continue
elif cc == '`'': raw = True; i += 1; continue
elif cc == '"""': rawd = True; continue
elif c == '"' and not dquote:
dquote = True
elif c == '"' and dquote:
dquote = False
elif cc == '{{': i += 1; continue
elif cc == '}}': i += 1; continue
else:
if c == '{': depth += 1
elif c == '}':
depth -= 1
if depth == 0:
end = i + 1
break
i += 1
return src[start:end], start, end
body, start, end = extract_function_method('postToArr')
print(f'function_offset_start={start}')
print(f'log_offset={src.find("logger.info(s\"Sending JSON to Sonarr:", start, end)}')
print(f'for_offset={src.find("for {", start, end)}')
inside_brace = body.index('logger.info(s"Sending JSON to Sonarr:') < body.index('for {')
# The method assigns into a for-comprehension; the returned effect starts at the for, not before it.
returned_effect_start = body.index('for {')
print(f'log_before_for={inside_brace}')
print(f'log_inside_returned_effect_body={inside_brace}')
print(f'postToArr_return_effect_starts_at_for={returned_effect_start > start}')
PYRepository: nylonee/watchlistarr Length of output: 332 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Exact Sonarr log position =="
python3 - <<'PY'
from pathlib import Path
text = Path('src/main/scala/sonarr/SonarrUtils.scala').read_text()
needle = 'logger.info(s"Sending JSON to Sonarr:'
start = text.find(needle, text.index('private def postToArr'))
end = text.index('\n}', start) + 2
lines = text.splitlines()
for_offset = text.find('for {', text.indexOf('private def postToArr') if hasattr(text,'indexOf') else text.index('private def postToArr'))
if start != -1:
line_no = text[:text.find(needle, text.index('private def postToArr'))].count('\n') + 1
print(f'log_at_line={line_no}')
print(f'for_at_block={text.find("for {", start)}')
PY
echo
echo "== Read-only line-number sanity check from git diff against local file =="
(git diff -- src/main/scala/sonarr/SonarrUtils.scala | sed -n '1,80p' || true)
sed -n '92,103p' src/main/scala/sonarr/SonarrUtils.scalaRepository: nylonee/watchlistarr Length of output: 869 Move the Sonarr request log inside the Line 95 logs while Proposed fix )(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = {
- logger.info(s"Sending JSON to Sonarr: $payload")
for {
+ _ <- EitherT.liftF[IO, Throwable, Unit](IO(logger.info(s"Sending JSON to Sonarr: $payload")))
response <- EitherT(
client.httpRequest(Method.POST, baseUrl / "api" / "v3" / endpoint, Some(apiKey), Some(payload))
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| for { | ||||||||||||||||||||
| response <- EitherT( | ||||||||||||||||||||
| client.httpRequest(Method.POST, baseUrl / "api" / "v3" / endpoint, Some(apiKey), Some(payload)) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| maybeDecoded <- EitherT.pure[IO, Throwable](response.as[T]) | ||||||||||||||||||||
| decoded <- EitherT.fromOption[IO](maybeDecoded.toOption, new Throwable("Unable to decode response from Sonarr")) | ||||||||||||||||||||
| } yield decoded | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this necessary to get the image to build? If not removing these changes would make the change smaller and easier to get merged (although it doesn't appear like the size of the change is the issue and this is abandoned)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, was necessary as openjdk has been deprecated.