mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
wip: arr progress - request approval/denial
This commit is contained in:
+58
-11
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user