style: remove claude comments
This commit is contained in:
@@ -4,26 +4,9 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
|
||||||
# Mount your custom config here — overrides the defaults inside the image
|
|
||||||
- ./config:/app/config:ro
|
|
||||||
environment:
|
|
||||||
# Optional: override the port
|
|
||||||
- LOG_LEVEL=debug
|
|
||||||
- SERVER_PORT=8080
|
|
||||||
networks:
|
networks:
|
||||||
- seerr-net
|
- seerr-net
|
||||||
|
|
||||||
# ── Jellyseerr (local test instance) ─────────────────────────────────────
|
|
||||||
# Access the UI at: http://localhost:5055
|
|
||||||
#
|
|
||||||
# After first start, go through the setup wizard. When asked for a media
|
|
||||||
# server you can skip it or point to a local Jellyfin (see below).
|
|
||||||
#
|
|
||||||
# Webhook setup in Jellyseerr:
|
|
||||||
# Settings → Notifications → Webhook
|
|
||||||
# URL: http://webhook-proxy:8080/webhook ← uses the internal Docker hostname
|
|
||||||
# Payload: leave as default
|
|
||||||
seerr:
|
seerr:
|
||||||
image: seerr/seerr:latest
|
image: seerr/seerr:latest
|
||||||
container_name: seerr
|
container_name: seerr
|
||||||
@@ -36,9 +19,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- seerr-net
|
- seerr-net
|
||||||
|
|
||||||
# ── Jellyfin (optional — only needed if Jellyseerr asks for a media server)
|
|
||||||
# Access the UI at: http://localhost:8096
|
|
||||||
# Remove this block if you don't want a full media server locally.
|
|
||||||
jellyfin:
|
jellyfin:
|
||||||
image: jellyfin/jellyfin:latest
|
image: jellyfin/jellyfin:latest
|
||||||
container_name: jellyfin
|
container_name: jellyfin
|
||||||
|
|||||||
@@ -4,19 +4,13 @@ import lombok.*;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Component
|
@Component
|
||||||
@ConfigurationProperties(prefix = "proxy")
|
@ConfigurationProperties(prefix = "proxy")
|
||||||
public class ProxyConfig {
|
public class ProxyConfig {
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of notification_type → list of Discord webhook URLs.
|
|
||||||
* Use the key "default" to catch all unmatched notification types.
|
|
||||||
*/
|
|
||||||
private List<String> routes = List.of();
|
private List<String> routes = List.of();
|
||||||
private Filters filters;
|
private Filters filters;
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/**
|
|
||||||
* Receives incoming Overseerr webhook POST requests and
|
|
||||||
* delegates forwarding to {@link DiscordForwardingService}.
|
|
||||||
*
|
|
||||||
* Endpoint: POST /webhook
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -24,10 +18,6 @@ public class WebhookController {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ProxyConfig proxyConfig;
|
private final ProxyConfig proxyConfig;
|
||||||
|
|
||||||
/**
|
|
||||||
* Main webhook endpoint.
|
|
||||||
* Overseerr should be configured to POST to: http://<your-host>:<port>/webhook
|
|
||||||
*/
|
|
||||||
@PostMapping("/webhook")
|
@PostMapping("/webhook")
|
||||||
public ResponseEntity<String> receiveWebhook(@RequestBody String rawBody) {
|
public ResponseEntity<String> receiveWebhook(@RequestBody String rawBody) {
|
||||||
log.info("Received webhook payload: {}", rawBody);
|
log.info("Received webhook payload: {}", rawBody);
|
||||||
@@ -51,17 +41,8 @@ public class WebhookController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward asynchronously — we return 200 immediately to Overseerr
|
|
||||||
forwardingService.forward(payload, rawBody);
|
forwardingService.forward(payload, rawBody);
|
||||||
|
|
||||||
return ResponseEntity.ok("OK");
|
return ResponseEntity.ok("OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple health / test endpoint.
|
|
||||||
*/
|
|
||||||
@GetMapping("/health")
|
|
||||||
public ResponseEntity<String> health() {
|
|
||||||
return ResponseEntity.ok("Webhook proxy is running.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ public class SeerrPayload {
|
|||||||
|
|
||||||
/** Media overview / synopsis, or issue description. */
|
/** Media overview / synopsis, or issue description. */
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
/** URL to the media poster image. */
|
|
||||||
private String image;
|
private String image;
|
||||||
|
|
||||||
private Media media;
|
private Media media;
|
||||||
private Request request;
|
private Request request;
|
||||||
private Issue issue;
|
private Issue issue;
|
||||||
@@ -39,14 +36,9 @@ public class SeerrPayload {
|
|||||||
|
|
||||||
@JsonProperty("media_type")
|
@JsonProperty("media_type")
|
||||||
private String mediaType;
|
private String mediaType;
|
||||||
|
|
||||||
private String tmdbId;
|
private String tmdbId;
|
||||||
private String tvdbId;
|
private String tvdbId;
|
||||||
|
|
||||||
/** Availability status: UNKNOWN | PENDING | PROCESSING | PARTIALLY_AVAILABLE | AVAILABLE */
|
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
/** 4K availability status (same values as status). */
|
|
||||||
private String status4k;
|
private String status4k;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +72,9 @@ public class SeerrPayload {
|
|||||||
@JsonProperty("issue_id")
|
@JsonProperty("issue_id")
|
||||||
private String issueId;
|
private String issueId;
|
||||||
|
|
||||||
/** VIDEO | AUDIO | SUBTITLES | OTHER */
|
|
||||||
@JsonProperty("issue_type")
|
@JsonProperty("issue_type")
|
||||||
private String issueType;
|
private String issueType;
|
||||||
|
|
||||||
/** OPEN | IN_PROGRESS | RESOLVED */
|
|
||||||
@JsonProperty("issue_status")
|
@JsonProperty("issue_status")
|
||||||
private String issueStatus;
|
private String issueStatus;
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a Discord embed message from the Overseerr payload
|
|
||||||
* and forwards it concurrently to all configured webhook URLs.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -34,12 +30,6 @@ public class DiscordForwardingService {
|
|||||||
.connectTimeout(Duration.ofSeconds(5))
|
.connectTimeout(Duration.ofSeconds(5))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/**
|
|
||||||
* Forwards the payload to all Discord webhooks matching the notification type.
|
|
||||||
*
|
|
||||||
* @param payload the parsed Overseerr payload
|
|
||||||
* @param rawBody the original raw JSON body (used as fallback)
|
|
||||||
*/
|
|
||||||
public void forward(SeerrPayload payload, String rawBody) {
|
public void forward(SeerrPayload payload, String rawBody) {
|
||||||
String mediaName = payload.getSubject();
|
String mediaName = payload.getSubject();
|
||||||
List<String> targets = proxyConfig.getRoutes();
|
List<String> targets = proxyConfig.getRoutes();
|
||||||
@@ -53,20 +43,14 @@ public class DiscordForwardingService {
|
|||||||
|
|
||||||
String discordBody = buildDiscordPayload(payload);
|
String discordBody = buildDiscordPayload(payload);
|
||||||
|
|
||||||
// Fire all requests concurrently
|
|
||||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||||
for (String webhookUrl : targets) {
|
for (String webhookUrl : targets) {
|
||||||
futures.add(sendAsync(webhookUrl, discordBody, mediaName));
|
futures.add(sendAsync(webhookUrl, discordBody, mediaName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all to complete (best-effort, errors are logged but not re-thrown)
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Internal helpers
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private CompletableFuture<Void> sendAsync(String webhookUrl, String body, String notificationType) {
|
private CompletableFuture<Void> sendAsync(String webhookUrl, String body, String notificationType) {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(webhookUrl))
|
.uri(URI.create(webhookUrl))
|
||||||
@@ -92,10 +76,6 @@ public class DiscordForwardingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a Discord-compatible JSON payload with an embed.
|
|
||||||
* Discord webhooks expect: { "embeds": [ { "title": "...", ... } ] }
|
|
||||||
*/
|
|
||||||
private String buildDiscordPayload(SeerrPayload payload) {
|
private String buildDiscordPayload(SeerrPayload payload) {
|
||||||
try {
|
try {
|
||||||
int color = colorForType(payload.getNotificationType());
|
int color = colorForType(payload.getNotificationType());
|
||||||
@@ -110,7 +90,6 @@ public class DiscordForwardingService {
|
|||||||
embed.put("thumbnail", Map.of("url", payload.getImage()));
|
embed.put("thumbnail", Map.of("url", payload.getImage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field: requested by
|
|
||||||
List<Map<String, Object>> fields = new ArrayList<>();
|
List<Map<String, Object>> fields = new ArrayList<>();
|
||||||
if (payload.getRequest() != null && payload.getRequest().getRequestedByUsername() != null) {
|
if (payload.getRequest() != null && payload.getRequest().getRequestedByUsername() != null) {
|
||||||
fields.add(field("Requested by", payload.getRequest().getRequestedByUsername(), true));
|
fields.add(field("Requested by", payload.getRequest().getRequestedByUsername(), true));
|
||||||
@@ -153,9 +132,6 @@ public class DiscordForwardingService {
|
|||||||
return Map.of("name", name, "value", value, "inline", inline);
|
return Map.of("name", name, "value", value, "inline", inline);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a Discord embed color (decimal) based on the notification type.
|
|
||||||
*/
|
|
||||||
private int colorForType(String type) {
|
private int colorForType(String type) {
|
||||||
if (type == null) return 0x95A5A6; // grey
|
if (type == null) return 0x95A5A6; // grey
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
@@ -166,11 +142,6 @@ public class DiscordForwardingService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Masks the webhook token in the URL for safe logging.
|
|
||||||
* Input: https://discord.com/api/webhooks/1234567890/AbCdEfGhIj...
|
|
||||||
* Output: https://discord.com/api/webhooks/1234567890/Ab***
|
|
||||||
*/
|
|
||||||
private String maskUrl(String url) {
|
private String maskUrl(String url) {
|
||||||
int lastSlash = url.lastIndexOf('/');
|
int lastSlash = url.lastIndexOf('/');
|
||||||
if (lastSlash < 0 || lastSlash >= url.length() - 1) return url;
|
if (lastSlash < 0 || lastSlash >= url.length() - 1) return url;
|
||||||
|
|||||||
Reference in New Issue
Block a user