wip: arr progress - request approval/denial

This commit is contained in:
IRHM
2024-04-07 17:42:58 +01:00
parent a4fab86472
commit 42ca418d55
9 changed files with 486 additions and 58 deletions
+58 -11
View File
@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"errors"
"log/slog"
"time"
@@ -9,18 +10,41 @@ import (
"gorm.io/gorm"
)
type ArrRequestStatus string
const (
// Pending approval from an admin.
ARR_REQUEST_PENDING ArrRequestStatus = "PENDING"
// Request has been approved and should be added to sonarr/radarr.
ARR_REQUEST_APPROVED ArrRequestStatus = "APPROVED"
ARR_REQUEST_AUTO_APPROVED ArrRequestStatus = "AUTO_APPROVED"
// Request has been denied, not adding content.
ARR_REQUEST_DENIED ArrRequestStatus = "DENIED"
// Content was found on sonarr/radarr already, nothing needs to be done.
ARR_REQUEST_FOUND ArrRequestStatus = "FOUND"
)
type ArrRequest struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
UserID uint `json:"-" gorm:"not null"`
User User `json:"-"`
ContentID *int `json:"-" gorm:"uniqueIndex:sn_to_cid;not null"`
Content *Content `json:"content,omitempty"`
// Username of `User`.
// We don't want to send back the entire user object, just their name.
// Not stored in DB, only used for our response from api.
Username string `json:"username" gorm:"-"`
ContentID *int `json:"-" gorm:"uniqueIndex:sn_to_cid;not null"`
Content *Content `json:"content,omitempty"`
// Server names are used as an identifier
ServerName string `json:"serverName" gorm:"uniqueIndex:sn_to_cid;not null"`
// Sonarr/Radarrs seriesId/movieId
ArrID int `json:"arrId"`
// Tracked request status
Status ArrRequestStatus `json:"status" gorm:"default:PENDING"`
// Full request made by user (arr.SonarrRequest / arr.RadarrRequest)
// so we know how to fulfil the request if approved.
RequestJson string `json:"requestJson"`
}
func deleteArrRequest(db *gorm.DB, id uint) error {
@@ -35,11 +59,14 @@ func deleteArrRequest(db *gorm.DB, id uint) error {
// Gets all requests.
func getArrRequests(db *gorm.DB) ([]ArrRequest, error) {
var req []ArrRequest
resp := db.Preload("Content").Find(&req)
resp := db.Preload("Content").Preload("User").Find(&req)
if resp.Error != nil {
slog.Error("getArrRequests: Failed to search for requests in db", "error", resp.Error)
return []ArrRequest{}, errors.New("failed to find requests")
}
for i := range req {
req[i].Username = req[i].User.Username
}
return req, nil
}
@@ -63,13 +90,13 @@ func getArrRequestByTmdbId(db *gorm.DB, contentType ContentType, tmdbId int) (Ar
return req, nil
}
func createArrRequest(db *gorm.DB, userId uint, serverName string, contentType ContentType, tmdbId int) (*ArrRequest, error) {
func createArrRequest(db *gorm.DB, userId uint, serverName string, contentType ContentType, tmdbId int, reqJson string) (*ArrRequest, error) {
content, err := getOrCacheContent(db, contentType, tmdbId)
if err != nil {
slog.Error("createArrRequest: getOrCacheContent errored.")
return &ArrRequest{}, err
}
req := ArrRequest{UserID: userId, ServerName: serverName, ContentID: &content.ID}
req := ArrRequest{UserID: userId, ServerName: serverName, ContentID: &content.ID, RequestJson: reqJson}
resp := db.Create(&req)
if resp.Error != nil {
slog.Error("createArrRequest: Failed when inserting request into db.", "error", err)
@@ -84,7 +111,13 @@ func createSonarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.SonarrR
slog.Error("createSonarrRequest: Failed to get server", "error", err)
return &ArrRequest{}, errors.New("failed to get server")
}
arrReq, err := createArrRequest(db, userId, ur.ServerName, SHOW, ur.TMDBID)
reqJson, err := json.Marshal(ur)
if err != nil {
slog.Error("createRadarrRequest: Failed when marshalling json request", "error", err)
return &ArrRequest{}, errors.New("failed when processing request")
}
// Since we create the request in the db now, we don't have to check for duplicates, a unique constraint will error us here if hit.
arrReq, err := createArrRequest(db, userId, ur.ServerName, SHOW, ur.TMDBID, string(reqJson[:]))
if err != nil {
slog.Error("createSonarrRequest: Failed when creating arr request", "error", err)
return &ArrRequest{}, errors.New("failed when creating request")
@@ -97,7 +130,7 @@ func createSonarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.SonarrR
found := lookupRes[0] // There should only be one result when looking up by id.
// If it has an ID, then it will have already been added to Sonarr.
if found.ID != 0 {
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", found.ID)
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", found.ID).Update("status", ARR_REQUEST_FOUND)
if dbResp.Error != nil {
slog.Error("createSonarrRequest: Failed to update request in db", "error", err)
return &ArrRequest{}, errors.New("content was requested, but we failed to update the db")
@@ -117,7 +150,7 @@ func createSonarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.SonarrR
slog.Error("createSonarrRequest: Failed to add content", "error", err)
return &ArrRequest{}, errors.New("failed to add content")
}
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", resp["id"])
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", resp["id"]).Update("status", ARR_REQUEST_AUTO_APPROVED)
if dbResp.Error != nil {
slog.Error("createSonarrRequest: Failed to update request in db", "error", err)
return &ArrRequest{}, errors.New("content was requested, but we failed to update the db")
@@ -138,7 +171,13 @@ func createRadarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.RadarrR
slog.Error("createRadarrRequest: Failed to get server", "error", err)
return &ArrRequest{}, errors.New("failed to get server")
}
arrReq, err := createArrRequest(db, userId, ur.ServerName, MOVIE, ur.TMDBID)
reqJson, err := json.Marshal(ur)
if err != nil {
slog.Error("createRadarrRequest: Failed when marshalling json request", "error", err)
return &ArrRequest{}, errors.New("failed when processing request")
}
// Since we create the request in the db now, we don't have to check for duplicates, a unique constraint will error us here if hit.
arrReq, err := createArrRequest(db, userId, ur.ServerName, MOVIE, ur.TMDBID, string(reqJson[:]))
if err != nil {
slog.Error("createRadarrRequest: Failed when creating arr request", "error", err)
return &ArrRequest{}, errors.New("failed when creating request")
@@ -151,7 +190,7 @@ func createRadarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.RadarrR
found := lookupRes[0] // There should only be one result when looking up by id.
// If it has an ID, then it will have already been added to Radarr.
if found.ID != 0 {
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", found.ID)
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", found.ID).Update("status", ARR_REQUEST_FOUND)
if dbResp.Error != nil {
slog.Error("createRadarrRequest: Failed to update request in db", "error", err)
return &ArrRequest{}, errors.New("content was requested, but we failed to update the db")
@@ -171,7 +210,7 @@ func createRadarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.RadarrR
slog.Error("createRadarrRequest: Failed to add content", "error", err)
return &ArrRequest{}, errors.New("failed to add content")
}
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", resp["id"])
dbResp := db.Model(&ArrRequest{}).Where("id = ?", arrReq.ID).Update("arr_id", resp["id"]).Update("status", ARR_REQUEST_AUTO_APPROVED)
if dbResp.Error != nil {
slog.Error("createRadarrRequest: Failed to update request in db", "error", err)
return &ArrRequest{}, errors.New("content was requested, but we failed to update the db")
@@ -187,6 +226,10 @@ func createRadarrRequest(db *gorm.DB, userId uint, userPerms int, ur arr.RadarrR
}
func getRadarrRequestInfo(db *gorm.DB, requestId uint) (arr.MovieSerie, error) {
if requestId == 0 {
slog.Error("sonarr info: No request id provided")
return arr.MovieSerie{}, errors.New("no request id provided")
}
arrRequest, err := getArrRequest(db, requestId)
if err != nil {
slog.Error("radarr info: Failed to get server", "error", err)
@@ -216,6 +259,10 @@ func getRadarrRequestInfo(db *gorm.DB, requestId uint) (arr.MovieSerie, error) {
}
func getSonarrRequestInfo(db *gorm.DB, requestId uint) (arr.MovieSerie, error) {
if requestId == 0 {
slog.Error("sonarr info: No request id provided")
return arr.MovieSerie{}, errors.New("no request id provided")
}
arrRequest, err := getArrRequest(db, requestId)
if err != nil {
slog.Error("sonarr info: Failed to get server", "error", err)
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"errors"
"log/slog"
"github.com/sbondCo/Watcharr/arr"
"gorm.io/gorm"
)
// Deny an arr request
func denyArrRequest(db *gorm.DB, id uint) error {
resp := db.Model(&ArrRequest{}).Where("id = ?", id).Update("status", ARR_REQUEST_DENIED)
if resp.Error != nil {
slog.Error("denyArrRequest: Failed to update status to denied", "error", resp.Error)
return errors.New("failed when updating request status")
}
return nil
}
// Approve radarr movie
func approveRadarrRequest(db *gorm.DB, reqId uint, ur arr.RadarrRequest) (int, error) {
_, err := getArrRequest(db, reqId)
if err != nil {
slog.Error("approveRadarrRequest: Failed to get request from db", "error", err)
return 0, errors.New("failed to get request")
}
// Get server in request
server, err := getRadarr(ur.ServerName)
if err != nil {
slog.Error("approveRadarrRequest: Failed to get server", "error", err)
return 0, errors.New("failed to get server")
}
radarr := arr.New(arr.RADARR, &server.Host, &server.Key)
ur.AutomaticSearch = server.AutomaticSearch
resp, err := radarr.AddContent(radarr.BuildAddMovieBody(ur))
if err != nil {
slog.Error("approveRadarrRequest: Failed to add content", "error", err)
return 0, errors.New("failed to add content")
}
dbResp := db.Model(&ArrRequest{}).Where("id = ?", reqId).Update("arr_id", resp["id"]).Update("status", ARR_REQUEST_APPROVED)
if dbResp.Error != nil {
slog.Error("approveRadarrRequest: Failed to update request in db", "error", err)
return 0, errors.New("content was requested, but we failed to update the db")
}
arrId, ok := resp["id"].(float64)
if !ok {
slog.Error("approveRadarrRequest: Failed to cast arr id as an int", "id", resp["id"])
return 0, errors.New("content added, but failed to get arr id")
}
return int(arrId), nil
}
// Approve sonarr movie
func approveSonarrRequest(db *gorm.DB, reqId uint, ur arr.SonarrRequest) (int, error) {
_, err := getArrRequest(db, reqId)
if err != nil {
slog.Error("approveSonarrRequest: Failed to get request from db", "error", err)
return 0, errors.New("failed to get request")
}
// Get server in request
server, err := getSonarr(ur.ServerName)
if err != nil {
slog.Error("approveSonarrRequest: Failed to get server", "error", err)
return 0, errors.New("failed to get server")
}
sonarr := arr.New(arr.SONARR, &server.Host, &server.Key)
ur.AutomaticSearch = server.AutomaticSearch
resp, err := sonarr.AddContent(sonarr.BuildAddShowBody(ur))
if err != nil {
slog.Error("approveSonarrRequest: Failed to add content", "error", err)
return 0, errors.New("failed to add content")
}
dbResp := db.Model(&ArrRequest{}).Where("id = ?", reqId).Update("arr_id", resp["id"]).Update("status", ARR_REQUEST_APPROVED)
if dbResp.Error != nil {
slog.Error("approveSonarrRequest: Failed to update request in db", "error", err)
return 0, errors.New("content was requested, but we failed to update the db")
}
arrId, ok := resp["id"].(float64)
if !ok {
slog.Error("approveSonarrRequest: Failed to cast arr id as an int", "id", resp["id"])
return 0, errors.New("failed to get arr id")
}
return int(arrId), nil
}
+58
View File
@@ -1121,6 +1121,27 @@ func (b *BaseRouter) addSonarrRoutes() {
c.JSON(http.StatusOK, response)
})
s.POST("/request/approve/:id", PermRequired(PERM_ADMIN), func(c *gin.Context) {
var ur arr.SonarrRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
requestId, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("Couldn't parse request id", "request_id", requestId)
c.Status(400)
return
}
response, err := approveSonarrRequest(b.db, uint(requestId), ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.GET("/status/:serverName/:arrId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
response, err := getSonarrQueueDetails(c.Param("serverName"), c.Param("arrId"))
if err != nil {
@@ -1266,6 +1287,27 @@ func (b *BaseRouter) addRadarrRoutes() {
c.JSON(http.StatusOK, response)
})
s.POST("/request/approve/:id", PermRequired(PERM_ADMIN), func(c *gin.Context) {
var ur arr.RadarrRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
requestId, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("Couldn't parse request id", "request_id", requestId)
c.Status(400)
return
}
response, err := approveRadarrRequest(b.db, uint(requestId), ur)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
})
s.GET("/status/:serverName/:arrId", PermRequired(PERM_REQUEST_CONTENT), func(c *gin.Context) {
response, err := getRadarrQueueDetails(c.Param("serverName"), c.Param("arrId"))
if err != nil {
@@ -1311,6 +1353,22 @@ func (b *BaseRouter) addArrRequestRoutes() {
}
c.JSON(http.StatusOK, response)
})
// Deny a request (for manage_requests view), only for admins.
s.POST("/deny/:id", AdminRequired(), func(c *gin.Context) {
requestId, err := strconv.Atoi(c.Param("id"))
if err != nil {
slog.Error("Couldn't parse request id", "request_id", requestId)
c.Status(400)
return
}
err = denyArrRequest(b.db, uint(requestId))
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
})
}
func (b *BaseRouter) addJobRoutes() {
+1 -1
View File
@@ -7,7 +7,7 @@
<style lang="scss">
.loader {
width: 16px;
height: 13px;
height: 12px;
border: 2px solid #000;
border-bottom-color: transparent;
border-radius: 50%;
+17 -1
View File
@@ -3,6 +3,7 @@
ArrDetailsResponse,
ArrInfoResponse,
ArrRequestResponse,
ArrRequestStatus,
ContentType
} from "@/types";
import axios from "axios";
@@ -18,7 +19,8 @@
let existingRequest: ArrRequestResponse | undefined;
let info: ArrInfoResponse | undefined;
let status: ArrDetailsResponse | "available" | "requested" | undefined;
// The status, extra added string types are set in this file.
let status: ArrDetailsResponse | "available" | "requested" | ArrRequestStatus | undefined;
let estimatedCompletionIn: string | undefined;
async function getInfo() {
@@ -100,6 +102,12 @@
if (existingRequestResp?.data && existingRequestResp?.data?.arrId) {
existingRequest = existingRequestResp?.data;
getInfo();
} else if (existingRequestResp?.data) {
// If no arrId, use status in request (pending, denied, etc)
console.log("No arrId in request resp.. using request status for btn status if set.");
if (existingRequestResp?.data?.status) {
status = existingRequestResp?.data?.status;
}
}
} catch (err) {
console.error("ArrRequestButton: lookForExisting failed!", err);
@@ -161,6 +169,14 @@
</button>
{:else if status === "available"}
<button disabled>Available</button>
{:else if status === "PENDING"}
<button disabled>Pending</button>
{:else if status === "APPROVED"}
<button disabled>Approved</button>
{:else if status === "AUTO_APPROVED"}
<button disabled>Auto Approved</button>
{:else if status === "DENIED"}
<button disabled>Denied</button>
{:else}
<button on:click={openRequestModal}>Request</button>
{/if}
+59 -11
View File
@@ -15,6 +15,9 @@
export let content: TMDBMovieDetails;
export let onClose: (r: ArrRequestResponse | undefined) => void;
export let approveMode = false;
export let originalRequest: ArrRequestResponse | undefined = undefined;
let servarrs: RadarrSettings[];
let selectedServarrIndex: number;
let inputsDisabled = true;
@@ -32,7 +35,9 @@
notify({ text: "No servers found", type: "error" });
}
inputsDisabled = false;
processOriginalRequest();
} catch (err) {
console.error("Failed to get servers!", err);
notify({ text: "Failed to load servers", type: "error" });
}
}
@@ -43,7 +48,10 @@
const r = await axios.get<RadarrTestResponse>(`/arr/rad/config/${name}`);
selectedServerCfg = r.data;
inputsDisabled = false;
} catch (err) {}
} catch (err) {
console.error("Failed to get config!", err);
notify({ text: "Failed to load config", type: "error" });
}
}
async function request() {
@@ -71,14 +79,17 @@
notify({ id: nid, text: "No Root Folder Found", type: "error" });
return;
}
const resp = await axios.post<ArrRequestResponse>("/arr/rad/request", {
serverName: server.name,
title: content.title,
year: new Date(content.release_date)?.getFullYear(),
tmdbId: content.id,
qualityProfile: server.qualityProfile,
rootFolder: rootFolder.path
});
const resp = await axios.post<ArrRequestResponse>(
`/arr/rad/request${approveMode && originalRequest ? `/approve/${originalRequest.id}` : ""}`,
{
serverName: server.name,
title: content.title,
year: new Date(content.release_date)?.getFullYear(),
tmdbId: content.id,
qualityProfile: server.qualityProfile,
rootFolder: rootFolder.path
}
);
addRequestRunning = false;
if (resp.data) {
notify({ id: nid, text: "Request complete", type: "success" });
@@ -91,6 +102,37 @@
}
}
function processOriginalRequest() {
if (!originalRequest) {
return;
}
try {
if (originalRequest.requestJson) {
const ogr = JSON.parse(originalRequest.requestJson);
if (!ogr) {
console.info("processOriginalRequest: No json.", ogr);
return;
}
if (ogr?.serverName) {
console.debug("processOriginalRequest: restoring server name:", ogr?.serverName);
const idx = servarrs?.findIndex((s) => s.name === ogr?.serverName);
if (idx !== -1) {
selectedServarrIndex = idx;
}
}
} else {
notify({
type: "error",
text: "Full original request could not be restored. You may continue, but prefilled settings may not be true to the original request.",
time: 10000
});
}
} catch (err) {
console.error("processOriginalRequest: Failed!", err);
notify({ text: "Failed when processing original request!", type: "error" });
}
}
$: {
if (typeof selectedServarrIndex !== "undefined" && servarrs?.length > 0) {
const s = servarrs[selectedServarrIndex];
@@ -105,7 +147,11 @@
getServers();
</script>
<Modal title="Request" desc={content.title} onClose={() => onClose(undefined)}>
<Modal
title={approveMode ? "Approve Request" : "Request"}
desc={content.title}
onClose={() => onClose(undefined)}
>
<div class="req-ctr">
{#if servarrs}
{@const server = servarrs[selectedServarrIndex]}
@@ -124,7 +170,9 @@
</Setting>
{/if}
<button on:click={request} disabled={addRequestRunning}>Request</button>
<button on:click={request} disabled={addRequestRunning}>
{approveMode ? "Approve" : "Request"}
</button>
{:else}
<Spinner />
{/if}
+84 -22
View File
@@ -19,6 +19,9 @@
export let content: TMDBShowDetails;
export let onClose: (r: ArrRequestResponse | undefined) => void;
export let approveMode = false;
export let originalRequest: ArrRequestResponse | undefined = undefined;
let servarrs: SonarrSettings[];
let selectedServarrIndex: number;
let inputsDisabled = true;
@@ -43,7 +46,9 @@
notify({ text: "No servers found", type: "error" });
}
inputsDisabled = false;
processOriginalRequest();
} catch (err) {
console.error("Failed to get servers!", err);
notify({ text: "Failed to load servers", type: "error" });
}
}
@@ -54,7 +59,10 @@
const r = await axios.get<SonarrTestResponse>(`/arr/son/config/${name}`);
selectedServerCfg = r.data;
inputsDisabled = false;
} catch (err) {}
} catch (err) {
console.error("Failed to get config!", err);
notify({ text: "Failed to load config", type: "error" });
}
}
async function request() {
@@ -82,25 +90,28 @@
notify({ id: nid, text: "No Root Folder Found", type: "error" });
return;
}
const resp = await axios.post("/arr/son/request", {
serverName: server.name,
title: content.name,
year: new Date(content.first_air_date)?.getFullYear(),
tmdbId: content.id,
tvdbId: content.external_ids.tvdb_id,
seriesType: content.keywords.results?.find((k) => k.id == animeKeywordId)
? "anime"
: "standard",
qualityProfile: server.qualityProfile,
rootFolder: rootFolder.path,
languageProfile: server.languageProfile,
seasons: seasonItems.map((s) => {
return {
seasonNumber: s.id,
monitored: s.value
};
})
});
const resp = await axios.post(
`/arr/son/request${approveMode && originalRequest ? `/approve/${originalRequest.id}` : ""}`,
{
serverName: server.name,
title: content.name,
year: new Date(content.first_air_date)?.getFullYear(),
tmdbId: content.id,
tvdbId: content.external_ids.tvdb_id,
seriesType: content.keywords.results?.find((k) => k.id == animeKeywordId)
? "anime"
: "standard",
qualityProfile: server.qualityProfile,
rootFolder: rootFolder.path,
languageProfile: server.languageProfile,
seasons: seasonItems.map((s) => {
return {
seasonNumber: s.id,
monitored: s.value
};
})
}
);
addRequestRunning = false;
if (resp?.data) {
notify({ id: nid, text: "Request complete", type: "success" });
@@ -113,6 +124,51 @@
}
}
function processOriginalRequest() {
if (!originalRequest) {
return;
}
try {
if (originalRequest.requestJson) {
const ogr = JSON.parse(originalRequest.requestJson);
if (!ogr) {
console.info("processOriginalRequest: No json.", ogr);
return;
}
if (ogr?.seasons?.length > 0) {
console.debug("processOriginalRequest: Found seasons.. restoring.");
for (let i = 0; i < ogr.seasons.length; i++) {
const s = ogr.seasons[i];
// Default is not monitored, so no point going through the whole rigmarole to 'restore' the default value.
if (!s.monitored) {
continue;
}
const sItem = seasonItems?.find((si) => si.id === s.seasonNumber);
if (sItem) {
sItem.value = s.monitored;
}
}
}
if (ogr?.serverName) {
console.debug("processOriginalRequest: restoring server name:", ogr?.serverName);
const idx = servarrs?.findIndex((s) => s.name === ogr?.serverName);
if (idx !== -1) {
selectedServarrIndex = idx;
}
}
} else {
notify({
type: "error",
text: "Full original request could not be restored. You may continue, but prefilled settings may not be true to the original request.",
time: 10000
});
}
} catch (err) {
console.error("processOriginalRequest: Failed!", err);
notify({ text: "Failed when processing original request!", type: "error" });
}
}
$: {
if (typeof selectedServarrIndex !== "undefined" && servarrs?.length > 0) {
const s = servarrs[selectedServarrIndex];
@@ -127,7 +183,11 @@
getServers();
</script>
<Modal title="Request" desc={content.name} {onClose}>
<Modal
title={approveMode ? "Approve Request" : "Request"}
desc={content.name}
onClose={() => onClose(undefined)}
>
<div class="req-ctr">
{#if servarrs}
{@const server = servarrs[selectedServarrIndex]}
@@ -150,7 +210,9 @@
</Setting>
{/if}
<button on:click={request} disabled={addRequestRunning}>Request</button>
<button on:click={request} disabled={addRequestRunning}>
{approveMode ? "Approve" : "Request"}
</button>
{:else}
<Spinner />
{/if}
+119 -12
View File
@@ -1,17 +1,57 @@
<script lang="ts">
import Icon from "@/lib/Icon.svelte";
import PageError from "@/lib/PageError.svelte";
import Spinner from "@/lib/Spinner.svelte";
import RequestMovie from "@/lib/request/RequestMovie.svelte";
import RequestShow from "@/lib/request/RequestShow.svelte";
import { baseURL } from "@/lib/util/api";
import { getOrdinalSuffix, monthsShort, userHasPermission } from "@/lib/util/helpers";
import { UserPermission, type ManagedUser, type ArrRequestResponse } from "@/types";
import { notify } from "@/lib/util/notify";
import { type ArrRequestResponse, type TMDBMovieDetails, type TMDBShowDetails } from "@/types";
import axios from "axios";
let allRequests: ArrRequestResponse[];
let editingUser: ManagedUser | undefined;
let showBeingApproved: TMDBShowDetails | undefined;
let movieBeingApproved: TMDBMovieDetails | undefined;
let beingApprovedOriginalRequest: ArrRequestResponse | undefined;
async function getRequests() {
allRequests = (await axios.get(`/arr/request/`)).data as ArrRequestResponse[];
try {
allRequests = (await axios.get(`/arr/request/`)).data as ArrRequestResponse[];
if (allRequests?.length > 0) {
allRequests = allRequests?.sort((a, b) => {
if (b.status === "PENDING") return 1;
if (a.status === "PENDING") return -1;
return 0;
});
}
} catch (err) {
console.error("Failed to get requests!", err);
notify({ type: "error", text: "Failed when getting all requests!" });
}
}
async function deny(r: ArrRequestResponse) {
try {
await axios.post(`/arr/request/deny/${r.id}`);
getRequests();
} catch (err) {
console.error("Failed to deny request!", err);
notify({ type: "error", text: "Failed when denying request!" });
}
}
async function approve(r: ArrRequestResponse) {
console.debug("Approving request:", r);
if (r.content.type === "tv") {
showBeingApproved = (await axios.get(`/content/tv/${r.content.tmdbId}`))
.data as TMDBShowDetails;
} else if (r.content.type === "movie") {
movieBeingApproved = (await axios.get(`/content/movie/${r.content.tmdbId}`))
.data as TMDBMovieDetails;
} else {
notify({ type: "error", text: "Unknown content type, can't continue approval!" });
return;
}
beingApprovedOriginalRequest = r;
}
</script>
@@ -25,10 +65,13 @@
{:then}
<div class="request-container">
{#each allRequests as r}
<div class="request">
<img src={`${baseURL}/img${r.content?.poster_path}`} alt="" />
<img src={`${baseURL}/img${r.content?.poster_path}`} alt="" />
<div>
<div class={`request ${r.content.type}`}>
<div class="poster">
<img src={`${baseURL}/img${r.content?.poster_path}`} alt="" />
<span title={r.serverName}>{r.serverName}</span>
</div>
<img class="backdrop" src={`${baseURL}/img${r.content?.poster_path}`} alt="" />
<div class="wordsnstuff">
<h2 class="norm">
<span>{r.content.title}</span>
{#if r.content.release_date}
@@ -37,8 +80,19 @@
</h2>
<p>{r.content.overview}</p>
<div class="btns">
<button class="decline">Decline</button>
<button class="approve">Approve</button>
{#if r.username}
<span
style="font-size: 12px; margin-top: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>
By {r.username}
</span>
{/if}
{#if r.status === "PENDING"}
<button class="decline" on:click={() => deny(r)}>Decline</button>
<button class="approve" on:click={() => approve(r)}>Approve</button>
{:else}
<button disabled>{r.status}</button>
{/if}
</div>
</div>
</div>
@@ -47,6 +101,30 @@
{:catch err}
<PageError error={err} pretty="Failed to fetch requests!" />
{/await}
{#if showBeingApproved}
<RequestShow
content={showBeingApproved}
approveMode={true}
originalRequest={beingApprovedOriginalRequest}
onClose={() => {
showBeingApproved = undefined;
// HACK
getRequests();
}}
/>
{:else if movieBeingApproved}
<RequestMovie
content={movieBeingApproved}
approveMode={true}
originalRequest={beingApprovedOriginalRequest}
onClose={() => {
movieBeingApproved = undefined;
// HACK
getRequests();
}}
/>
{/if}
</div>
</div>
@@ -75,7 +153,7 @@
height: 225px;
border-radius: 6px;
&:first-of-type {
&.backdrop {
position: absolute;
left: 0;
top: 0;
@@ -88,10 +166,39 @@
}
}
&.tv {
.poster > span {
background-color: #35c5f4;
}
}
.poster {
position: relative;
> span {
position: absolute;
bottom: 3px;
left: 3px;
color: black;
background-color: #ffc230;
padding: 3px 5px;
font-size: 11px;
border-radius: 5px;
max-width: calc(100% - 6px);
overflow: hidden;
text-overflow: ellipsis;
}
}
& > div {
display: flex;
flex-flow: column;
&.wordsnstuff {
overflow: hidden;
width: 100%;
}
& > h2 {
font-size: 22px;
+5
View File
@@ -869,6 +869,8 @@ export interface RadarrTestResponse {
rootFolders: RootFolder[];
}
export type ArrRequestStatus = "PENDING" | "APPROVED" | "AUTO_APPROVED" | "DENIED";
export interface ArrRequestResponse {
id: number;
createdAt: Date;
@@ -876,6 +878,9 @@ export interface ArrRequestResponse {
serverName: string;
arrId: number;
content: Content;
status: ArrRequestStatus;
requestJson: string;
username: string;
}
export interface ArrDetailsResponse {