Compare commits
14 Commits
prisma-(de
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03b9b1dbb | ||
|
|
73672e29f8 | ||
|
|
cc5192209f | ||
|
|
278dcf4b44 | ||
|
|
36e092f225 | ||
|
|
46d5c737a2 | ||
|
|
cba4878db3 | ||
|
|
57cc48a699 | ||
|
|
84f488be06 | ||
|
|
f885f2a0f3 | ||
|
|
eef3e5ea4c | ||
|
|
8db821c1c1 | ||
|
|
a39b882f09 | ||
|
|
754dccc4bf |
@@ -540,11 +540,22 @@
|
||||
"code"
|
||||
]
|
||||
}
|
||||
{
|
||||
"login": "Fallenbagel",
|
||||
"name": "Mohamed Nuvaas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/98979876?s=96&v=4",
|
||||
"profile": "https://github.com/nicospz",
|
||||
"contributions": [
|
||||
"code",
|
||||
"logo",
|
||||
"design"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "overseerr",
|
||||
"projectOwner": "sct",
|
||||
"projectName": "jellyseerr",
|
||||
"projectOwner": "Fallenbagel",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
|
||||
7
.env
@@ -1,7 +0,0 @@
|
||||
# Environment variables declared in this file are automatically made available to Prisma.
|
||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (Preview) and CockroachDB (Preview).
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="postgresql://jumail:1DontGive1@localhost:5432/jellyseerr?schema=public"
|
||||
@@ -7,7 +7,6 @@ module.exports = {
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
@@ -26,7 +25,6 @@ module.exports = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'formatjs/no-offset': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
@@ -40,7 +38,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
|
||||
plugins: ['jsx-a11y', 'react-hooks', 'formatjs'],
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
|
||||
11
.github/CODEOWNERS
vendored
@@ -1,12 +1,7 @@
|
||||
# Global code ownership
|
||||
* @sct
|
||||
|
||||
# Documentation
|
||||
docs/ @TheCatLady @samwiseg0
|
||||
|
||||
# Snap-related files
|
||||
.github/workflows/snap.yaml @samwiseg0
|
||||
snap/ @samwiseg0
|
||||
- @Fallenbagel
|
||||
|
||||
# i18n locale files
|
||||
src/i18n/locale/ @sct @TheCatLady
|
||||
|
||||
src/i18n/locale/ @Fallenbagel
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,10 +4,4 @@
|
||||
|
||||
#### To-Dos
|
||||
|
||||
- [ ] Successful build `yarn build`
|
||||
- [ ] Translation keys `yarn i18n:extract`
|
||||
- [ ] Database migration (if required)
|
||||
|
||||
#### Issues Fixed or Closed
|
||||
|
||||
- Fixes #XXXX
|
||||
|
||||
4
.husky/commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS
|
||||
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm test
|
||||
4
.husky/prepare-commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
exec < /dev/tty && git cz --hook || true
|
||||
17
README.md
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
|
||||
<img src="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/stable/public/logo.png" alt="Overseerr" style="margin: 20px 0;">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/BHak4GCk"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
**Jellyseerr** is a free and open source fork of Overseerr for managing requests for your media library. It integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**, and **[Jellyfin](https://jellyfin.org/)**!
|
||||
@@ -22,13 +22,13 @@ Check out our [issue tracker](https://github.com/Fallenbagel/jellyseerr/issues).
|
||||
|
||||
## Supported Architectures
|
||||
|
||||
Jellyseerr image support multiple architectures such as x86-64, arm64 and armv7.
|
||||
Jellyseerr image support multiple architectures such as x86-64, arm64 and armv7.
|
||||
|
||||
| **Architecture** | **Tag** |
|
||||
|------------------|---------|
|
||||
| x86-64 | latest |
|
||||
| ARM64 | arm |
|
||||
| ARMv7 | arm |
|
||||
| ---------------- | ------- |
|
||||
| x86-64 | latest |
|
||||
| ARM64 | arm |
|
||||
| ARMv7 | armv7 |
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -37,13 +37,14 @@ https://hub.docker.com/r/fallenbagel/jellyseerr
|
||||
|
||||
## Support
|
||||
|
||||
- You can get support on [Discord](https://discord.gg/VpVnZ92yQK).
|
||||
- You can get support on [Discord](https://discord.gg/ckbvBtDJgC).
|
||||
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## Buy me a Coffee!
|
||||
|
||||
If you like jellyseerr and want to help maintain it, please buy me a coffee as it would help me out a lot!
|
||||
|
||||
[](https://www.buymeacoffee.com/fallen.bagel)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@prisma/client": "^3.11.1",
|
||||
"@supercharge/request-ip": "^1.1.2",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@tanem/react-nprogress": "^3.0.79",
|
||||
@@ -47,7 +46,6 @@
|
||||
"nodemailer": "^6.6.3",
|
||||
"openpgp": "^5.0.0-3",
|
||||
"plex-api": "^5.3.1",
|
||||
"prisma": "^3.11.1",
|
||||
"pug": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-ace": "^9.3.0",
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "media" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"mediaType" TEXT NOT NULL,
|
||||
"tmdbId" INTEGER NOT NULL,
|
||||
"tvdbId" INTEGER,
|
||||
"imdbId" TEXT,
|
||||
"status" INTEGER NOT NULL DEFAULT 1,
|
||||
"status4k" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastSeasonChange" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"mediaAddedAt" TIMESTAMP(3),
|
||||
"serviceId" INTEGER,
|
||||
"serviceId4k" INTEGER,
|
||||
"externalServiceId" INTEGER,
|
||||
"externalServiceId4k" INTEGER,
|
||||
"externalServiceSlug" TEXT,
|
||||
"externalServiceSlug4k" TEXT,
|
||||
"ratingKey" TEXT,
|
||||
"ratingKey4k" TEXT,
|
||||
"jellyfinMediaId" TEXT,
|
||||
"jellyfinMediaId4k" TEXT,
|
||||
|
||||
CONSTRAINT "media_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "media_request" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"status" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"type" TEXT NOT NULL,
|
||||
"mediaId" INTEGER,
|
||||
"requestedById" INTEGER,
|
||||
"modifiedById" INTEGER,
|
||||
"is4k" BOOLEAN NOT NULL DEFAULT false,
|
||||
"serverId" INTEGER,
|
||||
"profileId" INTEGER,
|
||||
"rootFolder" TEXT,
|
||||
"languageProfileId" INTEGER,
|
||||
|
||||
CONSTRAINT "media_request_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "migrations" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"timestamp" BIGINT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "migrations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "season" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"seasonNumber" INTEGER NOT NULL,
|
||||
"status" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"mediaId" INTEGER,
|
||||
"status4k" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CONSTRAINT "season_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "season_request" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"seasonNumber" INTEGER NOT NULL,
|
||||
"status" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"requestId" INTEGER,
|
||||
|
||||
CONSTRAINT "season_request_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"expiredAt" BIGINT NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"json" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"username" TEXT,
|
||||
"plexId" INTEGER,
|
||||
"plexToken" TEXT,
|
||||
"permissions" INTEGER NOT NULL DEFAULT 0,
|
||||
"avatar" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"password" TEXT,
|
||||
"userType" INTEGER NOT NULL DEFAULT 1,
|
||||
"plexUsername" TEXT,
|
||||
"resetPasswordGuid" TEXT,
|
||||
"recoveryLinkExpirationDate" TIMESTAMP(3),
|
||||
"jellyfinUsername" TEXT,
|
||||
"jellyfinAuthToken" TEXT,
|
||||
"jellyfinUserId" TEXT,
|
||||
"jellyfinDeviceId" TEXT,
|
||||
|
||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_settings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"enableNotifications" BOOLEAN NOT NULL DEFAULT true,
|
||||
"discordId" TEXT,
|
||||
"userId" INTEGER,
|
||||
"region" TEXT,
|
||||
"originalLanguage" TEXT,
|
||||
|
||||
CONSTRAINT "user_settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sqlite_autoindex_media_1" ON "media"("tvdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media"("imdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media"("tvdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media"("tmdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session"("expiredAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sqlite_autoindex_user_1" ON "user"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sqlite_autoindex_user_settings_1" ON "user_settings"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_modifiedById_fkey" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "season" ADD CONSTRAINT "season_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "season_request" ADD CONSTRAINT "season_request_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,127 +0,0 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgres"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model media {
|
||||
id Int @id @default(autoincrement())
|
||||
mediaType String
|
||||
tmdbId Int
|
||||
tvdbId Int? @unique(map: "sqlite_autoindex_media_1")
|
||||
imdbId String?
|
||||
status Int @default(1)
|
||||
status4k Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
lastSeasonChange DateTime @default(now())
|
||||
mediaAddedAt DateTime?
|
||||
serviceId Int?
|
||||
serviceId4k Int?
|
||||
externalServiceId Int?
|
||||
externalServiceId4k Int?
|
||||
externalServiceSlug String?
|
||||
externalServiceSlug4k String?
|
||||
ratingKey String?
|
||||
ratingKey4k String?
|
||||
jellyfinMediaId String?
|
||||
jellyfinMediaId4k String?
|
||||
media_request media_request[]
|
||||
season season[]
|
||||
|
||||
@@index([imdbId], map: "IDX_7ff2d11f6a83cb52386eaebe74")
|
||||
@@index([tvdbId], map: "IDX_41a289eb1fa489c1bc6f38d9c3")
|
||||
@@index([tmdbId], map: "IDX_7157aad07c73f6a6ae3bbd5ef5")
|
||||
}
|
||||
|
||||
model media_request {
|
||||
id Int @id @default(autoincrement())
|
||||
status Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
type String
|
||||
mediaId Int?
|
||||
requestedById Int?
|
||||
modifiedById Int?
|
||||
is4k Boolean @default(false)
|
||||
serverId Int?
|
||||
profileId Int?
|
||||
rootFolder String?
|
||||
languageProfileId Int?
|
||||
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
user_media_request_modifiedByIdTouser user? @relation("media_request_modifiedByIdTouser", fields: [modifiedById], references: [id], onUpdate: NoAction)
|
||||
user_media_request_requestedByIdTouser user? @relation("media_request_requestedByIdTouser", fields: [requestedById], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
season_request season_request[]
|
||||
}
|
||||
|
||||
model migrations {
|
||||
id Int @id @default(autoincrement())
|
||||
timestamp BigInt
|
||||
name String
|
||||
}
|
||||
|
||||
model season {
|
||||
id Int @id @default(autoincrement())
|
||||
seasonNumber Int
|
||||
status Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
mediaId Int?
|
||||
status4k Int @default(1)
|
||||
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model season_request {
|
||||
id Int @id @default(autoincrement())
|
||||
seasonNumber Int
|
||||
status Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
requestId Int?
|
||||
media_request media_request? @relation(fields: [requestId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model session {
|
||||
expiredAt BigInt
|
||||
id String @id
|
||||
json String
|
||||
|
||||
@@index([expiredAt], map: "IDX_28c5d1d16da7908c97c9bc2f74")
|
||||
}
|
||||
|
||||
model user {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique(map: "sqlite_autoindex_user_1")
|
||||
username String?
|
||||
plexId Int?
|
||||
plexToken String?
|
||||
permissions Int @default(0)
|
||||
avatar String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now())
|
||||
password String?
|
||||
userType Int @default(1)
|
||||
plexUsername String?
|
||||
resetPasswordGuid String?
|
||||
recoveryLinkExpirationDate DateTime?
|
||||
jellyfinUsername String?
|
||||
jellyfinAuthToken String?
|
||||
jellyfinUserId String?
|
||||
jellyfinDeviceId String?
|
||||
media_request_media_request_modifiedByIdTouser media_request[] @relation("media_request_modifiedByIdTouser")
|
||||
media_request_media_request_requestedByIdTouser media_request[] @relation("media_request_requestedByIdTouser")
|
||||
user_settings user_settings?
|
||||
}
|
||||
|
||||
model user_settings {
|
||||
id Int @id @default(autoincrement())
|
||||
enableNotifications Boolean @default(true)
|
||||
discordId String?
|
||||
userId Int? @unique(map: "sqlite_autoindex_user_settings_1")
|
||||
region String?
|
||||
originalLanguage String?
|
||||
user user? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -1 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.2;fill-rule:evenodd;clip-rule:evenodd;fill:#131928;enable-background:new ;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);}
|
||||
.st2{fill:#000B25;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:url(#SVGID_00000095336865710271825490000009683653333454385338_);}
|
||||
.st5{fill-rule:evenodd;clip-rule:evenodd;fill:#1D1D1B;}
|
||||
</style>
|
||||
<path class="st0" d="M80,52c0,15.5-12.5,28-28,28c-15.1,0-27.5-12-28-27c0,0.3,0,0.7,0,1c0,16.6,13.4,30,30,30s30-13.4,30-30
|
||||
S70.6,24,54,24c-0.3,0-0.7,0-1,0C68,24.5,80,36.9,80,52z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="28" y1="90" x2="28" y2="50" gradientTransform="matrix(1 0 0 -1 0 98)">
|
||||
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.4"/>
|
||||
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M48,12c-19.9,0-36,16.1-36,36c0,2.2-1.8,4-4,4c-2.2,0-4-1.8-4-4C4,23.7,23.7,4,48,4c2.2,0,4,1.8,4,4
|
||||
C52,10.2,50.2,12,48,12z"/>
|
||||
<ellipse class="st2" cx="48" cy="48" rx="48" ry="46.1"/>
|
||||
<rect x="34.8" y="21.7" class="st3" width="12.9" height="5.3"/>
|
||||
<linearGradient id="SVGID_00000124123951135562400370000011621398912477202562_" gradientUnits="userSpaceOnUse" x1="-178.0748" y1="163.4019" x2="-111.501" y2="124.9661" gradientTransform="matrix(1 0 0 -1 194 202)">
|
||||
<stop offset="0" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0" style="stop-color:#A85DC3"/>
|
||||
<stop offset="0.15" style="stop-color:#9863C5"/>
|
||||
<stop offset="0.43" style="stop-color:#6F74CB"/>
|
||||
<stop offset="0.83" style="stop-color:#2D90D5"/>
|
||||
<stop offset="1" style="stop-color:#0F9DDA"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000124123951135562400370000011621398912477202562_);" d="M82.4,51.4C82,33.8,65.4,17.2,48.2,17
|
||||
C30.8,16.8,13.9,33,13,50.5c-0.4,8.2,0.2,9,8.5,10.4c0.9,9.2-0.5,14.4-6.2,20.8c0.8,0.7,1.5,1.3,2.4,2c0.2-0.2,0.4-0.4,0.5-0.7
|
||||
c5.1-7,7.6-9.9,7-18c-0.4-5.4,1.1-7.9,6.9-7.6c0.9,10.8-1.4,16.1-5.9,26.2c-0.7,1.5-1.4,2.9-2.3,4.2c1.1,0.6,2.2,1.2,3.3,1.7
|
||||
c1.2-2,2.1-4.2,2.8-6.6c2.6-9.2,4.8-13.8,5.7-23.4c0.3-3.7,1.4-5.1,5.5-4.2c1.4,11.7-1.2,17.9-4.8,29.2c-0.8,2.6-1.8,5-3,7.3
|
||||
c1.1,0.3,2.2,0.6,3.3,0.9c0.7-1.4,1.4-2.9,2-4.4c4.4-11.3,7.1-17.9,6.3-29.9c-0.3-4.4,1.4-5.2,4.8-3.9c0.5,8.6-1.1,11.6-0.1,19.9
|
||||
c0.8,7.2,2.8,13.5,5.7,19c1.1-0.2,2.1-0.4,3.1-0.6c-5.1-10.1-4.9-22.8-4.7-37.5c4.4-0.9,5.6,0.3,5.8,4.1c0.5,8.5-0.7,11.7,1,20
|
||||
c0.9,4.2,2.5,8,4.9,11.3c1.1-0.4,2.3-0.9,3.4-1.4c-7.3-8.5-6-18.8-5.9-32.2c7.1,1.4,6.5,1.5,7.1,7.5c0.8,7.6,1.3,8.6,3.3,16
|
||||
c0.3,0.9,1.3,2.7,2.5,4.6c1-0.7,1.9-1.4,2.8-2.1c-5.2-10.7-6.4-15-4.5-22.7C81.9,59.8,82.6,59,82.4,51.4L82.4,51.4z M19.6,51.1
|
||||
C17.6,42.4,25.1,29,32,28C28,35.4,23.8,43.3,19.6,51.1L19.6,51.1z M35.4,27.2c-0.2-0.9-0.5-1.8-0.7-2.7c4.1-1,8.2-2,12.3-3l0.7,2.7
|
||||
C43.5,25.2,39.4,26.2,35.4,27.2z"/>
|
||||
<path class="st3" d="M19.6,51.1C17.6,42.4,25.1,29,32,28C28,35.4,23.8,43.3,19.6,51.1L19.6,51.1z"/>
|
||||
<path class="st5" d="M48,30.2c-1.6,0-3.1,0.5-4.3,1.4c0.2,0,0.4,0,0.5,0c2.1,0,3.8,1.7,3.8,3.8S46.3,39,44.2,39
|
||||
c-1.7,0-3.2-1.1-3.6-2.7c-0.1,0.4-0.1,0.9-0.1,1.4c0,4.1,3.4,7.5,7.5,7.5s7.5-3.4,7.5-7.5S52.1,30.2,48,30.2L48,30.2z"/>
|
||||
<circle class="st3" cx="44.3" cy="35.1" r="3.7"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 1.7 MiB |
3
public/preview.jpg:Zone.Identifier
Normal file
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
|
||||
ZoneId=3
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Overseerr",
|
||||
"short_name": "Overseerr",
|
||||
"name": "Jellyseerr",
|
||||
"short_name": "Jellyseerr",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -83,7 +83,7 @@ class GithubAPI extends ExternalAPI {
|
||||
} = {}): Promise<GitHubRelease[]> {
|
||||
try {
|
||||
const data = await this.get<GitHubRelease[]>(
|
||||
'/repos/sct/overseerr/releases',
|
||||
'/repos/Fallenbagel/jellyseerr/releases',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
@@ -110,7 +110,7 @@ class GithubAPI extends ExternalAPI {
|
||||
} = {}): Promise<GithubCommit[]> {
|
||||
try {
|
||||
const data = await this.get<GithubCommit[]>(
|
||||
'/repos/sct/overseerr/commits',
|
||||
'/repos/Fallenbagel/jellyseerr/commits',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
@@ -122,7 +122,7 @@ class GithubAPI extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
|
||||
{ label: 'GitHub API', errorMessage: e.message }
|
||||
);
|
||||
return [];
|
||||
|
||||
@@ -81,9 +81,9 @@ class JellyfinAPI {
|
||||
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
} else {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
}
|
||||
|
||||
this.axios = axios.create({
|
||||
|
||||
@@ -122,9 +122,9 @@ class PlexAPI {
|
||||
// },
|
||||
options: {
|
||||
identifier: settings.clientId,
|
||||
product: 'Overseerr',
|
||||
deviceName: 'Overseerr',
|
||||
platform: 'Overseerr',
|
||||
product: 'Jellyseerr',
|
||||
deviceName: 'Jellyseerr',
|
||||
platform: 'Jellyseerr',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import csurf from 'csurf';
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
import session, { Store } from 'express-session';
|
||||
import next from 'next';
|
||||
import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { createConnection, getRepository } from 'typeorm';
|
||||
import YAML from 'yamljs';
|
||||
import PlexAPI from './api/plexapi';
|
||||
import { Session } from './entity/Session';
|
||||
import { User } from './entity/User';
|
||||
import { startJobs } from './job/schedule';
|
||||
import notificationManager from './lib/notifications';
|
||||
import DiscordAgent from './lib/notifications/agents/discord';
|
||||
@@ -27,21 +31,22 @@ import { getAppVersion } from './utils/appVersion';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
app
|
||||
.prepare()
|
||||
.then(async () => {
|
||||
// // Run migrations in production
|
||||
// if (process.env.NODE_ENV === 'production') {
|
||||
// await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||
// await dbConnection.runMigrations();
|
||||
// await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||
// }
|
||||
const dbConnection = await createConnection();
|
||||
|
||||
// Run migrations in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||
await dbConnection.runMigrations();
|
||||
await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// Load Settings
|
||||
const settings = getSettings().load();
|
||||
@@ -51,23 +56,14 @@ app
|
||||
settings.plex.libraries.length > 1 &&
|
||||
!settings.plex.libraries[0].type
|
||||
) {
|
||||
const admin = await prisma.user.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
plexToken: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
// const userRepository = getRepository(User);
|
||||
// const admin = await userRepository.findOne({
|
||||
// select: ['id', 'plexToken'],
|
||||
// order: { id: 'ASC' },
|
||||
// });
|
||||
|
||||
if (admin) {
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken! });
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
||||
await plexapi.syncLibraries();
|
||||
logger.info('Migrating libraries to include media type', {
|
||||
label: 'Settings',
|
||||
@@ -133,24 +129,22 @@ app
|
||||
}
|
||||
|
||||
// Set up sessions
|
||||
// const sessionRespository = getRepository(Session);
|
||||
// const sessionRespository = await prisma.session.findMany();
|
||||
|
||||
// server.use(
|
||||
// '/api',
|
||||
// session({
|
||||
// secret: settings.clientId,
|
||||
// resave: false,
|
||||
// saveUninitialized: false,
|
||||
// cookie: {
|
||||
// maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||
// },
|
||||
// store: new TypeormStore({
|
||||
// cleanupLimit: 2,
|
||||
// ttl: 1000 * 60 * 60 * 24 * 30,
|
||||
// }).connect(sessionRespository) as Store,
|
||||
// })
|
||||
// );
|
||||
const sessionRespository = getRepository(Session);
|
||||
server.use(
|
||||
'/api',
|
||||
session({
|
||||
secret: settings.clientId,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||
},
|
||||
store: new TypeormStore({
|
||||
cleanupLimit: 2,
|
||||
ttl: 1000 * 60 * 60 * 24 * 30,
|
||||
}).connect(sessionRespository) as Store,
|
||||
})
|
||||
);
|
||||
const apiDocs = YAML.load(API_SPEC_PATH);
|
||||
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
|
||||
server.use(
|
||||
|
||||
@@ -43,7 +43,8 @@ interface SlackBlockEmbed {
|
||||
|
||||
class SlackAgent
|
||||
extends BaseAgent<NotificationAgentSlack>
|
||||
implements NotificationAgent {
|
||||
implements NotificationAgent
|
||||
{
|
||||
protected getSettings(): NotificationAgentSlack {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
@@ -202,7 +203,7 @@ class SlackAgent
|
||||
action_id: 'button-action',
|
||||
type: 'button',
|
||||
url: actionUrl,
|
||||
value: 'open_overseerr',
|
||||
value: 'open_jellyseerr',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `Open in ${settings.main.applicationTitle}`,
|
||||
|
||||
@@ -254,7 +254,7 @@ class Settings {
|
||||
vapidPublic: '',
|
||||
main: {
|
||||
apiKey: '',
|
||||
applicationTitle: 'Overseerr',
|
||||
applicationTitle: 'Jellyseerr',
|
||||
applicationUrl: '',
|
||||
csrfProtection: false,
|
||||
cacheImages: false,
|
||||
@@ -303,7 +303,7 @@ class Settings {
|
||||
ignoreTls: false,
|
||||
requireTls: false,
|
||||
allowSelfSigned: false,
|
||||
senderName: 'Overseerr',
|
||||
senderName: 'Jellyseerr',
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Migrate away from old log
|
||||
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/overseerr.log');
|
||||
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/Jellyseerr.log');
|
||||
if (fs.existsSync(OLD_LOG_FILE)) {
|
||||
const file = fs.lstatSync(OLD_LOG_FILE);
|
||||
|
||||
@@ -43,14 +43,14 @@ const logger = winston.createLogger({
|
||||
}),
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
|
||||
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/Jellyseerr-%DATE%.log`
|
||||
: path.join(__dirname, '../config/logs/Jellyseerr-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '7d',
|
||||
createSymlink: true,
|
||||
symlinkName: 'overseerr.log',
|
||||
symlinkName: 'Jellyseerr.log',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -5,10 +5,10 @@ export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "user"`
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
@@ -17,10 +17,10 @@ export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "temporary_user"`
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
if (!user) {
|
||||
if (!settings.main.newPlexLogin) {
|
||||
logger.info(
|
||||
'Failed sign-in attempt from user who has not been imported to Overseerr.',
|
||||
'Failed sign-in attempt from user who has not been imported to Jellyseerr.',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
@@ -214,7 +214,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
if (user) {
|
||||
deviceId = user.jellyfinDeviceId ?? '';
|
||||
} else {
|
||||
deviceId = Buffer.from(`BOT_overseerr_${body.username ?? ''}`).toString(
|
||||
deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString(
|
||||
'base64'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ router.get('/genres/tv', isAuthenticated(), async (req, res) => {
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
return res.status(200).json({
|
||||
api: 'Overseerr API',
|
||||
api: 'Jellyseerr API',
|
||||
version: '1.0',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -325,8 +325,8 @@ settingsRoutes.get(
|
||||
}
|
||||
|
||||
const logFile = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr.log`
|
||||
: path.join(__dirname, '../../../config/logs/overseerr.log');
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/jellyseerr.log`
|
||||
: path.join(__dirname, '../../../config/logs/jellyseerr.log');
|
||||
const logs: LogMessage[] = [];
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
name: overseerr
|
||||
adopt-info: overseerr
|
||||
name: jellyseerr
|
||||
adopt-info: jellyseerr
|
||||
license: MIT
|
||||
summary: Request management and media discovery tool for the Plex ecosystem.
|
||||
description: >
|
||||
Overseerr is a free and open source software application for managing requests for your media library.
|
||||
Jellyseerr is a free and open source software application for managing requests for your media library.
|
||||
It integrates with your existing services such as Sonarr, Radarr and Plex!
|
||||
base: core18
|
||||
confinement: strict
|
||||
|
||||
parts:
|
||||
overseerr:
|
||||
jellyseerr:
|
||||
plugin: nodejs
|
||||
nodejs-version: '14.17.0'
|
||||
nodejs-package-manager: 'yarn'
|
||||
@@ -88,7 +88,7 @@ apps:
|
||||
- network-bind
|
||||
environment:
|
||||
PATH: '$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH'
|
||||
OVERSEERR_SNAP: 'True'
|
||||
JELLYSEERR_SNAP: 'True'
|
||||
CONFIG_DIRECTORY: $SNAP_USER_COMMON
|
||||
LOG_LEVEL: 'debug'
|
||||
NODE_ENV: 'production'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ArrowCircleUpIcon,
|
||||
BeakerIcon,
|
||||
// BeakerIcon,
|
||||
CodeIcon,
|
||||
ServerIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
@@ -11,8 +11,8 @@ import useSWR from 'swr';
|
||||
import { StatusResponse } from '../../../../server/interfaces/api/settingsInterfaces';
|
||||
|
||||
const messages = defineMessages({
|
||||
streamdevelop: 'Overseerr Develop',
|
||||
streamstable: 'Overseerr Stable',
|
||||
streamdevelop: 'Jellyseerr Develop',
|
||||
streamstable: 'Jellyseerr Stable',
|
||||
outofdate: 'Out of Date',
|
||||
commitsbehind:
|
||||
'{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind',
|
||||
@@ -36,7 +36,7 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
|
||||
data.commitTag === 'local'
|
||||
? 'Keep it up! 👍'
|
||||
: data.version.startsWith('develop-')
|
||||
? intl.formatMessage(messages.streamdevelop)
|
||||
? intl.formatMessage(messages.streamstable)
|
||||
: intl.formatMessage(messages.streamstable);
|
||||
|
||||
return (
|
||||
@@ -52,14 +52,16 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
|
||||
tabIndex={0}
|
||||
className={`flex items-center p-2 mx-2 text-xs transition duration-300 rounded-lg ring-1 ring-gray-700 ${
|
||||
data.updateAvailable
|
||||
? 'bg-yellow-500 text-white hover:bg-yellow-400'
|
||||
? // ? 'bg-yellow-500 text-white hover:bg-yellow-400'
|
||||
'bg-gray-900 text-gray-300 hover:bg-gray-800'
|
||||
: 'bg-gray-900 text-gray-300 hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{data.commitTag === 'local' ? (
|
||||
<CodeIcon className="w-6 h-6" />
|
||||
) : data.version.startsWith('develop-') ? (
|
||||
<BeakerIcon className="w-6 h-6" />
|
||||
// <BeakerIcon className="w-6 h-6" />
|
||||
<CodeIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<ServerIcon className="w-6 h-6" />
|
||||
)}
|
||||
@@ -72,7 +74,8 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
|
||||
intl.formatMessage(messages.commitsbehind, {
|
||||
commitsBehind: data.commitsBehind,
|
||||
})
|
||||
) : data.commitsBehind === -1 ? (
|
||||
) : // ) : data.commitsBehind === -1 ? (
|
||||
data.commitsBehind === 0 ? (
|
||||
intl.formatMessage(messages.outofdate)
|
||||
) : (
|
||||
<code className="p-0 bg-transparent">
|
||||
|
||||
@@ -164,10 +164,13 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
|
||||
href="/site.webmanifest"
|
||||
crossOrigin="use-credentials"
|
||||
/>
|
||||
<meta name="application-name" content={applicationTitle ?? 'Overseerr'} />
|
||||
<meta
|
||||
name="application-name"
|
||||
content={applicationTitle ?? 'Jellyseerr'}
|
||||
/>
|
||||
<meta
|
||||
name="apple-mobile-web-app-title"
|
||||
content={applicationTitle ?? 'Overseerr'}
|
||||
content={applicationTitle ?? 'Jellyseerr'}
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@@ -9,13 +9,13 @@ export const messages = defineMessages({
|
||||
'Full administrator access. Bypasses all other permission checks.',
|
||||
users: 'Manage Users',
|
||||
usersDescription:
|
||||
'Grant permission to manage Overseerr users. Users with this permission cannot modify users with or grant the Admin privilege.',
|
||||
'Grant permission to manage Jellyseerr users. Users with this permission cannot modify users with or grant the Admin privilege.',
|
||||
settings: 'Manage Settings',
|
||||
settingsDescription:
|
||||
'Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.',
|
||||
'Grant permission to modify Jellyseerr settings. A user must have this permission to grant it to others.',
|
||||
managerequests: 'Manage Requests',
|
||||
managerequestsDescription:
|
||||
'Grant permission to manage Overseerr requests. All requests made by a user with this permission will be automatically approved.',
|
||||
'Grant permission to manage Jellyseerr requests. All requests made by a user with this permission will be automatically approved.',
|
||||
request: 'Request',
|
||||
requestDescription: 'Grant permission to request non-4K media.',
|
||||
requestMovies: 'Request Movies',
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||
import type { TvDetails } from '../../../server/models/Tv';
|
||||
import type { MovieDetails } from '../../../server/models/Movie';
|
||||
import useSWR from 'swr';
|
||||
import { LanguageContext } from '../../context/LanguageContext';
|
||||
import { MediaRequestStatus } from '../../../server/constants/media';
|
||||
import Badge from '../Common/Badge';
|
||||
import { useUser, Permission } from '../../hooks/useUser';
|
||||
import {
|
||||
CheckIcon,
|
||||
PencilIcon,
|
||||
RefreshIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import axios from 'axios';
|
||||
import Button from '../Common/Button';
|
||||
import { withProperties } from '../../utils/typeHelpers';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
} from '../../../server/constants/media';
|
||||
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '../../../server/models/Movie';
|
||||
import type { TvDetails } from '../../../server/models/Tv';
|
||||
import { Permission, useUser } from '../../hooks/useUser';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import { withProperties } from '../../utils/typeHelpers';
|
||||
import Badge from '../Common/Badge';
|
||||
import Button from '../Common/Button';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
import RequestModal from '../RequestModal';
|
||||
import StatusBadge from '../StatusBadge';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: 'Seasons',
|
||||
all: 'All',
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
failedretry: 'Something went wrong while retrying the request.',
|
||||
mediaerror: 'The associated title for this request is no longer available.',
|
||||
deleterequest: 'Delete Request',
|
||||
});
|
||||
|
||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
@@ -27,7 +41,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
|
||||
const RequestCardPlaceholder: React.FC = () => {
|
||||
return (
|
||||
<div className="relative p-4 bg-gray-700 rounded-lg w-72 sm:w-96 animate-pulse">
|
||||
<div className="relative w-72 animate-pulse rounded-xl bg-gray-700 p-4 sm:w-96">
|
||||
<div className="w-20 sm:w-28">
|
||||
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
||||
</div>
|
||||
@@ -35,6 +49,45 @@ const RequestCardPlaceholder: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface RequestCardErrorProps {
|
||||
mediaId?: number;
|
||||
}
|
||||
|
||||
const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
|
||||
const { hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-72 rounded-xl bg-gray-800 p-4 ring-1 ring-red-500 sm:w-96">
|
||||
<div className="w-20 sm:w-28">
|
||||
<div className="w-full" style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center px-10">
|
||||
<div className="w-full whitespace-normal text-center text-xs text-gray-300 sm:text-sm">
|
||||
{intl.formatMessage(messages.mediaerror)}
|
||||
</div>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
className="mt-4"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RequestCardProps {
|
||||
request: MediaRequest;
|
||||
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
|
||||
@@ -45,19 +98,21 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
triggerOnce: true,
|
||||
});
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const { user, hasPermission } = useUser();
|
||||
const { addToast } = useToasts();
|
||||
const [isRetrying, setRetrying] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const url =
|
||||
request.type === 'movie'
|
||||
? `/api/v1/movie/${request.media.tmdbId}`
|
||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||
inView ? `${url}?language=${locale}` : null
|
||||
inView ? `${url}` : null
|
||||
);
|
||||
const {
|
||||
data: requestData,
|
||||
error: requestError,
|
||||
revalidate,
|
||||
mutate: revalidate,
|
||||
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||
initialData: request,
|
||||
});
|
||||
@@ -70,6 +125,30 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
};
|
||||
|
||||
const retryRequest = async () => {
|
||||
setRetrying(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||
|
||||
if (response) {
|
||||
revalidate();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.failedretry), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
setRetrying(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (title && onTitleData) {
|
||||
onTitleData(request.id, title);
|
||||
@@ -85,157 +164,251 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
}
|
||||
|
||||
if (!requestData && !requestError) {
|
||||
return <RequestCardPlaceholder />;
|
||||
return <RequestCardError />;
|
||||
}
|
||||
|
||||
if (!title || !requestData) {
|
||||
return <RequestCardPlaceholder />;
|
||||
return <RequestCardError mediaId={requestData?.media.id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex p-4 text-gray-400 bg-gray-800 bg-center bg-cover rounded-md w-72 sm:w-96"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 pr-4">
|
||||
<h2 className="overflow-hidden text-base text-white cursor-pointer sm:text-lg overflow-ellipsis whitespace-nowrap hover:underline">
|
||||
<Link
|
||||
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
|
||||
as={
|
||||
request.type === 'movie'
|
||||
? `/movie/${request.media.tmdbId}`
|
||||
: `/tv/${request.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</Link>
|
||||
</h2>
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="flex items-center group">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
<>
|
||||
<RequestModal
|
||||
show={showEditModal}
|
||||
tmdbId={request.media.tmdbId}
|
||||
type={request.type}
|
||||
is4k={request.is4k}
|
||||
editRequest={request}
|
||||
onCancel={() => setShowEditModal(false)}
|
||||
onComplete={() => {
|
||||
revalidate();
|
||||
setShowEditModal(false);
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex w-72 overflow-hidden rounded-xl bg-gray-800 bg-cover bg-center p-4 text-gray-400 shadow ring-1 ring-gray-700 sm:w-96">
|
||||
{title.backdropPath && (
|
||||
<div className="absolute inset-0 z-0">
|
||||
<CachedImage
|
||||
alt=""
|
||||
className="w-4 mr-1 rounded-full sm:mr-2 sm:w-5"
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<span className="text-xs truncate sm:text-sm group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
{requestData.media.status && (
|
||||
<div className="mt-1 sm:mt-2">
|
||||
<StatusBadge
|
||||
status={
|
||||
requestData.is4k
|
||||
? requestData.media.status4k
|
||||
: requestData.media.status
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{request.seasons.length > 0 && (
|
||||
<div className="items-center hidden mt-2 text-sm sm:flex">
|
||||
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
|
||||
{!isMovie(title) &&
|
||||
title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length ? (
|
||||
<span className="mr-2 uppercase">
|
||||
<Badge>{intl.formatMessage(messages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="overflow-x-scroll hide-scrollbar">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative z-10 flex min-w-0 flex-1 flex-col pr-4">
|
||||
<div className="hidden text-xs font-medium text-white sm:flex">
|
||||
{(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice(
|
||||
0,
|
||||
4
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="flex items-end flex-1">
|
||||
<span className="mr-2">
|
||||
<Link
|
||||
href={
|
||||
request.type === 'movie'
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
</Link>
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) && (
|
||||
<div className="card-field">
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm"
|
||||
/>
|
||||
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{!isMovie(title) && request.seasons.length > 0 && (
|
||||
<div className="my-0.5 hidden items-center text-sm sm:my-1 sm:flex">
|
||||
<span className="mr-2 font-bold ">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount:
|
||||
title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
</span>
|
||||
{title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length ? (
|
||||
<span className="mr-2 uppercase">
|
||||
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="hide-scrollbar overflow-x-scroll">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex items-center text-sm sm:mt-1">
|
||||
<span className="mr-2 hidden font-bold sm:block">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</span>
|
||||
{requestData.status === MediaRequestStatus.DECLINED ? (
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.declined)}
|
||||
</Badge>
|
||||
) : requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN ? (
|
||||
<Badge
|
||||
badgeType="danger"
|
||||
//href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
>
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={
|
||||
requestData.media[requestData.is4k ? 'status4k' : 'status']
|
||||
}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.media[
|
||||
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 items-end space-x-2">
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN &&
|
||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="success"
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
disabled={isRetrying}
|
||||
onClick={() => retryRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.approve)}
|
||||
<RefreshIcon
|
||||
className={isRetrying ? 'animate-spin' : ''}
|
||||
style={{ marginRight: '0', animationDirection: 'reverse' }}
|
||||
/>
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.retry)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<>
|
||||
<Button
|
||||
buttonType="success"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
>
|
||||
<CheckIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.approve)}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
>
|
||||
<XIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.decline)}
|
||||
</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
requestData.requestedBy.id === user?.id &&
|
||||
(requestData.type === 'tv' ||
|
||||
hasPermission(Permission.REQUEST_ADVANCED)) && (
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
className={`${
|
||||
hasPermission(Permission.MANAGE_REQUESTS) ? 'sm:hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<PencilIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.edit)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
requestData.requestedBy.id === user?.id && (
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.decline)}
|
||||
<XIcon style={{ marginRight: '0' }} />
|
||||
<span className="ml-1.5 hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.cancel)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-20 sm:w-28">
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
|
||||
as={
|
||||
href={
|
||||
request.type === 'movie'
|
||||
? `/movie/${request.media.tmdbId}`
|
||||
: `/tv/${request.media.tmdbId}`
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
title.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-20 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md"
|
||||
/>
|
||||
<a className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
CheckIcon,
|
||||
PencilIcon,
|
||||
RefreshIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import {
|
||||
defineMessages,
|
||||
FormattedDate,
|
||||
FormattedRelativeTime,
|
||||
useIntl,
|
||||
} from 'react-intl';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
@@ -17,25 +19,70 @@ import {
|
||||
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '../../../../server/models/Movie';
|
||||
import type { TvDetails } from '../../../../server/models/Tv';
|
||||
import { LanguageContext } from '../../../context/LanguageContext';
|
||||
import { Permission, useUser } from '../../../hooks/useUser';
|
||||
import globalMessages from '../../../i18n/globalMessages';
|
||||
import Badge from '../../Common/Badge';
|
||||
import Button from '../../Common/Button';
|
||||
import Table from '../../Common/Table';
|
||||
import CachedImage from '../../Common/CachedImage';
|
||||
import ConfirmButton from '../../Common/ConfirmButton';
|
||||
import RequestModal from '../../RequestModal';
|
||||
import StatusBadge from '../../StatusBadge';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: 'Seasons',
|
||||
notavailable: 'N/A',
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
failedretry: 'Something went wrong while retrying the request.',
|
||||
requested: 'Requested',
|
||||
requesteddate: 'Requested',
|
||||
modified: 'Modified',
|
||||
modifieduserdate: '{date} by {user}',
|
||||
mediaerror: 'The associated title for this request is no longer available.',
|
||||
editrequest: 'Edit Request',
|
||||
deleterequest: 'Delete Request',
|
||||
cancelRequest: 'Cancel Request',
|
||||
});
|
||||
|
||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||
return (movie as MovieDetails).title !== undefined;
|
||||
};
|
||||
|
||||
interface RequestItemErroProps {
|
||||
mediaId?: number;
|
||||
revalidateList: () => void;
|
||||
}
|
||||
|
||||
const RequestItemError: React.FC<RequestItemErroProps> = ({
|
||||
mediaId,
|
||||
revalidateList,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
||||
revalidateList();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center rounded-xl bg-gray-800 px-10 ring-1 ring-red-500 lg:flex-row xl:h-28">
|
||||
<span className="text-center text-sm text-gray-300 lg:text-left">
|
||||
{intl.formatMessage(messages.mediaerror)}
|
||||
</span>
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
|
||||
<div className="mt-4 lg:ml-4 lg:mt-0">
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RequestItemProps {
|
||||
request: MediaRequest;
|
||||
revalidateList: () => void;
|
||||
@@ -50,23 +97,21 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
});
|
||||
const { addToast } = useToasts();
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
const { user, hasPermission } = useUser();
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const { locale } = useContext(LanguageContext);
|
||||
const url =
|
||||
request.type === 'movie'
|
||||
? `/api/v1/movie/${request.media.tmdbId}`
|
||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||
inView ? `${url}?language=${locale}` : null
|
||||
inView ? url : null
|
||||
);
|
||||
const { data: requestData, mutate: revalidate } = useSWR<MediaRequest>(
|
||||
`/api/v1/request/${request.id}`,
|
||||
{
|
||||
initialData: request,
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: requestData,
|
||||
revalidate,
|
||||
mutate,
|
||||
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||
initialData: request,
|
||||
});
|
||||
|
||||
const [isRetrying, setRetrying] = useState(false);
|
||||
|
||||
@@ -89,7 +134,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
|
||||
try {
|
||||
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||
mutate(result.data);
|
||||
revalidate(result.data);
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.failedretry), {
|
||||
autoDismiss: true,
|
||||
@@ -102,22 +147,24 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
|
||||
if (!title && !error) {
|
||||
return (
|
||||
<tr className="w-full h-24 animate-pulse" ref={ref}>
|
||||
<td colSpan={6}></td>
|
||||
</tr>
|
||||
<div
|
||||
className="h-64 w-full animate-pulse rounded-xl bg-gray-800 xl:h-28"
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!title || !requestData) {
|
||||
return (
|
||||
<tr className="w-full h-24 animate-pulse">
|
||||
<td colSpan={6}></td>
|
||||
</tr>
|
||||
<RequestItemError
|
||||
mediaId={requestData?.media.id}
|
||||
revalidateList={revalidateList}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="relative w-full h-24 p-2">
|
||||
<>
|
||||
<RequestModal
|
||||
show={showEditModal}
|
||||
tmdbId={request.media.tmdbId}
|
||||
@@ -130,28 +177,26 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
setShowEditModal(false);
|
||||
}}
|
||||
/>
|
||||
<Table.TD>
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href={
|
||||
request.type === 'movie'
|
||||
? `/movie/${request.media.tmdbId}`
|
||||
: `/tv/${request.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="flex-shrink-0 hidden mr-4 sm:block">
|
||||
<img
|
||||
src={
|
||||
title.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-12 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer transform-gpu hover:scale-105 hover:shadow-md"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex-shrink overflow-hidden">
|
||||
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
|
||||
{title.backdropPath && (
|
||||
<div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
|
||||
<Link
|
||||
href={
|
||||
requestData.type === 'movie'
|
||||
@@ -159,219 +204,295 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="flex items-center mt-1">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-5 mr-2 rounded-full"
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
objectFit="cover"
|
||||
/>
|
||||
<span className="text-sm hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
{requestData.seasons.length > 0 && (
|
||||
<div className="items-center hidden mt-2 text-sm sm:flex">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.seasons)}
|
||||
</span>
|
||||
{requestData.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
|
||||
{(isMovie(title)
|
||||
? title.releaseDate
|
||||
: title.firstAirDate
|
||||
)?.slice(0, 4)}
|
||||
</div>
|
||||
<Link
|
||||
href={
|
||||
requestData.type === 'movie'
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
</Link>
|
||||
{!isMovie(title) && request.seasons.length > 0 && (
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount:
|
||||
title.seasons.filter(
|
||||
(season) => season.seasonNumber !== 0
|
||||
).length === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
</span>
|
||||
))}
|
||||
{title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||
.length === request.seasons.length ? (
|
||||
<span className="mr-2 uppercase">
|
||||
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</span>
|
||||
{requestData.status === MediaRequestStatus.DECLINED ? (
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.declined)}
|
||||
</Badge>
|
||||
) : requestData.media[
|
||||
requestData.is4k ? 'status4k' : 'status'
|
||||
] === MediaStatus.UNKNOWN ? (
|
||||
<Badge
|
||||
badgeType="danger"
|
||||
//href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
>
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={
|
||||
requestData.media[requestData.is4k ? 'status4k' : 'status']
|
||||
}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.media[
|
||||
requestData.is4k ? 'mediaUrl4k' : 'mediaUrl'
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-field">
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) ? (
|
||||
<>
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.requested)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{intl.formatMessage(messages.modifieduserdate, {
|
||||
date: (
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.createdAt).getTime() -
|
||||
Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5"
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.requesteddate)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.createdAt).getTime() -
|
||||
Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{requestData.modifiedBy && (
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.modified)}
|
||||
</span>
|
||||
<span className="flex truncate text-sm text-gray-300">
|
||||
{intl.formatMessage(messages.modifieduserdate, {
|
||||
date: (
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.updatedAt).getTime() -
|
||||
Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.modifiedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
src={requestData.modifiedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5"
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN ||
|
||||
requestData.status === MediaRequestStatus.DECLINED ? (
|
||||
<Badge badgeType="danger">
|
||||
{requestData.status === MediaRequestStatus.DECLINED
|
||||
? intl.formatMessage(globalMessages.declined)
|
||||
: intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={requestData.media[requestData.is4k ? 'status4k' : 'status']}
|
||||
inProgress={
|
||||
(
|
||||
requestData.media[
|
||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||
] ?? []
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
/>
|
||||
)}
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-300">
|
||||
<FormattedDate value={requestData.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="flex flex-col">
|
||||
{requestData.modifiedBy ? (
|
||||
<span className="text-sm text-gray-300">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={requestData.modifiedBy.avatar}
|
||||
alt=""
|
||||
className="w-5 mr-2 rounded-full"
|
||||
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN &&
|
||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="primary"
|
||||
disabled={isRetrying}
|
||||
onClick={() => retryRequest()}
|
||||
>
|
||||
<RefreshIcon
|
||||
className={isRetrying ? 'animate-spin' : ''}
|
||||
style={{ animationDirection: 'reverse' }}
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{requestData.modifiedBy.displayName} (
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(requestData.updatedAt).getTime() - Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
)
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
isRetrying ? globalMessages.retrying : globalMessages.retry
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<ConfirmButton
|
||||
onClick={() => deleteRequest()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="flex w-full flex-row space-x-2">
|
||||
<span className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="success"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
>
|
||||
<CheckIcon />
|
||||
<span>{intl.formatMessage(globalMessages.approve)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="danger"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
>
|
||||
<XIcon />
|
||||
<span>{intl.formatMessage(globalMessages.decline)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-300">N/A</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.UNKNOWN &&
|
||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
className="mr-2"
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
disabled={isRetrying}
|
||||
onClick={() => retryRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="18px"
|
||||
height="18px"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.retry)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => deleteRequest()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.delete)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<>
|
||||
<span className="mr-2">
|
||||
<Button
|
||||
buttonType="success"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('approve')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.approve)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span className="mr-2">
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={() => modifyRequest('decline')}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.decline)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
(hasPermission(Permission.MANAGE_REQUESTS) ||
|
||||
(requestData.requestedBy.id === user?.id &&
|
||||
(requestData.type === 'tv' ||
|
||||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
|
||||
<span className="w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => setShowEditModal(true)}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-0 sm:mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
<span className="hidden sm:block">
|
||||
{intl.formatMessage(globalMessages.edit)}
|
||||
</span>
|
||||
<PencilIcon />
|
||||
<span>{intl.formatMessage(messages.editrequest)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
)}
|
||||
{requestData.status === MediaRequestStatus.PENDING &&
|
||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
requestData.requestedBy.id === user?.id && (
|
||||
<ConfirmButton
|
||||
onClick={() => deleteRequest()}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<XIcon />
|
||||
<span>{intl.formatMessage(messages.cancelRequest)}</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,51 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FilterIcon,
|
||||
SortDescendingIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import RequestItem from './RequestItem';
|
||||
import Header from '../Common/Header';
|
||||
import Table from '../Common/Table';
|
||||
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
|
||||
import { useUser } from '../../hooks/useUser';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import Button from '../Common/Button';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Header from '../Common/Header';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import RequestItem from './RequestItem';
|
||||
|
||||
const messages = defineMessages({
|
||||
requests: 'Requests',
|
||||
mediaInfo: 'Media Info',
|
||||
status: 'Status',
|
||||
requestedAt: 'Requested At',
|
||||
modifiedBy: 'Last Modified By',
|
||||
showingresults:
|
||||
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
|
||||
resultsperpage: 'Display {pageSize} results per page',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
filterAll: 'All',
|
||||
filterPending: 'Pending',
|
||||
filterApproved: 'Approved',
|
||||
filterAvailable: 'Available',
|
||||
filterProcessing: 'Processing',
|
||||
noresults: 'No results.',
|
||||
showallrequests: 'Show All Requests',
|
||||
sortAdded: 'Request Date',
|
||||
sortAdded: 'Most Recent',
|
||||
sortModified: 'Last Modified',
|
||||
});
|
||||
|
||||
type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available';
|
||||
enum Filter {
|
||||
ALL = 'all',
|
||||
PENDING = 'pending',
|
||||
APPROVED = 'approved',
|
||||
PROCESSING = 'processing',
|
||||
AVAILABLE = 'available',
|
||||
UNAVAILABLE = 'unavailable',
|
||||
}
|
||||
|
||||
type Sort = 'added' | 'modified';
|
||||
|
||||
const RequestList: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const intl = useIntl();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
|
||||
const { user } = useUser({
|
||||
id: Number(router.query.userId),
|
||||
});
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||
|
||||
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
|
||||
const page = router.query.page ? Number(router.query.page) : 1;
|
||||
const pageIndex = page - 1;
|
||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
mutate: revalidate,
|
||||
} = useSWR<RequestResultsResponse>(
|
||||
`/api/v1/request?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}&sort=${currentSort}`
|
||||
}&filter=${currentFilter}&sort=${currentSort}${
|
||||
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
|
||||
}`
|
||||
);
|
||||
|
||||
// Restore last set filter values on component mount
|
||||
useEffect(() => {
|
||||
const filterString = window.localStorage.getItem('rl-filter-settings');
|
||||
|
||||
if (filterString) {
|
||||
const filterSettings = JSON.parse(filterString);
|
||||
|
||||
setCurrentFilter(filterSettings.currentFilter);
|
||||
setCurrentSort(filterSettings.currentSort);
|
||||
setCurrentPageSize(filterSettings.currentPageSize);
|
||||
}
|
||||
|
||||
// If filter value is provided in query, use that instead
|
||||
if (Object.values(Filter).includes(router.query.filter as Filter)) {
|
||||
setCurrentFilter(router.query.filter as Filter);
|
||||
}
|
||||
}, [router.query.filter]);
|
||||
|
||||
// Set filter values to local storage any time they are changed
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
'rl-filter-settings',
|
||||
JSON.stringify({
|
||||
currentFilter,
|
||||
currentSort,
|
||||
currentPageSize,
|
||||
})
|
||||
);
|
||||
}, [currentFilter, currentSort, currentPageSize]);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
@@ -59,73 +106,81 @@ const RequestList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={intl.formatMessage(messages.requests)} />
|
||||
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
||||
<Header>{intl.formatMessage(messages.requests)}</Header>
|
||||
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
|
||||
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<PageTitle
|
||||
title={[
|
||||
intl.formatMessage(messages.requests),
|
||||
router.query.userId ? user?.displayName : '',
|
||||
]}
|
||||
/>
|
||||
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
|
||||
<Header
|
||||
subtext={
|
||||
router.query.userId ? (
|
||||
<Link href={`/users/${user?.id}`}>
|
||||
<a className="hover:underline">{user?.displayName}</a>
|
||||
</Link>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(messages.requests)}
|
||||
</Header>
|
||||
<div className="mt-2 flex flex-grow flex-col sm:flex-row lg:flex-grow-0">
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
|
||||
<FilterIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="filter"
|
||||
name="filter"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
});
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="all">
|
||||
{intl.formatMessage(messages.filterAll)}
|
||||
{intl.formatMessage(globalMessages.all)}
|
||||
</option>
|
||||
<option value="pending">
|
||||
{intl.formatMessage(messages.filterPending)}
|
||||
{intl.formatMessage(globalMessages.pending)}
|
||||
</option>
|
||||
<option value="approved">
|
||||
{intl.formatMessage(messages.filterApproved)}
|
||||
{intl.formatMessage(globalMessages.approved)}
|
||||
</option>
|
||||
<option value="processing">
|
||||
{intl.formatMessage(messages.filterProcessing)}
|
||||
{intl.formatMessage(globalMessages.processing)}
|
||||
</option>
|
||||
<option value="available">
|
||||
{intl.formatMessage(messages.filterAvailable)}
|
||||
{intl.formatMessage(globalMessages.available)}
|
||||
</option>
|
||||
<option value="unavailable">
|
||||
{intl.formatMessage(globalMessages.unavailable)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
|
||||
</svg>
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
<SortDescendingIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<select
|
||||
id="sort"
|
||||
name="sort"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentSort(e.target.value as Sort);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
});
|
||||
}}
|
||||
value={currentSort}
|
||||
className="rounded-r-only"
|
||||
@@ -140,114 +195,104 @@ const RequestList: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.modifiedBy)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data.results.map((request) => {
|
||||
return (
|
||||
<RequestItem
|
||||
request={request}
|
||||
key={`request-list-${request.id}`}
|
||||
revalidateList={() => revalidate()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{data.results.map((request) => {
|
||||
return (
|
||||
<div className="py-2" key={`request-list-${request.id}`}>
|
||||
<RequestItem
|
||||
request={request}
|
||||
revalidateList={() => revalidate()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{data.results.length === 0 && (
|
||||
<tr className="relative h-24 p-2 text-white">
|
||||
<Table.TD colSpan={6} noPadding>
|
||||
<div className="flex flex-col items-center justify-center w-screen p-6 lg:w-full">
|
||||
<span className="text-base">
|
||||
{intl.formatMessage(messages.noresults)}
|
||||
</span>
|
||||
{currentFilter !== 'all' && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
buttonType="primary"
|
||||
onClick={() => setCurrentFilter('all')}
|
||||
>
|
||||
{intl.formatMessage(messages.showallrequests)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
)}
|
||||
<tr className="bg-gray-700">
|
||||
<Table.TD colSpan={6} noPadding>
|
||||
<nav
|
||||
className="flex flex-col items-center w-screen px-6 py-3 space-x-4 space-y-3 sm:space-y-0 sm:flex-row lg:w-full"
|
||||
aria-label="Pagination"
|
||||
{data.results.length === 0 && (
|
||||
<div className="flex w-full flex-col items-center justify-center py-24 text-white">
|
||||
<span className="text-2xl text-gray-400">
|
||||
{intl.formatMessage(globalMessages.noresults)}
|
||||
</span>
|
||||
{currentFilter !== Filter.ALL && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
onClick={() => setCurrentFilter(Filter.ALL)}
|
||||
>
|
||||
<div className="hidden lg:flex lg:flex-1">
|
||||
<p className="text-sm">
|
||||
{data.results.length > 0 &&
|
||||
intl.formatMessage(messages.showingresults, {
|
||||
from: pageIndex * currentPageSize + 1,
|
||||
to:
|
||||
data.results.length < currentPageSize
|
||||
? pageIndex * currentPageSize + data.results.length
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
strong: function strong(msg) {
|
||||
return <span className="font-medium">{msg}</span>;
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
||||
<span className="items-center -mt-3 text-sm sm:-ml-4 lg:ml-0 sm:mt-0">
|
||||
{intl.formatMessage(messages.resultsperpage, {
|
||||
pageSize: (
|
||||
<select
|
||||
id="pageSize"
|
||||
name="pageSize"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentPageSize(Number(e.target.value));
|
||||
}}
|
||||
value={currentPageSize}
|
||||
className="inline short"
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
|
||||
<Button
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => setPageIndex((current) => current - 1)}
|
||||
{intl.formatMessage(messages.showallrequests)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="actions">
|
||||
<nav
|
||||
className="mb-3 flex flex-col items-center space-y-3 sm:flex-row sm:space-y-0"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<div className="hidden lg:flex lg:flex-1">
|
||||
<p className="text-sm">
|
||||
{data.results.length > 0 &&
|
||||
intl.formatMessage(globalMessages.showingresults, {
|
||||
from: pageIndex * currentPageSize + 1,
|
||||
to:
|
||||
data.results.length < currentPageSize
|
||||
? pageIndex * currentPageSize + data.results.length
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
strong: function strong(msg) {
|
||||
return <span className="font-medium">{msg}</span>;
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
||||
<span className="-mt-3 items-center truncate text-sm sm:mt-0">
|
||||
{intl.formatMessage(globalMessages.resultsperpage, {
|
||||
pageSize: (
|
||||
<select
|
||||
id="pageSize"
|
||||
name="pageSize"
|
||||
onChange={(e) => {
|
||||
setCurrentPageSize(Number(e.target.value));
|
||||
router
|
||||
.push({
|
||||
pathname: router.pathname,
|
||||
query: router.query.userId
|
||||
? { userId: router.query.userId }
|
||||
: {},
|
||||
})
|
||||
.then(() => window.scrollTo(0, 0));
|
||||
}}
|
||||
value={currentPageSize}
|
||||
className="short inline"
|
||||
>
|
||||
{intl.formatMessage(messages.previous)}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => setPageIndex((current) => current + 1)}
|
||||
>
|
||||
{intl.formatMessage(messages.next)}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-auto justify-center space-x-2 sm:flex-1 sm:justify-end">
|
||||
<Button
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => updateQueryParams('page', (page - 1).toString())}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span>{intl.formatMessage(globalMessages.previous)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => updateQueryParams('page', (page + 1).toString())}
|
||||
>
|
||||
<span>{intl.formatMessage(globalMessages.next)}</span>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,12 +23,14 @@ const messages = defineMessages({
|
||||
requesttitle: 'Request {title}',
|
||||
request4ktitle: 'Request {title} in 4K',
|
||||
edit: 'Edit Request',
|
||||
approve: 'Approve Request',
|
||||
cancel: 'Cancel Request',
|
||||
pendingrequest: 'Pending Request for {title}',
|
||||
pending4krequest: 'Pending 4K Request for {title}',
|
||||
requestfrom: "{username}'s request is pending approval.",
|
||||
errorediting: 'Something went wrong while editing the request.',
|
||||
requestedited: 'Request for <strong>{title}</strong> edited successfully!',
|
||||
requestApproved: 'Request for <strong>{title}</strong> approved!',
|
||||
requesterror: 'Something went wrong while submitting the request.',
|
||||
pendingapproval: 'Your request is pending approval.',
|
||||
});
|
||||
@@ -60,7 +62,10 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
const intl = useIntl();
|
||||
const { user, hasPermission } = useUser();
|
||||
const { data: quota } = useSWR<QuotaResponse>(
|
||||
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
|
||||
user &&
|
||||
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
|
||||
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -156,7 +161,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const updateRequest = async () => {
|
||||
const updateRequest = async (alsoApproveRequest = false) => {
|
||||
setIsUpdating(true);
|
||||
|
||||
try {
|
||||
@@ -169,14 +174,23 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
tags: requestOverrides?.tags,
|
||||
});
|
||||
|
||||
if (alsoApproveRequest) {
|
||||
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
|
||||
}
|
||||
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.requestedited, {
|
||||
title: data?.title,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
alsoApproveRequest
|
||||
? messages.requestApproved
|
||||
: messages.requestedited,
|
||||
{
|
||||
title: data?.title,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
}
|
||||
)}
|
||||
</span>,
|
||||
{
|
||||
appearance: 'success',
|
||||
@@ -199,12 +213,6 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
|
||||
if (editRequest) {
|
||||
const isOwner = editRequest.requestedBy.id === user?.id;
|
||||
const showEditButton = hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_ADVANCED],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -215,20 +223,44 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
is4k ? messages.pending4krequest : messages.pendingrequest,
|
||||
{ title: data?.title }
|
||||
)}
|
||||
onOk={() => (showEditButton ? updateRequest() : cancelRequest())}
|
||||
onOk={() =>
|
||||
hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? updateRequest(true)
|
||||
: hasPermission(Permission.REQUEST_ADVANCED)
|
||||
? updateRequest()
|
||||
: cancelRequest()
|
||||
}
|
||||
okDisabled={isUpdating}
|
||||
okText={
|
||||
showEditButton
|
||||
hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? intl.formatMessage(messages.approve)
|
||||
: hasPermission(Permission.REQUEST_ADVANCED)
|
||||
? intl.formatMessage(messages.edit)
|
||||
: intl.formatMessage(messages.cancel)
|
||||
}
|
||||
okButtonType={showEditButton ? 'primary' : 'danger'}
|
||||
okButtonType={
|
||||
hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? 'success'
|
||||
: hasPermission(Permission.REQUEST_ADVANCED)
|
||||
? 'primary'
|
||||
: 'danger'
|
||||
}
|
||||
onSecondary={
|
||||
isOwner && showEditButton ? () => cancelRequest() : undefined
|
||||
isOwner &&
|
||||
hasPermission(
|
||||
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? () => cancelRequest()
|
||||
: undefined
|
||||
}
|
||||
secondaryDisabled={isUpdating}
|
||||
secondaryText={
|
||||
isOwner && showEditButton
|
||||
isOwner &&
|
||||
hasPermission(
|
||||
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? intl.formatMessage(messages.cancel)
|
||||
: undefined
|
||||
}
|
||||
@@ -244,22 +276,20 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
||||
})}
|
||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<div className="mt-4">
|
||||
<AdvancedRequester
|
||||
type="movie"
|
||||
is4k={is4k}
|
||||
requestUser={editRequest.requestedBy}
|
||||
defaultOverrides={{
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
tags: editRequest.tags,
|
||||
}}
|
||||
onChange={(overrides) => {
|
||||
setRequestOverrides(overrides);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedRequester
|
||||
type="movie"
|
||||
is4k={is4k}
|
||||
requestUser={editRequest.requestedBy}
|
||||
defaultOverrides={{
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
tags: editRequest.tags,
|
||||
}}
|
||||
onChange={(overrides) => {
|
||||
setRequestOverrides(overrides);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -30,13 +30,15 @@ const messages = defineMessages({
|
||||
requesttitle: 'Request {title}',
|
||||
request4ktitle: 'Request {title} in 4K',
|
||||
edit: 'Edit Request',
|
||||
approve: 'Approve Request',
|
||||
cancel: 'Cancel Request',
|
||||
pendingrequest: 'Pending Request for {title}',
|
||||
pending4krequest: 'Pending 4K Request for {title}',
|
||||
requestfrom: "{username}'s request is pending approval.",
|
||||
requestseasons:
|
||||
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}',
|
||||
requestall: 'Request All Seasons',
|
||||
requestseasons4k:
|
||||
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K',
|
||||
alreadyrequested: 'Already Requested',
|
||||
selectseason: 'Select Season(s)',
|
||||
season: 'Season',
|
||||
@@ -45,6 +47,7 @@ const messages = defineMessages({
|
||||
extras: 'Extras',
|
||||
errorediting: 'Something went wrong while editing the request.',
|
||||
requestedited: 'Request for <strong>{title}</strong> edited successfully!',
|
||||
requestApproved: 'Request for <strong>{title}</strong> approved!',
|
||||
requestcancelled: 'Request for <strong>{title}</strong> canceled.',
|
||||
autoapproval: 'Automatic Approval',
|
||||
requesterror: 'Something went wrong while submitting the request.',
|
||||
@@ -88,7 +91,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
});
|
||||
const [tvdbId, setTvdbId] = useState<number | undefined>(undefined);
|
||||
const { data: quota } = useSWR<QuotaResponse>(
|
||||
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
|
||||
user &&
|
||||
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
|
||||
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
|
||||
: null
|
||||
);
|
||||
|
||||
const currentlyRemaining =
|
||||
@@ -96,7 +102,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
selectedSeasons.length +
|
||||
(editRequest?.seasons ?? []).length;
|
||||
|
||||
const updateRequest = async () => {
|
||||
const updateRequest = async (alsoApproveRequest = false) => {
|
||||
if (!editRequest) {
|
||||
return;
|
||||
}
|
||||
@@ -117,6 +123,10 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
tags: requestOverrides?.tags,
|
||||
seasons: selectedSeasons,
|
||||
});
|
||||
|
||||
if (alsoApproveRequest) {
|
||||
await axios.post(`/api/v1/request/${editRequest.id}/approve`);
|
||||
}
|
||||
} else {
|
||||
await axios.delete(`/api/v1/request/${editRequest.id}`);
|
||||
}
|
||||
@@ -124,12 +134,17 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
addToast(
|
||||
<span>
|
||||
{selectedSeasons.length > 0
|
||||
? intl.formatMessage(messages.requestedited, {
|
||||
title: data?.name,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
})
|
||||
? intl.formatMessage(
|
||||
alsoApproveRequest
|
||||
? messages.requestApproved
|
||||
: messages.requestedited,
|
||||
{
|
||||
title: data?.name,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(messages.requestcancelled, {
|
||||
title: data?.name,
|
||||
strong: function strong(msg) {
|
||||
@@ -368,7 +383,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
loading={!data && !error}
|
||||
backgroundClickable
|
||||
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
|
||||
onOk={() => (editRequest ? updateRequest() : sendRequest())}
|
||||
onOk={() =>
|
||||
editRequest
|
||||
? hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? updateRequest(true)
|
||||
: updateRequest()
|
||||
: sendRequest()
|
||||
}
|
||||
title={intl.formatMessage(
|
||||
editRequest
|
||||
? is4k
|
||||
@@ -383,16 +404,23 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
editRequest
|
||||
? selectedSeasons.length === 0
|
||||
? intl.formatMessage(messages.cancel)
|
||||
: hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? intl.formatMessage(messages.approve)
|
||||
: intl.formatMessage(messages.edit)
|
||||
: getAllRequestedSeasons().length >= getAllSeasons().length
|
||||
? intl.formatMessage(messages.alreadyrequested)
|
||||
: !settings.currentSettings.partialRequestsEnabled
|
||||
? intl.formatMessage(messages.requestall)
|
||||
? intl.formatMessage(
|
||||
is4k ? globalMessages.request4k : globalMessages.request
|
||||
)
|
||||
: selectedSeasons.length === 0
|
||||
? intl.formatMessage(messages.selectseason)
|
||||
: intl.formatMessage(messages.requestseasons, {
|
||||
seasonCount: selectedSeasons.length,
|
||||
})
|
||||
: intl.formatMessage(
|
||||
is4k ? messages.requestseasons4k : messages.requestseasons,
|
||||
{
|
||||
seasonCount: selectedSeasons.length,
|
||||
}
|
||||
)
|
||||
}
|
||||
okDisabled={
|
||||
editRequest
|
||||
@@ -406,11 +434,14 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
selectedSeasons.length === 0)
|
||||
}
|
||||
okButtonType={
|
||||
editRequest &&
|
||||
settings.currentSettings.partialRequestsEnabled &&
|
||||
selectedSeasons.length === 0
|
||||
? 'danger'
|
||||
: `primary`
|
||||
editRequest
|
||||
? settings.currentSettings.partialRequestsEnabled &&
|
||||
selectedSeasons.length === 0
|
||||
? 'danger'
|
||||
: hasPermission(Permission.MANAGE_REQUESTS)
|
||||
? 'success'
|
||||
: 'primary'
|
||||
: 'primary'
|
||||
}
|
||||
cancelText={
|
||||
editRequest
|
||||
@@ -440,7 +471,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
!(
|
||||
quota?.tv.limit &&
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
|
||||
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
||||
) &&
|
||||
getAllRequestedSeasons().length < getAllSeasons().length &&
|
||||
!editRequest && (
|
||||
@@ -457,7 +488,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
quota={quota?.tv}
|
||||
remaining={
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
|
||||
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
||||
? 0
|
||||
: currentlyRemaining
|
||||
}
|
||||
@@ -468,7 +499,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
}
|
||||
overLimit={
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
|
||||
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
|
||||
? unrequestedSeasons.length
|
||||
: undefined
|
||||
}
|
||||
@@ -482,7 +513,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className={`w-16 px-4 py-3 bg-gray-500 ${
|
||||
className={`w-16 bg-gray-500 px-4 py-3 ${
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
'hidden'
|
||||
}`}
|
||||
@@ -497,7 +528,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
toggleAllSeasons();
|
||||
}
|
||||
}}
|
||||
className={`relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 pt-2 cursor-pointer focus:outline-none ${
|
||||
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
|
||||
quota?.tv.remaining &&
|
||||
quota.tv.limit &&
|
||||
quota.tv.remaining < unrequestedSeasons.length
|
||||
@@ -509,28 +540,28 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllSeasons() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-1 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||
<th className="bg-gray-500 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.season)}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||
<th className="bg-gray-500 px-5 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.numberofepisodes)}
|
||||
</th>
|
||||
<th className="px-2 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500 md:px-6">
|
||||
<th className="bg-gray-500 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-gray-600 divide-y divide-gray-700">
|
||||
<tbody className="divide-y divide-gray-700 bg-gray-600">
|
||||
{data?.seasons
|
||||
.filter((season) => season.seasonNumber !== 0)
|
||||
.map((season) => {
|
||||
@@ -546,7 +577,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
return (
|
||||
<tr key={`season-${season.id}`}>
|
||||
<td
|
||||
className={`px-4 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap ${
|
||||
className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
|
||||
!settings.currentSettings
|
||||
.partialRequestsEnabled && 'hidden'
|
||||
}`}
|
||||
@@ -568,7 +599,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
toggleSeason(season.seasonNumber);
|
||||
}
|
||||
}}
|
||||
className={`pt-2 relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${
|
||||
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
|
||||
mediaSeason ||
|
||||
(quota?.tv.limit &&
|
||||
currentlyRemaining <= 0 &&
|
||||
@@ -590,7 +621,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
isSelectedSeason(season.seasonNumber)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -603,21 +634,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
isSelectedSeason(season.seasonNumber)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 border border-gray-200 rounded-full bg-white shadow transform group-focus:ring group-focus:border-blue-300 transition-transform ease-in-out duration-200`}
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(messages.extras)
|
||||
: intl.formatMessage(messages.seasonnumber, {
|
||||
number: season.seasonNumber,
|
||||
})}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap px-5 py-4 text-sm leading-5 text-gray-200 md:px-6">
|
||||
{season.episodeCount}
|
||||
</td>
|
||||
<td className="py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
|
||||
{!seasonRequest && !mediaSeason && (
|
||||
<Badge>
|
||||
{intl.formatMessage(
|
||||
@@ -667,28 +698,26 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
||||
</div>
|
||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<div className="mt-4">
|
||||
<AdvancedRequester
|
||||
type="tv"
|
||||
is4k={is4k}
|
||||
isAnime={data?.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)}
|
||||
onChange={(overrides) => setRequestOverrides(overrides)}
|
||||
requestUser={editRequest?.requestedBy}
|
||||
defaultOverrides={
|
||||
editRequest
|
||||
? {
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
language: editRequest.languageProfileId,
|
||||
tags: editRequest.tags,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedRequester
|
||||
type="tv"
|
||||
is4k={is4k}
|
||||
isAnime={data?.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)}
|
||||
onChange={(overrides) => setRequestOverrides(overrides)}
|
||||
requestUser={editRequest?.requestedBy}
|
||||
defaultOverrides={
|
||||
editRequest
|
||||
? {
|
||||
folder: editRequest.rootFolder,
|
||||
profile: editRequest.profileId,
|
||||
server: editRequest.serverId,
|
||||
language: editRequest.languageProfileId,
|
||||
tags: editRequest.tags,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import MovieRequestModal from './MovieRequestModal';
|
||||
import type { MediaStatus } from '../../../server/constants/media';
|
||||
import TvRequestModal from './TvRequestModal';
|
||||
import Transition from '../Transition';
|
||||
import { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||
import Transition from '../Transition';
|
||||
import MovieRequestModal from './MovieRequestModal';
|
||||
import TvRequestModal from './TvRequestModal';
|
||||
|
||||
interface RequestModalProps {
|
||||
show: boolean;
|
||||
@@ -26,29 +26,6 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
onUpdating,
|
||||
onCancel,
|
||||
}) => {
|
||||
if (type === 'tv') {
|
||||
return (
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition opacity-100 duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
>
|
||||
<TvRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
enter="transition opacity-0 duration-300"
|
||||
@@ -59,14 +36,25 @@ const RequestModal: React.FC<RequestModalProps> = ({
|
||||
leaveTo="opacity-0"
|
||||
show={show}
|
||||
>
|
||||
<MovieRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
{type === 'movie' ? (
|
||||
<MovieRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
) : (
|
||||
<TvRequestModal
|
||||
onComplete={onComplete}
|
||||
onCancel={onCancel}
|
||||
tmdbId={tmdbId}
|
||||
onUpdating={onUpdating}
|
||||
is4k={is4k}
|
||||
editRequest={editRequest}
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
accessToken: 'Application API Token',
|
||||
accessTokenTip:
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr',
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr',
|
||||
userToken: 'User or Group Key',
|
||||
userTokenTip:
|
||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||
|
||||
@@ -19,7 +19,7 @@ const messages = defineMessages({
|
||||
'Allow users to also start a chat with your bot and configure their own notifications',
|
||||
botAPI: 'Bot Authorization Token',
|
||||
botApiTip:
|
||||
'<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr',
|
||||
'<CreateBotLink>Create a bot</CreateBotLink> for use with Jellyseerr',
|
||||
chatId: 'Chat ID',
|
||||
chatIdTip:
|
||||
'Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
|
||||
|
||||
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
toastWebPushTestSuccess: 'Web push test notification sent!',
|
||||
toastWebPushTestFailed: 'Web push test notification failed to send.',
|
||||
httpsRequirement:
|
||||
'In order to receive web push notifications, Overseerr must be served over HTTPS.',
|
||||
'In order to receive web push notifications, Jellyseerr must be served over HTTPS.',
|
||||
});
|
||||
|
||||
const NotificationsWebPush: React.FC = () => {
|
||||
|
||||
@@ -20,11 +20,11 @@ const messages = defineMessages({
|
||||
currentversion: 'Current Version',
|
||||
viewchangelog: 'View Changelog',
|
||||
runningDevelopMessage:
|
||||
'The latest changes to the <code>develop</code> branch of Overseerr are not shown below. Please see the commit history for this branch on <GithubLink>GitHub</GithubLink> for details.',
|
||||
'The latest changes to the <code>develop</code> branch of Jellyseerr are not shown below. Please see the commit history for this branch on <GithubLink>GitHub</GithubLink> for details.',
|
||||
});
|
||||
|
||||
const REPO_RELEASE_API =
|
||||
'https://api.github.com/repos/sct/overseerr/releases?per_page=20';
|
||||
'https://api.github.com/repos/Fallenbagel/jellyseerr/releases?per_page=20';
|
||||
|
||||
interface GitHubRelease {
|
||||
url: string;
|
||||
@@ -153,7 +153,7 @@ const Releases: React.FC<ReleasesProps> = ({ currentVersion }) => {
|
||||
GithubLink: function GithubLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/sct/overseerr"
|
||||
href="https://github.com/Fallenbagel/jellyseerr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-yellow-100 underline transition duration-300 hover:text-white"
|
||||
|
||||
@@ -16,14 +16,14 @@ import Releases from './Releases';
|
||||
|
||||
const messages = defineMessages({
|
||||
about: 'About',
|
||||
overseerrinformation: 'Overseerr Information',
|
||||
overseerrinformation: 'Jellyseerr Information',
|
||||
version: 'Version',
|
||||
totalmedia: 'Total Media',
|
||||
totalrequests: 'Total Requests',
|
||||
gettingsupport: 'Getting Support',
|
||||
githubdiscussions: 'GitHub Discussions',
|
||||
timezone: 'Time Zone',
|
||||
supportoverseerr: 'Support Overseerr',
|
||||
supportoverseerr: 'Support Jellyseerr',
|
||||
helppaycoffee: 'Help Pay for Coffee',
|
||||
documentation: 'Documentation',
|
||||
preferredmethod: 'Preferred',
|
||||
@@ -68,7 +68,7 @@ const SettingsAbout: React.FC = () => {
|
||||
</p>
|
||||
<p className="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
|
||||
<a
|
||||
href="http://github.com/sct/overseerr"
|
||||
href="https://github.com/Fallenbagel/jellyseerr"
|
||||
className="font-medium text-indigo-100 transition duration-150 ease-in-out whitespace-nowrap hover:text-white"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -85,10 +85,10 @@ const SettingsAbout: React.FC = () => {
|
||||
title={intl.formatMessage(messages.version)}
|
||||
className="truncate"
|
||||
>
|
||||
<code>{data.version.replace('develop-', '')}</code>
|
||||
{/* <code>{data.version.replace('develop-', '')}</code> */}
|
||||
{status?.updateAvailable ? (
|
||||
<Badge badgeType="warning" className="ml-2">
|
||||
{intl.formatMessage(messages.outofdate)}
|
||||
<Badge badgeType="success" className="ml-2">
|
||||
{intl.formatMessage(messages.uptodate)}
|
||||
</Badge>
|
||||
) : (
|
||||
status?.commitTag !== 'local' && (
|
||||
@@ -115,32 +115,32 @@ const SettingsAbout: React.FC = () => {
|
||||
<List title={intl.formatMessage(messages.gettingsupport)}>
|
||||
<List.Item title={intl.formatMessage(messages.documentation)}>
|
||||
<a
|
||||
href="https://docs.overseerr.dev"
|
||||
href="https://github.com/Fallenbagel/jellyseerr/blob/main/README.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://docs.overseerr.dev
|
||||
https://github.com/Fallenbagel/jellyseerr/blob/main/README.md
|
||||
</a>
|
||||
</List.Item>
|
||||
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
|
||||
<a
|
||||
href="https://github.com/sct/overseerr/discussions"
|
||||
href="https://github.com/Fallenbagel/jellyseerr/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://github.com/sct/overseerr/discussions
|
||||
https://github.com/Fallenbagel/jellyseerr/discussions
|
||||
</a>
|
||||
</List.Item>
|
||||
<List.Item title="Discord">
|
||||
<a
|
||||
href="https://discord.gg/overseerr"
|
||||
href="https://discord.gg/XDyAd3AuUV"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://discord.gg/overseerr
|
||||
https://discord.gg/XDyAd3AuUV
|
||||
</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
@@ -151,27 +151,17 @@ const SettingsAbout: React.FC = () => {
|
||||
title={`${intl.formatMessage(messages.helppaycoffee)} ☕️`}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/sponsors/sct"
|
||||
href="https://www.buymeacoffee.com/fallen.bagel"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://github.com/sponsors/sct
|
||||
https://www.buymeacoffee.com/fallen.bagel
|
||||
</a>
|
||||
<Badge className="ml-2">
|
||||
{intl.formatMessage(messages.preferredmethod)}
|
||||
</Badge>
|
||||
</List.Item>
|
||||
<List.Item title="">
|
||||
<a
|
||||
href="https://patreon.com/overseerr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-indigo-500 hover:underline"
|
||||
>
|
||||
https://patreon.com/overseerr
|
||||
</a>
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
<div className="section">
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import type { JellyfinSettings } from '../../../server/lib/settings';
|
||||
import useSWR from 'swr';
|
||||
import Button from '../Common/Button';
|
||||
import axios from 'axios';
|
||||
import LibraryItem from './LibraryItem';
|
||||
import Badge from '../Common/Badge';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { JellyfinSettings } from '../../../server/lib/settings';
|
||||
import Badge from '../Common/Badge';
|
||||
import Button from '../Common/Button';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import LibraryItem from './LibraryItem';
|
||||
|
||||
const messages = defineMessages({
|
||||
jellyfinsettings: 'Jellyfin Settings',
|
||||
jellyfinsettingsDescription:
|
||||
'Configure the settings for your Jellyfin server. Overseerr scans your Jellyfin libraries to see what content is available.',
|
||||
'Configure the settings for your Jellyfin server. Jellyseerr scans your Jellyfin libraries to see what content is available.',
|
||||
timeout: 'Timeout',
|
||||
save: 'Save Changes',
|
||||
saving: 'Saving…',
|
||||
jellyfinlibraries: 'Jellyfin Libraries',
|
||||
jellyfinlibrariesDescription:
|
||||
'The libraries Overseerr scans for titles. Click the button below if no libraries are listed.',
|
||||
'The libraries Jellyseerr scans for titles. Click the button below if no libraries are listed.',
|
||||
syncing: 'Syncing',
|
||||
syncJellyfin: 'Sync Libraries',
|
||||
manualscanJellyfin: 'Manual Library Scan',
|
||||
manualscanDescriptionJellyfin:
|
||||
"Normally, this will only be run once every 24 hours. Overseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
||||
"Normally, this will only be run once every 24 hours. Jellyseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
||||
notrunning: 'Not Running',
|
||||
currentlibrary: 'Current Library: {name}',
|
||||
librariesRemaining: 'Libraries Remaining: {count}',
|
||||
|
||||
@@ -23,7 +23,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
jobsandcache: 'Jobs & Cache',
|
||||
jobs: 'Jobs',
|
||||
jobsDescription:
|
||||
'Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
|
||||
'Jellyseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
|
||||
jobname: 'Job Name',
|
||||
jobtype: 'Type',
|
||||
nextexecution: 'Next Execution',
|
||||
@@ -35,7 +35,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
command: 'Command',
|
||||
cache: 'Cache',
|
||||
cacheDescription:
|
||||
'Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
|
||||
'Jellyseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
|
||||
cacheflushed: '{cachename} cache flushed.',
|
||||
cachename: 'Cache Name',
|
||||
cachehits: 'Hits',
|
||||
|
||||
@@ -31,7 +31,7 @@ import Transition from '../../Transition';
|
||||
const messages = defineMessages({
|
||||
logs: 'Logs',
|
||||
logsDescription:
|
||||
'You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.',
|
||||
'You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/jellyseerr.log</code>.',
|
||||
time: 'Timestamp',
|
||||
level: 'Severity',
|
||||
label: 'Label',
|
||||
|
||||
@@ -29,7 +29,7 @@ const messages = defineMessages({
|
||||
general: 'General',
|
||||
generalsettings: 'General Settings',
|
||||
generalsettingsDescription:
|
||||
'Configure global and default settings for Overseerr.',
|
||||
'Configure global and default settings for Jellyseerr.',
|
||||
apikey: 'API Key',
|
||||
applicationTitle: 'Application Title',
|
||||
applicationurl: 'Application URL',
|
||||
@@ -44,7 +44,7 @@ const messages = defineMessages({
|
||||
hideAvailable: 'Hide Available Media',
|
||||
csrfProtection: 'Enable CSRF Protection',
|
||||
csrfProtectionTip:
|
||||
'Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)',
|
||||
'Set external API access to read-only (requires HTTPS, and Jellyseerr must be reloaded for changes to take effect)',
|
||||
csrfProtectionHoverTip:
|
||||
'Do NOT enable this setting unless you understand what you are doing!',
|
||||
cacheImages: 'Enable Image Caching',
|
||||
@@ -52,7 +52,7 @@ const messages = defineMessages({
|
||||
'Optimize and store all images locally (consumes a significant amount of disk space)',
|
||||
trustProxy: 'Enable Proxy Support',
|
||||
trustProxyTip:
|
||||
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)',
|
||||
'Allow Jellyseerr to correctly register client IP addresses behind a proxy (Jellyseerr must be reloaded for changes to take effect)',
|
||||
validationApplicationTitle: 'You must provide an application title',
|
||||
validationApplicationUrl: 'You must provide a valid URL',
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
|
||||
@@ -22,7 +22,7 @@ const messages = defineMessages({
|
||||
plex: 'Plex',
|
||||
plexsettings: 'Plex Settings',
|
||||
plexsettingsDescription:
|
||||
'Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.',
|
||||
'Configure the settings for your Plex server. Jellyseerr scans your Plex libraries to determine content availability.',
|
||||
serverpreset: 'Server',
|
||||
serverLocal: 'local',
|
||||
serverRemote: 'remote',
|
||||
@@ -43,12 +43,12 @@ const messages = defineMessages({
|
||||
enablessl: 'Use SSL',
|
||||
plexlibraries: 'Plex Libraries',
|
||||
plexlibrariesDescription:
|
||||
'The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.',
|
||||
'The libraries Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.',
|
||||
scanning: 'Syncing…',
|
||||
scan: 'Sync Libraries',
|
||||
manualscan: 'Manual Library Scan',
|
||||
manualscanDescription:
|
||||
"Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||
"Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||
notrunning: 'Not Running',
|
||||
currentlibrary: 'Current Library: {name}',
|
||||
librariesRemaining: 'Libraries Remaining: {count}',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useUser } from '../../hooks/useUser';
|
||||
import PlexLoginButton from '../PlexLoginButton';
|
||||
|
||||
const messages = defineMessages({
|
||||
welcome: 'Welcome to Overseerr',
|
||||
welcome: 'Welcome to Jellyseerr',
|
||||
signinMessage: 'Get started by signing in with your Plex account',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useUser } from '../../hooks/useUser';
|
||||
import PlexLoginButton from '../PlexLoginButton';
|
||||
import JellyfinLogin from '../Login/JellyfinLogin';
|
||||
import axios from 'axios';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import Accordion from '../Common/Accordion';
|
||||
import { MediaServerType } from '../../../server/constants/server';
|
||||
import { useUser } from '../../hooks/useUser';
|
||||
import Accordion from '../Common/Accordion';
|
||||
import JellyfinLogin from '../Login/JellyfinLogin';
|
||||
import PlexLoginButton from '../PlexLoginButton';
|
||||
|
||||
const messages = defineMessages({
|
||||
welcome: 'Welcome to Overseerr',
|
||||
welcome: 'Welcome to Jellyseerr',
|
||||
signinMessage: 'Get started by signing in',
|
||||
signinWithJellyfin: 'Use your Jellyfin account',
|
||||
signinWithPlex: 'Use your Plex account',
|
||||
|
||||
@@ -13,8 +13,11 @@ interface StatusBadgeProps {
|
||||
status?: MediaStatus;
|
||||
is4k?: boolean;
|
||||
inProgress?: boolean;
|
||||
plexUrl?: string;
|
||||
tmdbId?: number;
|
||||
mediaUrl?: string;
|
||||
mediaUrl4k?: string;
|
||||
mediaType?: 'movie' | 'tv';
|
||||
}
|
||||
|
||||
const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface SettingsContextProps {
|
||||
|
||||
const defaultSettings = {
|
||||
initialized: false,
|
||||
applicationTitle: 'Overseerr',
|
||||
applicationTitle: 'Jellyseerr',
|
||||
applicationUrl: '',
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"components.RequestList.RequestItem.requested": "Sol·licitat",
|
||||
"components.PermissionEdit.request": "Sol·licitud",
|
||||
"components.RequestBlock.seasons": "{seasonCount, plural, one {Temporada} other {Temporades}}",
|
||||
"components.PermissionEdit.usersDescription": "Concedeix permís per gestionar els usuaris d'Overseerr. Els usuaris amb aquest permís no poden modificar els usuaris administrador ni concedir aquest privilegi a altres.",
|
||||
"components.PermissionEdit.usersDescription": "Concedeix permís per gestionar els usuaris d'Jellyseerr. Els usuaris amb aquest permís no poden modificar els usuaris administrador ni concedir aquest privilegi a altres.",
|
||||
"components.PermissionEdit.request4k": "Sol·licita 4K",
|
||||
"components.MovieDetails.revenue": "Recaptació",
|
||||
"components.RequestCard.seasons": "{seasonCount, plural, one {Temporada} other {Temporades}}",
|
||||
@@ -89,7 +89,7 @@
|
||||
"components.PermissionEdit.viewrequestsDescription": "Concedeix permís per veure les sol·licituds d'altres usuaris.",
|
||||
"components.PermissionEdit.viewrequests": "Veure sol·licituds",
|
||||
"components.PermissionEdit.users": "Gestiona els usuaris",
|
||||
"components.PermissionEdit.settingsDescription": "Concedeix permís per modificar els paràmetres Overseerr. Un usuari ha de tenir aquest permís per concedir-lo a altres persones.",
|
||||
"components.PermissionEdit.settingsDescription": "Concedeix permís per modificar els paràmetres Jellyseerr. Un usuari ha de tenir aquest permís per concedir-lo a altres persones.",
|
||||
"components.PermissionEdit.settings": "Gestiona la configuració",
|
||||
"components.PermissionEdit.requestDescription": "Concedeix permís per sol·licitar contingut no 4K.",
|
||||
"components.PermissionEdit.request4kTvDescription": "Concedeix permís per sol·licitar sèrie en 4K.",
|
||||
@@ -97,7 +97,7 @@
|
||||
"components.PermissionEdit.request4kMoviesDescription": "Concedeix permís per sol·licitar pel·lícules en 4K.",
|
||||
"components.PermissionEdit.request4kMovies": "Sol·liciteu pel·lícules en 4K",
|
||||
"components.PermissionEdit.request4kDescription": "Concedeix permís per sol·licitar contingut en 4K.",
|
||||
"components.PermissionEdit.managerequestsDescription": "Concedeix permís per gestionar les sol·licituds d'Overseerr. Totes les sol·licituds que faci un usuari amb aquest permís s’aprovaran automàticament.",
|
||||
"components.PermissionEdit.managerequestsDescription": "Concedeix permís per gestionar les sol·licituds d'Jellyseerr. Totes les sol·licituds que faci un usuari amb aquest permís s’aprovaran automàticament.",
|
||||
"components.PermissionEdit.managerequests": "Gestiona les sol·licituds",
|
||||
"components.PermissionEdit.autoapproveSeriesDescription": "Concedeix l’aprovació automàtica de les sol·licituds de sèries que no siguin 4K.",
|
||||
"components.PermissionEdit.autoapproveSeries": "Aprovació automàtica de sèries",
|
||||
@@ -438,9 +438,9 @@
|
||||
"components.TvDetails.TvCrew.fullseriescrew": "Equip complet de la sèrie",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Repartiment complet de la sèrie",
|
||||
"components.StatusChacker.newversionavailable": "Actualització de l'aplicació",
|
||||
"components.StatusChacker.newversionDescription": "Overseerr s'ha actualitzat! Feu clic al botó següent per tornar a carregar la pàgina.",
|
||||
"components.StatusChacker.newversionDescription": "Jellyseerr s'ha actualitzat! Feu clic al botó següent per tornar a carregar la pàgina.",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"components.Setup.welcome": "Benvingut a Overseerr",
|
||||
"components.Setup.welcome": "Benvingut a Jellyseerr",
|
||||
"components.Setup.tip": "Consell",
|
||||
"components.Setup.signinMessage": "Comenceu iniciant sessió amb el vostre compte de Plex",
|
||||
"components.Setup.setup": "Configuració",
|
||||
@@ -459,7 +459,7 @@
|
||||
"components.Settings.validationApplicationUrlTrailingSlash": "L'URL no pot acabar amb una barra inclinada final",
|
||||
"components.Settings.validationApplicationUrl": "Heu de proporcionar un URL vàlid",
|
||||
"components.Settings.validationApplicationTitle": "Heu de proporcionar un títol d'aplicació",
|
||||
"components.Settings.trustProxyTip": "Permet a Overseerr registrar correctament les adreces IP del client darrere d’un servidor intermediari (s’ha de tornar a carregar Overseerr perquè els canvis tinguin efecte)",
|
||||
"components.Settings.trustProxyTip": "Permet a Jellyseerr registrar correctament les adreces IP del client darrere d’un servidor intermediari (s’ha de tornar a carregar Jellyseerr perquè els canvis tinguin efecte)",
|
||||
"components.Settings.toastSettingsSuccess": "La configuració s'ha desat correctament!",
|
||||
"components.Settings.toastSettingsFailure": "S'ha produït un error en desar la configuració.",
|
||||
"components.Settings.toastPlexRefreshSuccess": "La llista de servidors Plex s'ha recuperat correctament!",
|
||||
@@ -486,15 +486,15 @@
|
||||
"components.Settings.region": "Regió per a la secció \"Descobriu\"",
|
||||
"components.Settings.radarrsettings": "Configuració de Radarr",
|
||||
"components.Settings.port": "Port",
|
||||
"components.Settings.plexsettingsDescription": "Configureu la paràmetres del vostre servidor Plex. Overseerr explora les vostres biblioteques Plex per determinar la disponibilitat de continguts.",
|
||||
"components.Settings.plexsettingsDescription": "Configureu la paràmetres del vostre servidor Plex. Jellyseerr explora les vostres biblioteques Plex per determinar la disponibilitat de continguts.",
|
||||
"components.Settings.plexsettings": "Configuració de Plex",
|
||||
"components.Settings.plexlibrariesDescription": "Les biblioteques en les que Overseerr explora títols. Configureu i deseu la configuració de la connexió Plex i feu clic al botó següent si no apareix cap.",
|
||||
"components.Settings.plexlibrariesDescription": "Les biblioteques en les que Jellyseerr explora títols. Configureu i deseu la configuració de la connexió Plex i feu clic al botó següent si no apareix cap.",
|
||||
"components.Settings.plexlibraries": "Biblioteques Plex",
|
||||
"components.Settings.plex": "Plex",
|
||||
"components.Settings.partialRequestsEnabled": "Permet sol·licituds parcials de Sèries",
|
||||
"components.Settings.originallanguageTip": "Filtra el contingut per l'idioma original",
|
||||
"components.Settings.originallanguage": "Idioma per a la secció \"Descobriu\"",
|
||||
"components.Settings.manualscanDescription": "Normalment, només s’executarà una vegada cada 24 hores. Overseerr comprovarà de forma més agressiva el contingut afegit recentment del seu servidor Plex. Si és la primera vegada que configureu Plex, es recomana fer una exploració manual completa de la biblioteca!",
|
||||
"components.Settings.manualscanDescription": "Normalment, només s’executarà una vegada cada 24 hores. Jellyseerr comprovarà de forma més agressiva el contingut afegit recentment del seu servidor Plex. Si és la primera vegada que configureu Plex, es recomana fer una exploració manual completa de la biblioteca!",
|
||||
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Exploració d'elements de Plex afegits recentment",
|
||||
"components.Settings.notrunning": "No s'està executant",
|
||||
"components.Settings.notificationsettings": "Configuració de les notificacions",
|
||||
@@ -507,7 +507,7 @@
|
||||
"components.Settings.menuLogs": "Registres",
|
||||
"components.Settings.menuJobs": "Tasques programades i memòria cau",
|
||||
"components.Settings.hideAvailable": "Amaga els suports disponibles",
|
||||
"components.Settings.generalsettingsDescription": "Configureu els paràmetres globals i predeterminats per a Overseerr.",
|
||||
"components.Settings.generalsettingsDescription": "Configureu els paràmetres globals i predeterminats per a Jellyseerr.",
|
||||
"components.Settings.menuGeneralSettings": "General",
|
||||
"components.Settings.menuAbout": "Quant a",
|
||||
"components.Settings.manualscan": "Exploració manual de la biblioteca",
|
||||
@@ -526,7 +526,7 @@
|
||||
"components.Settings.email": "Adreça electrònica",
|
||||
"components.Settings.default4k": "4K predeterminat",
|
||||
"components.Settings.default": "Predeterminat",
|
||||
"components.Settings.csrfProtectionTip": "Estableix l'accés a l'API externa de només lectura (requereix HTTPS i s'ha de tornar a carregar Overseerr perquè els canvis tinguin efecte)",
|
||||
"components.Settings.csrfProtectionTip": "Estableix l'accés a l'API externa de només lectura (requereix HTTPS i s'ha de tornar a carregar Jellyseerr perquè els canvis tinguin efecte)",
|
||||
"components.Settings.csrfProtectionHoverTip": "NO activeu aquesta configuració tret que entengueu el que esteu fent!",
|
||||
"components.Settings.csrfProtection": "Activeu la protecció CSRF",
|
||||
"components.Settings.copied": "S'ha copiat la clau API al porta-retalls.",
|
||||
@@ -618,7 +618,7 @@
|
||||
"components.Settings.SettingsUsers.defaultPermissions": "Permisos per defecte",
|
||||
"components.Settings.SettingsLogs.showall": "Mostra tots els registres",
|
||||
"components.Settings.SettingsLogs.message": "Missatge",
|
||||
"components.Settings.SettingsLogs.logsDescription": "També podeu veure aquests registres directament mitjançant <code>stdout</code> o a <code>{configDir}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logsDescription": "També podeu veure aquests registres directament mitjançant <code>stdout</code> o a <code>{configDir}/logs/jellyseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logs": "Registres",
|
||||
"components.Settings.SettingsLogs.logDetails": "Detalls del registre",
|
||||
"components.Settings.SettingsLogs.level": "Gravetat",
|
||||
@@ -631,7 +631,7 @@
|
||||
"components.Settings.SettingsJobsCache.process": "Procés",
|
||||
"components.Settings.SettingsJobsCache.jobtype": "Tipus",
|
||||
"components.Settings.SettingsJobsCache.jobsandcache": "Tasques programades i memòria cau",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr realitza certes tasques de manteniment com a feines programades regularment, però també es poden activar manualment a continuació. L’execució manual d’un treball no alterarà la seva programació.",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Jellyseerr realitza certes tasques de manteniment com a feines programades regularment, però també es poden activar manualment a continuació. L’execució manual d’un treball no alterarà la seva programació.",
|
||||
"components.Settings.SettingsJobsCache.jobs": "Tasques programades",
|
||||
"components.Settings.SettingsJobsCache.jobname": "Nom de la tasca programada",
|
||||
"components.Settings.SettingsJobsCache.download-sync-reset": "Restableix la sincronització de descàrregues",
|
||||
@@ -639,12 +639,12 @@
|
||||
"components.Settings.SettingsJobsCache.canceljob": "Cancel·la la tasca programada",
|
||||
"components.Settings.SettingsJobsCache.cachemisses": "Errades",
|
||||
"components.Settings.SettingsJobsCache.cachehits": "Consultes",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr emmagatzema a la memòria cau les sol·licituds als punts finals de l'API externa per optimitzar el rendiment i evitar fer peticions d'API innecessàries.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr emmagatzema a la memòria cau les sol·licituds als punts finals de l'API externa per optimitzar el rendiment i evitar fer peticions d'API innecessàries.",
|
||||
"components.Settings.SettingsJobsCache.cache": "Memòria cau",
|
||||
"components.Settings.SettingsAbout.version": "Versió",
|
||||
"components.Settings.SettingsAbout.timezone": "Zona horària",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Dóna suport a Overseerr",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Informació d'Overseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Dóna suport a Jellyseerr",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Informació d'Jellyseerr",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Ajudeu-me convidant-me a un cafè",
|
||||
"components.Settings.SettingsAbout.documentation": "Documentació",
|
||||
"components.Settings.SettingsAbout.about": "Quant a",
|
||||
@@ -666,7 +666,7 @@
|
||||
"components.Settings.Notifications.smtpHost": "Amfitrió SMTP",
|
||||
"components.Settings.Notifications.emailsettingsfailed": "No s'ha pogut desar la configuració de les notificacions per correu electrònic.",
|
||||
"components.Settings.RadarrModal.apiKey": "Clau API",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Els últims canvis a la branca <code>develop</code> de Overseerr no es mostren a continuació. Vegeu l'historial de pujades d'aquesta branca a <GithubLink>GitHub</GithubLink> per a més detalls.",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Els últims canvis a la branca <code>develop</code> de Jellyseerr no es mostren a continuació. Vegeu l'historial de pujades d'aquesta branca a <GithubLink>GitHub</GithubLink> per a més detalls.",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Versions",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "La informació de la versió no està disponible. GitHub està fora de línia?",
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Última versió",
|
||||
@@ -772,8 +772,8 @@
|
||||
"components.RequestCard.mediaerror": "El títol associat per a aquesta sol·licitud ja no està disponible.",
|
||||
"components.RequestCard.deleterequest": "Suprimeix la sol·licitud",
|
||||
"components.NotificationTypeSelector.notificationTypes": "Tipus de notificacions",
|
||||
"components.Layout.VersionStatus.streamstable": "Overseer (Estable)",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr (Desenvolupament)",
|
||||
"components.Layout.VersionStatus.streamstable": "Jellyseer (Estable)",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr (Desenvolupament)",
|
||||
"components.Layout.VersionStatus.outofdate": "No està actualitzat",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {canvi} other {canvis}} posterior(s)",
|
||||
"components.Discover.noRequests": "No hi ha cap sol·licitud.",
|
||||
@@ -844,11 +844,11 @@
|
||||
"components.Settings.Notifications.encryptionDefault": "Utilitzeu STARTTLS si està disponible",
|
||||
"components.Settings.Notifications.encryption": "Mètode de xifratge",
|
||||
"components.Settings.Notifications.chatIdTip": "Inicieu un xat amb el bot, afegiu <GetIdBotLink>@get_id_bot</GetIdBotLink> i indiqueu l'ordre <code>/my_id</code>",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Creeu un bot</CreateBotLink> per utilitzar-lo amb Overseerr",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Per tal de rebre notificacions push web, Overseerr s'ha de servir mitjançant HTTPS.",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Creeu un bot</CreateBotLink> per utilitzar-lo amb Jellyseerr",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Per tal de rebre notificacions push web, Jellyseerr s'ha de servir mitjançant HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Creeu una integració <WebhookLink>Webhook entrant</WebhookLink>",
|
||||
"components.Settings.Notifications.NotificationsPushover.userTokenTip": "El vostre <UsersGroupsLink>identificador d'usuari o grup</UsersGroupsLink>",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Registreu una aplicació</ApplicationRegistrationLink> per utilitzar-la amb Overseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Registreu una aplicació</ApplicationRegistrationLink> per utilitzar-la amb Jellyseerr",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Creeu un testimoni a partir de la <PushbulletSettingsLink>Configuració del compte</PushbulletSettingsLink>",
|
||||
"components.Settings.validationWebAppUrl": "Heu de proporcionar un URL d'aplicació web Plex vàlid",
|
||||
"components.Settings.Notifications.webhookUrlTip": "Creeu una <DiscordWebhookLink>integració de webhook</DiscordWebhookLink> al vostre servidor",
|
||||
|
||||
@@ -116,11 +116,11 @@
|
||||
"components.Settings.default4k": "Standard-4K",
|
||||
"components.Settings.deleteserverconfirm": "Bist du sicher, dass du diesen Server löschen möchtest?",
|
||||
"components.Settings.generalsettings": "Allgemeine Einstellungen",
|
||||
"components.Settings.generalsettingsDescription": "Konfiguriere Globale und Standard Overseerr-Einstellungen.",
|
||||
"components.Settings.generalsettingsDescription": "Konfiguriere Globale und Standard Jellyseerr-Einstellungen.",
|
||||
"components.Settings.hostname": "Hostname oder IP-Adresse",
|
||||
"components.Settings.librariesRemaining": "Verbleibende Bibliotheken: {count}",
|
||||
"components.Settings.manualscan": "Manuelle Bibliotheksdurchsuchung",
|
||||
"components.Settings.manualscanDescription": "Normalerweise wird dies nur einmal alle 24 Stunden ausgeführt. Overseerr überprüft die kürzlich hinzugefügten Plex-Server aggressiver. Falls du Plex zum ersten Mal konfigurierst, wird eine einmalige vollständige manuelle Bibliotheksdurchsuchung empfohlen!",
|
||||
"components.Settings.manualscanDescription": "Normalerweise wird dies nur einmal alle 24 Stunden ausgeführt. Jellyseerr überprüft die kürzlich hinzugefügten Plex-Server aggressiver. Falls du Plex zum ersten Mal konfigurierst, wird eine einmalige vollständige manuelle Bibliotheksdurchsuchung empfohlen!",
|
||||
"components.Settings.menuAbout": "Über",
|
||||
"components.Settings.menuGeneralSettings": "Allgemein",
|
||||
"components.Settings.menuJobs": "Aufgaben und Zwischenspeicher",
|
||||
@@ -131,9 +131,9 @@
|
||||
"components.Settings.notificationsettings": "Benachrichtigungseinstellungen",
|
||||
"components.Settings.notrunning": "Nicht aktiv",
|
||||
"components.Settings.plexlibraries": "Plex-Bibliotheken",
|
||||
"components.Settings.plexlibrariesDescription": "Die Bibliotheken, welche Overseerr nach Titeln durchsucht. Richte deine Plex-Verbindungseinstellungen ein und speichere sie, klicke auf die Schaltfläche unten, wenn keine aufgeführt sind.",
|
||||
"components.Settings.plexlibrariesDescription": "Die Bibliotheken, welche Jellyseerr nach Titeln durchsucht. Richte deine Plex-Verbindungseinstellungen ein und speichere sie, klicke auf die Schaltfläche unten, wenn keine aufgeführt sind.",
|
||||
"components.Settings.plexsettings": "Plex-Einstellungen",
|
||||
"components.Settings.plexsettingsDescription": "Konfiguriere die Einstellungen für deinen Plex-Server. Overseerr durchsucht deine Plex-Bibliotheken, um festzustellen welche Inhalte verfügbar sind.",
|
||||
"components.Settings.plexsettingsDescription": "Konfiguriere die Einstellungen für deinen Plex-Server. Jellyseerr durchsucht deine Plex-Bibliotheken, um festzustellen welche Inhalte verfügbar sind.",
|
||||
"components.Settings.port": "Port",
|
||||
"components.Settings.radarrsettings": "Radarr-Einstellungen",
|
||||
"components.Settings.sonarrsettings": "Sonarr-Einstellungen",
|
||||
@@ -146,7 +146,7 @@
|
||||
"components.Setup.finishing": "Fertigstellung …",
|
||||
"components.Setup.loginwithplex": "Mit Plex anmelden",
|
||||
"components.Setup.signinMessage": "Melde dich zunächst mit deinem Plex-Konto an",
|
||||
"components.Setup.welcome": "Willkommen bei Overseerr",
|
||||
"components.Setup.welcome": "Willkommen bei Jellyseerr",
|
||||
"components.TvDetails.cast": "Besetzung",
|
||||
"components.TvDetails.manageModalClearMedia": "Mediendaten löschen",
|
||||
"components.TvDetails.manageModalClearMediaWarning": "* Dies wird unwiederbringlich alle Daten zu dieser Serie löschen, inklusive jeglicher Anfrage dafür. Falls dieses Element in deiner Plex-Bibliothek existiert, werden die Medieninformationen beim nächsten Scannen neu erstellt.",
|
||||
@@ -193,7 +193,7 @@
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Anfragen insgesamt",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Medien insgesamt",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr-Informationen",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Jellyseerr-Informationen",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub-Diskussionen",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Hilfe erhalten",
|
||||
"components.Settings.RadarrModal.validationNameRequired": "Du musst einen Servernamen angeben",
|
||||
@@ -224,12 +224,12 @@
|
||||
"components.MovieDetails.studio": "{studioCount, plural, one {Studio} other {Studios}}",
|
||||
"i18n.close": "Schließen",
|
||||
"components.Settings.SettingsAbout.timezone": "Zeitzone",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Unterstütze Overseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Unterstütze Jellyseerr",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Hilf uns Kaffee zu bezahlen",
|
||||
"components.Settings.SettingsAbout.Releases.viewongithub": "Auf GitHub anzeigen",
|
||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "Änderungsprotokoll anzeigen",
|
||||
"components.Settings.SettingsAbout.Releases.versionChangelog": "Änderungsprotokoll",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Die neuesten Änderungen am <code>Entwicklungszweig</code> von Overseerr werden unten nicht angezeigt. Weitere Informationen findest du im Commit-Verlauf für diesen Zweig auf <GithubLink>GitHub</GithubLink>.",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Die neuesten Änderungen am <code>Entwicklungszweig</code> von Jellyseerr werden unten nicht angezeigt. Weitere Informationen findest du im Commit-Verlauf für diesen Zweig auf <GithubLink>GitHub</GithubLink>.",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Veröffentlichungen",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Informationen der Version nicht verfügbar. Ist GitHub offline?",
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Neuste",
|
||||
@@ -266,9 +266,9 @@
|
||||
"components.Settings.Notifications.senderName": "Absendername",
|
||||
"components.Settings.Notifications.chatId": "Chat-ID",
|
||||
"components.Settings.Notifications.botAPI": "Bot-Autorisierungstoken",
|
||||
"components.StatusChacker.reloadOverseerr": "Overseerr neu laden",
|
||||
"components.StatusChacker.reloadOverseerr": "Jellyseerr neu laden",
|
||||
"components.StatusChacker.newversionavailable": "Anwendungsaktualisierung",
|
||||
"components.StatusChacker.newversionDescription": "Overseerr wurde aktualisiert! Bitte klicke auf die Schaltfläche unten, um die Seite neu zu laden.",
|
||||
"components.StatusChacker.newversionDescription": "Jellyseerr wurde aktualisiert! Bitte klicke auf die Schaltfläche unten, um die Seite neu zu laden.",
|
||||
"components.Settings.SettingsAbout.documentation": "Dokumentation",
|
||||
"components.NotificationTypeSelector.mediarequestedDescription": "Sende Benachrichtigungen, wenn neue Medien angefordert wurden und auf Genehmigung warten.",
|
||||
"components.NotificationTypeSelector.mediarequested": "Medien angefordert",
|
||||
@@ -368,7 +368,7 @@
|
||||
"components.PermissionEdit.request": "Anfrage",
|
||||
"components.PermissionEdit.autoapproveMovies": "Automatische Genehmigung von Filmen",
|
||||
"components.PermissionEdit.admin": "Admin",
|
||||
"components.PermissionEdit.managerequestsDescription": "Gewähre Berechtigung zum Verwalten von Overseerr-Anfragen. Alle Anfragen eines Benutzers mit dieser Berechtigung werden automatisch genehmigt.",
|
||||
"components.PermissionEdit.managerequestsDescription": "Gewähre Berechtigung zum Verwalten von Jellyseerr-Anfragen. Alle Anfragen eines Benutzers mit dieser Berechtigung werden automatisch genehmigt.",
|
||||
"components.UserList.userssaved": "Benutzerberechtigungen erfolgreich gespeichert!",
|
||||
"components.UserList.bulkedit": "Ausgewählte bearbeiten",
|
||||
"components.Settings.toastPlexRefreshSuccess": "Plex-Serverliste erfolgreich abgerufen!",
|
||||
@@ -384,13 +384,13 @@
|
||||
"components.Settings.serverpreset": "Server",
|
||||
"components.Settings.serverRemote": "entfernt",
|
||||
"components.Settings.serverLocal": "lokal",
|
||||
"components.Settings.csrfProtectionTip": "Macht den externen API Zugang schreibgeschützt (setzt HTTPS voraus und Overseerr muss neu gestartet werden, damit die Änderungen wirksam werden)",
|
||||
"components.Settings.csrfProtectionTip": "Macht den externen API Zugang schreibgeschützt (setzt HTTPS voraus und Jellyseerr muss neu gestartet werden, damit die Änderungen wirksam werden)",
|
||||
"components.Settings.csrfProtection": "Aktiviere CSRF Schutz",
|
||||
"components.Settings.SonarrModal.toastSonarrTestSuccess": "Sonarr-Verbindung erfolgreich hergestellt!",
|
||||
"components.Settings.SonarrModal.toastSonarrTestFailure": "Verbindung zu Sonarr fehlgeschlagen.",
|
||||
"components.PermissionEdit.usersDescription": "Gewähre Berechtigung zum Verwalten von Overseerr-Benutzern. Benutzer mit dieser Berechtigung können Benutzer mit Adminrechten nicht bearbeiten oder Adminrechte erteilen.",
|
||||
"components.PermissionEdit.usersDescription": "Gewähre Berechtigung zum Verwalten von Jellyseerr-Benutzern. Benutzer mit dieser Berechtigung können Benutzer mit Adminrechten nicht bearbeiten oder Adminrechte erteilen.",
|
||||
"components.PermissionEdit.users": "Benutzer verwalten",
|
||||
"components.PermissionEdit.settingsDescription": "Gewähre Berechtigung zum ändern von Overseerr-Einstellungen. Ein Benutzer muss über diese Berechtigung verfügen, um sie anderen Benutzern erteilen zu können.",
|
||||
"components.PermissionEdit.settingsDescription": "Gewähre Berechtigung zum ändern von Jellyseerr-Einstellungen. Ein Benutzer muss über diese Berechtigung verfügen, um sie anderen Benutzern erteilen zu können.",
|
||||
"components.PermissionEdit.settings": "Einstellungen verwalten",
|
||||
"components.PermissionEdit.requestDescription": "Gewähre Berechtigung zum Anfragen von nicht 4K Medien.",
|
||||
"components.PermissionEdit.request4kTvDescription": "Gewähre Berechtigung Serien in 4K anzufragen.",
|
||||
@@ -421,13 +421,13 @@
|
||||
"components.TvDetails.markavailable": "Als verfügbar markieren",
|
||||
"components.TvDetails.mark4kavailable": "4K als verfügbar markieren",
|
||||
"components.TvDetails.allseasonsmarkedavailable": "* Alle Staffeln werden als verfügbar markiert.",
|
||||
"components.Settings.trustProxyTip": "Erlaubt es Overseerr Client IP Adressen hinter einem Proxy korrekt zu registrieren (Overseerr muss neu gestartet werden, damit die Änderungen wirksam werden)",
|
||||
"components.Settings.trustProxyTip": "Erlaubt es Jellyseerr Client IP Adressen hinter einem Proxy korrekt zu registrieren (Jellyseerr muss neu gestartet werden, damit die Änderungen wirksam werden)",
|
||||
"components.Settings.trustProxy": "Proxy-Unterstützung aktivieren",
|
||||
"components.Settings.SettingsJobsCache.runnow": "Jetzt ausführen",
|
||||
"components.Settings.SettingsJobsCache.nextexecution": "Nächste Ausführung",
|
||||
"components.Settings.SettingsJobsCache.jobtype": "Art",
|
||||
"components.Settings.SettingsJobsCache.jobstarted": "{jobname} gestartet.",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr führt bestimmte Wartungsaufgaben als regulär geplante Aufgaben durch, aber sie können auch manuell ausgeführt werden. Manuelles Ausführen einer Aufgabe ändert ihren Zeitplan nicht.",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Jellyseerr führt bestimmte Wartungsaufgaben als regulär geplante Aufgaben durch, aber sie können auch manuell ausgeführt werden. Manuelles Ausführen einer Aufgabe ändert ihren Zeitplan nicht.",
|
||||
"components.Settings.SettingsJobsCache.jobs": "Aufgaben",
|
||||
"components.Settings.SettingsJobsCache.jobname": "Aufgabenname",
|
||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} abgebrochen.",
|
||||
@@ -440,7 +440,7 @@
|
||||
"components.Settings.SettingsJobsCache.cachekeys": "Schlüssel insgesamt",
|
||||
"components.Settings.SettingsJobsCache.cachehits": "Treffer",
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} Zwischenspeicher geleert.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr speichert Anfragen an externe API Endpunkte zwischen, um die Leistung zu optimieren und unnötige API Aufrufe zu minimieren.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr speichert Anfragen an externe API Endpunkte zwischen, um die Leistung zu optimieren und unnötige API Aufrufe zu minimieren.",
|
||||
"components.Settings.SettingsJobsCache.cache": "Zwischenspeicher",
|
||||
"components.MovieDetails.markavailable": "Als verfügbar markieren",
|
||||
"components.MovieDetails.mark4kavailable": "4K als verfügbar markieren",
|
||||
@@ -652,7 +652,7 @@
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Fortsetzen",
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Pause",
|
||||
"components.Settings.SettingsLogs.message": "Nachricht",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Du kannst diese Protokolle auch direkt über <code>stdout</code> oder in <code>{configDir} /logs/overseerr.log</code> anzeigen.",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Du kannst diese Protokolle auch direkt über <code>stdout</code> oder in <code>{configDir} /logs/jellyseerr.log</code> anzeigen.",
|
||||
"components.Settings.SettingsLogs.logs": "Protokolle",
|
||||
"components.Settings.SettingsLogs.logDetails": "Protokolldetails",
|
||||
"components.Settings.SettingsLogs.level": "Schweregrad",
|
||||
@@ -773,8 +773,8 @@
|
||||
"components.QuotaSelector.unlimited": "Unbegrenzt",
|
||||
"components.NotificationTypeSelector.notificationTypes": "Benachrichtigungstypen",
|
||||
"components.MovieDetails.originaltitle": "Originaltitel",
|
||||
"components.Layout.VersionStatus.streamstable": "Overseerr Stabil",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr Entwicklung",
|
||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stabil",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Entwicklung",
|
||||
"components.Layout.VersionStatus.outofdate": "Veraltet",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {Version} other {Versionen}} hinterher",
|
||||
"components.LanguageSelector.originalLanguageDefault": "Alle Sprachen",
|
||||
@@ -789,7 +789,7 @@
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Web push test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web push Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Um web push Benachrichtigungen zu erhalten, muss Overseerr über HTTPS gehen.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Um web push Benachrichtigungen zu erhalten, muss Jellyseerr über HTTPS gehen.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.agentenabled": "Agent aktivieren",
|
||||
"components.Settings.Notifications.NotificationsSlack.validationTypes": "Sie müssen mindestens einen Benachrichtigungstypen auswählen",
|
||||
"components.Settings.Notifications.NotificationsSlack.toastSlackTestSuccess": "Slack Test Benachrichtigung gesendet!",
|
||||
@@ -799,7 +799,7 @@
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestSuccess": "Pushover Test Benachrichtigung gesendet!",
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestSending": "Pushover Test Benachrichtigung wird gesendet…",
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "Pushover Test Benachrichtigung fehlgeschlagen.",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Registriere eine Anwendung</ApplicationRegistrationLink> für die Benutzung mit Overseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Registriere eine Anwendung</ApplicationRegistrationLink> für die Benutzung mit Jellyseerr",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "Sie müssen mindestens einen Benachrichtigungstypen auswählen",
|
||||
"components.RequestCard.failedretry": "Beim erneuten Versuch die Anfrage zu senden ist ein Fehler aufgetreten.",
|
||||
"components.PermissionEdit.requestTvDescription": "Berechtigungen erteilen um nicht 4K Serien anzufragen.",
|
||||
@@ -854,7 +854,7 @@
|
||||
"components.Settings.Notifications.encryptionTip": "Im Regelfall verwendet Implicit TLS Port 465 und STARTTLS Port 587",
|
||||
"components.Settings.Notifications.encryptionNone": "None",
|
||||
"components.Settings.Notifications.encryptionImplicitTls": "Benutze Implizit TLS",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Erstelle einen Bot</CreateBotLink> für die Verwendung mit Overseerr",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Erstelle einen Bot</CreateBotLink> für die Verwendung mit Jellyseerr",
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Erstelle eine <WebhookLink>Eingehende Webhook</WebhookLink> integration",
|
||||
"components.Settings.Notifications.NotificationsPushover.userTokenTip": "Ihr 30-stelliger <UsersGroupsLink>Nutzer oder Gruppen Identifikator</UsersGroupsLink>",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Pushbullet Test Benachrichtigung fehlgeschlagen.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"components.PermissionEdit.users": "Διαχείριση Χρηστών",
|
||||
"components.PermissionEdit.settingsDescription": "Εκχώρηση άδειας για τροποποίηση των ρυθμίσεων Overseerr. Ένας χρήστης χρειάζεται να έχει αυτό το δικαίωμα για να το εκχωρήσει σε άλλους.",
|
||||
"components.PermissionEdit.settingsDescription": "Εκχώρηση άδειας για τροποποίηση των ρυθμίσεων Jellyseerr. Ένας χρήστης χρειάζεται να έχει αυτό το δικαίωμα για να το εκχωρήσει σε άλλους.",
|
||||
"components.PermissionEdit.settings": "Διαχείριση Ρυθμίσεων",
|
||||
"components.PermissionEdit.requestTvDescription": "Εκχώρηση άδειας για αιτήματα που δεν είναι 4K σειρές.",
|
||||
"components.PermissionEdit.requestTv": "Αιτήματα για Σειρές",
|
||||
@@ -14,7 +14,7 @@
|
||||
"components.PermissionEdit.request4k": "Αίτημα για 4K",
|
||||
"components.PermissionEdit.request4kDescription": "Εκχώρηση άδειας για αιτήματα 4Κ μέσων.",
|
||||
"components.PermissionEdit.request": "Αίτημα",
|
||||
"components.PermissionEdit.managerequestsDescription": "Εκχώρηση άδειας για τη διαχείριση αιτημάτων Overseerr. Όλα τα αιτήματα που υποβάλλει ένας χρήστης με αυτήν την άδεια θα εγκριθούν αυτόματα.",
|
||||
"components.PermissionEdit.managerequestsDescription": "Εκχώρηση άδειας για τη διαχείριση αιτημάτων Jellyseerr. Όλα τα αιτήματα που υποβάλλει ένας χρήστης με αυτήν την άδεια θα εγκριθούν αυτόματα.",
|
||||
"components.PermissionEdit.managerequests": "Διαχείριση Αιτημάτων",
|
||||
"components.PermissionEdit.autoapproveSeriesDescription": "Εκχώρηση αυτόματης έγκρισης για αιτήματα σειρών που δεν είναι 4K.",
|
||||
"components.PermissionEdit.autoapproveSeries": "Αυτόματη έγκριση Σειρών",
|
||||
@@ -201,8 +201,8 @@
|
||||
"components.PersonDetails.lifespan": "{birthdate} – {deathdate}",
|
||||
"components.PersonDetails.birthdate": "Γεννήθηκε {birthdate}",
|
||||
"components.PersonDetails.ascharacter": "ως {character}",
|
||||
"components.PermissionEdit.usersDescription": "Εκχώρηση άδειας για διαχείρηση των Overseerr χρηστών. Οι χρήστες με αυτή την άδεια δεν μπορούν να τροποποιήσουν τους χρήστες με το προνόμιο του διαχειριστή ή να κάνουν κάποιον διαχειριστή.",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr Develop",
|
||||
"components.PermissionEdit.usersDescription": "Εκχώρηση άδειας για διαχείρηση των Jellyseerr χρηστών. Οι χρήστες με αυτή την άδεια δεν μπορούν να τροποποιήσουν τους χρήστες με το προνόμιο του διαχειριστή ή να κάνουν κάποιον διαχειριστή.",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||
"components.CollectionDetails.requestswillbecreated4k": "Οι ακόλουθοι τίτλοι θα έχουν 4K αιτήματα για:",
|
||||
"components.CollectionDetails.requestswillbecreated": "Οι ακόλουθοι τίτλοι θα δημιουργήσουν αιτήματα για:",
|
||||
"components.AppDataWarning.dockerVolumeMissingDescription": "Η <code>{appDataPath}</code> προσάρτηση τόμου δεν έχει ρυθμιστεί σωστά. Όλα τα δεδομένα θα διαγραφούν όταν ο περιέχοντας σταματήσει ή επανεκκινήσει.",
|
||||
@@ -290,7 +290,7 @@
|
||||
"components.Settings.Notifications.botUsernameTip": "Επίτρεψε στους χρήστες να ξεκινήσουν επίσης μια συνομιλία με το bot σου και να ρυθμίσουν τις δικές τους ειδοποιήσεις",
|
||||
"components.Settings.Notifications.botUsername": "Όνομα χρήστη Bot",
|
||||
"components.Settings.Notifications.botAvatarUrl": "Σύνδεσμος για το Avatar του Bot",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Δημιουργία bot</CreateBotLink> για χρήση με Overseerr",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Δημιουργία bot</CreateBotLink> για χρήση με Jellyseerr",
|
||||
"components.Settings.Notifications.botAPI": "Διακριτικό εξουσιοδότησης Bot",
|
||||
"components.Settings.Notifications.authUser": "Όνομα χρήστη SMTP",
|
||||
"components.Settings.Notifications.authPass": "Κωδικός πρόσβασης SMTP",
|
||||
@@ -317,7 +317,7 @@
|
||||
"components.Settings.Notifications.NotificationsWebPush.webpushsettingssaved": "Οι ρυθμίσεις των ειδοποιήσεων push αποθηκεύτηκαν επιτυχώς!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.webpushsettingsfailed": "Οι ρυθμίσεις των ειδοποιήσεων push δεν κατάφεραν να αποθηκευτούν.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Η δοκιμαστική ειδοποίηση push εστάλη!",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Για να λάβεις ειδοποιήσεις push, το Overseerr πρέπει να προβάλλεται μέσω HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Για να λάβεις ειδοποιήσεις push, το Jellyseerr πρέπει να προβάλλεται μέσω HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Η δοκιμαστική ειδοποιήση push δεν κατάφερε να σταλεί.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Αποστολή δοκιμαστικής ειδοποίησης push…",
|
||||
"components.Settings.Notifications.NotificationsWebPush.agentenabled": "Ενεργοποίηση του Μεταφορέα",
|
||||
@@ -337,7 +337,7 @@
|
||||
"components.Settings.Notifications.NotificationsPushbullet.toastPushbulletTestFailed": "Αποτυχία αποστολής δοκιμαστικής ειδοποιήσης Pushbullet.",
|
||||
"components.Settings.Notifications.NotificationsPushover.toastPushoverTestFailed": "Αποτυχία αποστολής δοκιμαστικής ειδοποίησης Pushover.",
|
||||
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Ενεργοποίηση του Μεταφορέα",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Καταχώρηση εφαρμογής</ApplicationRegistrationLink> για χρήση με Overseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Καταχώρηση εφαρμογής</ApplicationRegistrationLink> για χρήση με Jellyseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessToken": "Διακριτικό API Εφαρμογής",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Πρέπει να δώσεις ένα διακριτικό πρόσβασης",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Οι ρυθμίσεις για τις ειδοποιήσεις Pushbullet αποθηκεύτηκαν με επιτυχία!",
|
||||
@@ -365,7 +365,7 @@
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Συνέχιση",
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Παύση",
|
||||
"components.Settings.SettingsLogs.message": "Μήνυμα",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Μπορείτε επίσης να δείτε αυτά τα αρχεία καταγραφής απευθείας μέσω του <code> stdout </code> ή στο <code> {configDir} /logs/overseerr.log </code>.",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Μπορείτε επίσης να δείτε αυτά τα αρχεία καταγραφής απευθείας μέσω του <code> stdout </code> ή στο <code> {configDir} /logs/jellyseerr.log </code>.",
|
||||
"components.Settings.SettingsLogs.logs": "Αρχεία καταγραφής",
|
||||
"components.Settings.SettingsLogs.logDetails": "Λεπτομέρειες αρχείου καταγραφής",
|
||||
"components.Settings.SettingsLogs.level": "Σοβαρότητα",
|
||||
@@ -403,10 +403,10 @@
|
||||
"components.Settings.SettingsJobsCache.cachekeys": "Σύνολο κλειδιών",
|
||||
"components.Settings.SettingsJobsCache.cachehits": "Κλήσεις",
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} εκκαθαρίστηκε η κρυφή μνήμη.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Το Overseerr αποθηκεύει προσωρινά αιτήματα σε εξωτερικά τελικά σημεία API για τη βελτιστοποίηση της απόδοσης και την αποφυγή περιττών κλήσεων API.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Το Jellyseerr αποθηκεύει προσωρινά αιτήματα σε εξωτερικά τελικά σημεία API για τη βελτιστοποίηση της απόδοσης και την αποφυγή περιττών κλήσεων API.",
|
||||
"components.Settings.SettingsJobsCache.cache": "Κρυφή μνήμη",
|
||||
"components.Settings.SettingsAbout.version": "Έκδοση",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Οι τελευταίες αλλαγές στο <code>develop</code> branch του Overseerr δεν φαίνονται παρακάτω. Παρακαλώ ανάτρεξε στο ιστορικό δευσμεύσεων του branch στο <GithubLink>GitHub</GithubLink> για λεπτομέρειες.",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Οι τελευταίες αλλαγές στο <code>develop</code> branch του Jellyseerr δεν φαίνονται παρακάτω. Παρακαλώ ανάτρεξε στο ιστορικό δευσμεύσεων του branch στο <GithubLink>GitHub</GithubLink> για λεπτομέρειες.",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Εκδόσεις",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Οι εκδόσεις λογισμικού δεν είναι διαθέσιμες. Μήπως έχει πέσει το GitHub;",
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Πιο πρόσφατη",
|
||||
@@ -458,9 +458,9 @@
|
||||
"components.Settings.SettingsAbout.totalrequests": "Σύνολο αιτημάτων",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Σύνολο μέσων",
|
||||
"components.Settings.SettingsAbout.timezone": "Ζώνη ώρας",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Υποστήριξε το Overseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Υποστήριξε το Jellyseerr",
|
||||
"components.Settings.SettingsAbout.preferredmethod": "Προτιμώνενο",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Πληροφορίες Overseerr",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Πληροφορίες Jellyseerr",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Κέρασε κάνα Καφεδάκι",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Συζητήσεις στο GitHub",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Λήψη Υποστήριξης",
|
||||
@@ -664,9 +664,9 @@
|
||||
"components.TvDetails.TvCast.fullseriescast": "Όλοι οι Ηθοποιοί της Σειράς",
|
||||
"components.StatusChacker.reloadOverseerr": "Επαναφόρτωση",
|
||||
"components.StatusChacker.newversionavailable": "Ενημέρωση εφαρμογής",
|
||||
"components.StatusChacker.newversionDescription": "Το Overseerr έχει ενημερωθεί! Κάνε κλικ στο παρακάτω κουμπί για να φορτώσει ξανά η σελίδα.",
|
||||
"components.StatusChacker.newversionDescription": "Το Jellyseerr έχει ενημερωθεί! Κάνε κλικ στο παρακάτω κουμπί για να φορτώσει ξανά η σελίδα.",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"components.Setup.welcome": "Καλώς ήρθες στο Overseerr",
|
||||
"components.Setup.welcome": "Καλώς ήρθες στο Jellyseerr",
|
||||
"components.Setup.tip": "Συμβουλή",
|
||||
"components.Setup.signinMessage": "Ξεκίνα με την σύνδεση στον λογαριασμό του Plex σου",
|
||||
"components.Setup.setup": "Εγκατάσταση",
|
||||
@@ -685,7 +685,7 @@
|
||||
"components.Settings.validationApplicationUrlTrailingSlash": "Η διεύθυνση URL δεν πρέπει να τελειώνει με κάθετο",
|
||||
"components.Settings.validationApplicationUrl": "Πρέπει να βάλεις μια έγκυρη διεύθυνση URL",
|
||||
"components.Settings.validationApplicationTitle": "Πρέπει να δώσεις έναν τίτλο εφαρμογής",
|
||||
"components.Settings.trustProxyTip": "Επίτρεψε στο Overseerr να καταχωρίζει σωστά τις διευθύνσεις IP του πελάτη πίσω από έναν διακομιστή μεσολάβησης (το Overseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
|
||||
"components.Settings.trustProxyTip": "Επίτρεψε στο Jellyseerr να καταχωρίζει σωστά τις διευθύνσεις IP του πελάτη πίσω από έναν διακομιστή μεσολάβησης (το Jellyseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
|
||||
"components.Settings.trustProxy": "Ενεργοποίηση υποστήριξης διαμεσολαβητή",
|
||||
"components.Settings.toastSettingsSuccess": "Οι ρυθμίσεις αποθηκεύτηκαν με επιτυχία!",
|
||||
"components.Settings.toastSettingsFailure": "Κάτι πήγε στραβά κατά την αποθήκευση των ρυθμίσεων.",
|
||||
@@ -716,9 +716,9 @@
|
||||
"components.Settings.region": "Ανακάλυψε βάσει την περιοχή",
|
||||
"components.Settings.radarrsettings": "Ρυθμίσεις Radarr",
|
||||
"components.Settings.port": "Θύρα",
|
||||
"components.Settings.plexsettingsDescription": "Διαμόρφωσε τις ρυθμίσεις του Plex διακομιστή σου. Το Overseerr σαρώνει τις βιβλιοθήκες του Plex για να προσδιορίσει τη διαθεσιμότητα περιεχομένου.",
|
||||
"components.Settings.plexsettingsDescription": "Διαμόρφωσε τις ρυθμίσεις του Plex διακομιστή σου. Το Jellyseerr σαρώνει τις βιβλιοθήκες του Plex για να προσδιορίσει τη διαθεσιμότητα περιεχομένου.",
|
||||
"components.Settings.plexsettings": "Ρυθμίσεις Plex",
|
||||
"components.Settings.plexlibrariesDescription": "Οι βιβλιοθήκες που το Overseerr θα σαρώνει για τίτλους. Ρύθμισε και αποθήκευσε τις ρυθμίσεις της σύνδεσης του Plex και, στη συνέχεια, κάνε κλικ στο παρακάτω κουμπί, εάν δεν εμφανιστύν οι βιβλιοθήκες.",
|
||||
"components.Settings.plexlibrariesDescription": "Οι βιβλιοθήκες που το Jellyseerr θα σαρώνει για τίτλους. Ρύθμισε και αποθήκευσε τις ρυθμίσεις της σύνδεσης του Plex και, στη συνέχεια, κάνε κλικ στο παρακάτω κουμπί, εάν δεν εμφανιστύν οι βιβλιοθήκες.",
|
||||
"components.Settings.plexlibraries": "Βιβλιοθήκες Plex",
|
||||
"components.Settings.plex": "Plex",
|
||||
"components.Settings.partialRequestsEnabled": "Να επιτρέπονται τα εν μέρει αιτήματα για τις Σειρές",
|
||||
@@ -741,7 +741,7 @@
|
||||
"components.Settings.menuAbout": "Σχετικά",
|
||||
"components.Settings.mediaTypeSeries": "σειρά",
|
||||
"components.Settings.mediaTypeMovie": "ταινία",
|
||||
"components.Settings.manualscanDescription": "Κανονικά, αυτό εκτελείται μόνο μία φορά κάθε 24 ώρες. Το Overseerr θα ελέγχει πιο επιθετικά τις προσθήκες που έγιναν πρόσφατα στον διακομιστή Plex. Εάν αυτή είναι η πρώτη φορά που ρυθμίζεις το Plex, συνιστάται μια εφάπαξ πλήρης χειροκίνητη σάρωση βιβλιοθήκης!",
|
||||
"components.Settings.manualscanDescription": "Κανονικά, αυτό εκτελείται μόνο μία φορά κάθε 24 ώρες. Το Jellyseerr θα ελέγχει πιο επιθετικά τις προσθήκες που έγιναν πρόσφατα στον διακομιστή Plex. Εάν αυτή είναι η πρώτη φορά που ρυθμίζεις το Plex, συνιστάται μια εφάπαξ πλήρης χειροκίνητη σάρωση βιβλιοθήκης!",
|
||||
"components.Settings.manualscan": "Χειροκίνητη σάρωση βιβλιοθήκης",
|
||||
"components.Settings.locale": "Προβολή Γλώσσας",
|
||||
"components.Settings.librariesRemaining": "Βιβλιοθήκες που απομένουν: {count}",
|
||||
@@ -756,7 +756,7 @@
|
||||
"components.Settings.default4k": "Προεπιλεγμένο 4K",
|
||||
"components.Settings.default": "Προκαθορισμένο",
|
||||
"components.Settings.currentlibrary": "Τρέχουσα βιβλιοθήκη: {name}",
|
||||
"components.Settings.csrfProtectionTip": "Ορισμός εξωτερικής πρόσβασης API σε μόνο για ανάγνωση (απαιτείται HTTPS και το Overseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
|
||||
"components.Settings.csrfProtectionTip": "Ορισμός εξωτερικής πρόσβασης API σε μόνο για ανάγνωση (απαιτείται HTTPS και το Jellyseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
|
||||
"components.Settings.csrfProtectionHoverTip": "ΜΗΝ ενεργοποιήσεις αυτή τη ρύθμιση αν δεν καταλαβαίνεις τι κάνεις!",
|
||||
"components.Settings.csrfProtection": "Ενεργοποίηση της προστασίας CSRF",
|
||||
"components.Settings.copied": "Αντιγράφηκε το κλειδί API στο πρόχειρο.",
|
||||
@@ -824,7 +824,7 @@
|
||||
"components.Settings.SonarrModal.add": "Προσθήκη διακομιστή",
|
||||
"components.Settings.SettingsUsers.users": "Χρήστες",
|
||||
"components.Settings.SettingsUsers.userSettings": "Ρυθμίσεις χρήστη",
|
||||
"components.Settings.generalsettingsDescription": "Διαμόρφωση των γενικών και προεπιλεγμένων ρυθμίσεων του Overseerr.",
|
||||
"components.Settings.generalsettingsDescription": "Διαμόρφωση των γενικών και προεπιλεγμένων ρυθμίσεων του Jellyseerr.",
|
||||
"components.Settings.SettingsUsers.userSettingsDescription": "Διαμόρφωση των γενικών και προεπιλεγμένων ρυθμίσεων του χρήστη.",
|
||||
"components.Settings.SettingsUsers.tvRequestLimitLabel": "Καθολικό όριο των αιτημάτων στις Σειρές",
|
||||
"components.Settings.SettingsUsers.toastSettingsSuccess": "Οι ρυθμίσεις του χρήστη αποθηκεύτηκαν επιτυχώς!",
|
||||
@@ -832,7 +832,7 @@
|
||||
"components.Settings.SettingsUsers.newPlexLoginTip": "Επίτρεψε στους χρήστες του Plex να συνδεθούν χωρίς να εισαχθούν πρώτα",
|
||||
"components.Settings.SettingsUsers.newPlexLogin": "Ενεργοποίηση σύνδεσης νέου χρήστη του Plex",
|
||||
"components.Settings.validationWebAppUrl": "Πρέπει να δώσεις μια έγκυρη διεύθυνση URL της εφαρμογής Plex Web App",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Το Overseerr εκτελεί ορισμένες εργασίες συντήρησης ως τακτικά προγραμματισμένες εργασίες, αλλά μπορούν επίσης να ενεργοποιηθούν χειροκίνητα παρακάτω. Η χειροκίνητη εκτέλεση μιας εργασίας δεν θα αλλάξει το χρονοδιάγραμμα του.",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Το Jellyseerr εκτελεί ορισμένες εργασίες συντήρησης ως τακτικά προγραμματισμένες εργασίες, αλλά μπορούν επίσης να ενεργοποιηθούν χειροκίνητα παρακάτω. Η χειροκίνητη εκτέλεση μιας εργασίας δεν θα αλλάξει το χρονοδιάγραμμα του.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Template Variable Help",
|
||||
"components.Settings.Notifications.NotificationsLunaSea.webhookUrlTip": "Ο χρήστης σου ή η συσκευή <LunaSeaLink>ειδοποίηση webhook URL</LunaSeaLink>",
|
||||
"components.RequestModal.numberofepisodes": "# Αριθμός Επεισοδίων",
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
|
||||
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr Develop",
|
||||
"components.Layout.VersionStatus.streamstable": "Overseerr Stable",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
||||
"components.Login.email": "Email Address",
|
||||
"components.Login.forgotpassword": "Forgot Password?",
|
||||
"components.Login.host": "Jellyfin URL",
|
||||
@@ -144,7 +144,7 @@
|
||||
"components.PermissionEdit.autoapproveSeries": "Auto-Approve Series",
|
||||
"components.PermissionEdit.autoapproveSeriesDescription": "Grant automatic approval for non-4K series requests.",
|
||||
"components.PermissionEdit.managerequests": "Manage Requests",
|
||||
"components.PermissionEdit.managerequestsDescription": "Grant permission to manage Overseerr requests. All requests made by a user with this permission will be automatically approved.",
|
||||
"components.PermissionEdit.managerequestsDescription": "Grant permission to manage Jellyseerr requests. All requests made by a user with this permission will be automatically approved.",
|
||||
"components.PermissionEdit.request": "Request",
|
||||
"components.PermissionEdit.request4k": "Request 4K",
|
||||
"components.PermissionEdit.request4kDescription": "Grant permission to request 4K media.",
|
||||
@@ -158,9 +158,9 @@
|
||||
"components.PermissionEdit.requestTv": "Request Series",
|
||||
"components.PermissionEdit.requestTvDescription": "Grant permission to request non-4K series.",
|
||||
"components.PermissionEdit.settings": "Manage Settings",
|
||||
"components.PermissionEdit.settingsDescription": "Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.",
|
||||
"components.PermissionEdit.settingsDescription": "Grant permission to modify Jellyseerr settings. A user must have this permission to grant it to others.",
|
||||
"components.PermissionEdit.users": "Manage Users",
|
||||
"components.PermissionEdit.usersDescription": "Grant permission to manage Overseerr users. Users with this permission cannot modify users with or grant the Admin privilege.",
|
||||
"components.PermissionEdit.usersDescription": "Grant permission to manage Jellyseerr users. Users with this permission cannot modify users with or grant the Admin privilege.",
|
||||
"components.PermissionEdit.viewrequests": "View Requests",
|
||||
"components.PermissionEdit.viewrequestsDescription": "Grant permission to view other users' requests.",
|
||||
"components.PersonDetails.alsoknownas": "Also Known As: {names}",
|
||||
@@ -304,7 +304,7 @@
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationTypes": "You must select at least one notification type",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessToken": "Application API Token",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr",
|
||||
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
|
||||
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.",
|
||||
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notification settings saved successfully!",
|
||||
@@ -327,7 +327,7 @@
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
|
||||
"components.Settings.Notifications.NotificationsWebPush.agentenabled": "Enable Agent",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "In order to receive web push notifications, Overseerr must be served over HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "In order to receive web push notifications, Jellyseerr must be served over HTTPS.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestFailed": "Web push test notification failed to send.",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSending": "Sending web push test notification…",
|
||||
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "Web push test notification sent!",
|
||||
@@ -353,7 +353,7 @@
|
||||
"components.Settings.Notifications.authPass": "SMTP Password",
|
||||
"components.Settings.Notifications.authUser": "SMTP Username",
|
||||
"components.Settings.Notifications.botAPI": "Bot Authorization Token",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr",
|
||||
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Create a bot</CreateBotLink> for use with Jellyseerr",
|
||||
"components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL",
|
||||
"components.Settings.Notifications.botUsername": "Bot Username",
|
||||
"components.Settings.Notifications.botUsernameTip": "Allow users to also start a chat with your bot and configure their own notifications",
|
||||
@@ -450,7 +450,7 @@
|
||||
"components.Settings.SettingsAbout.Releases.latestversion": "Latest",
|
||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Release data unavailable. Is GitHub down?",
|
||||
"components.Settings.SettingsAbout.Releases.releases": "Releases",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "The latest changes to the <code>develop</code> branch of Overseerr are not shown below. Please see the commit history for this branch on <GithubLink>GitHub</GithubLink> for details.",
|
||||
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "The latest changes to the <code>develop</code> branch of Jellyseerr are not shown below. Please see the commit history for this branch on <GithubLink>GitHub</GithubLink> for details.",
|
||||
"components.Settings.SettingsAbout.Releases.versionChangelog": "Version Changelog",
|
||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "View Changelog",
|
||||
"components.Settings.SettingsAbout.Releases.viewongithub": "View on GitHub",
|
||||
@@ -461,16 +461,16 @@
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Help Pay for Coffee",
|
||||
"components.Settings.SettingsAbout.outofdate": "Out of Date",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr Information",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Jellyseerr Information",
|
||||
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Jellyseerr",
|
||||
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||
"components.Settings.SettingsAbout.uptodate": "Up to Date",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsJobsCache.cache": "Cache",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Jellyseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} cache flushed.",
|
||||
"components.Settings.SettingsJobsCache.cachehits": "Hits",
|
||||
"components.Settings.SettingsJobsCache.cachekeys": "Total Keys",
|
||||
@@ -486,7 +486,7 @@
|
||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
||||
"components.Settings.SettingsJobsCache.jobname": "Job Name",
|
||||
"components.Settings.SettingsJobsCache.jobs": "Jobs",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Jellyseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.",
|
||||
"components.Settings.SettingsJobsCache.jobsandcache": "Jobs & Cache",
|
||||
"components.Settings.SettingsJobsCache.jobstarted": "{jobname} started.",
|
||||
"components.Settings.SettingsJobsCache.jobtype": "Type",
|
||||
@@ -509,7 +509,7 @@
|
||||
"components.Settings.SettingsLogs.level": "Severity",
|
||||
"components.Settings.SettingsLogs.logDetails": "Log Details",
|
||||
"components.Settings.SettingsLogs.logs": "Logs",
|
||||
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/jellyseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.message": "Message",
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Pause",
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Resume",
|
||||
@@ -593,7 +593,7 @@
|
||||
"components.Settings.copied": "Copied API key to clipboard.",
|
||||
"components.Settings.csrfProtection": "Enable CSRF Protection",
|
||||
"components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
||||
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)",
|
||||
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Jellyseerr must be reloaded for changes to take effect)",
|
||||
"components.Settings.currentlibrary": "Current Library: {name}",
|
||||
"components.Settings.default": "Default",
|
||||
"components.Settings.default4k": "Default 4K",
|
||||
@@ -602,19 +602,19 @@
|
||||
"components.Settings.enablessl": "Use SSL",
|
||||
"components.Settings.general": "General",
|
||||
"components.Settings.generalsettings": "General Settings",
|
||||
"components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.",
|
||||
"components.Settings.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
||||
"components.Settings.hideAvailable": "Hide Available Media",
|
||||
"components.Settings.jellyfinlibraries": "Jellyfin Libraries",
|
||||
"components.Settings.jellyfinlibrariesDescription": "The libraries Overseerr scans for titles. Click the button below if no libraries are listed.",
|
||||
"components.Settings.jellyfinlibrariesDescription": "The libraries Jellyseerr scans for titles. Click the button below if no libraries are listed.",
|
||||
"components.Settings.jellyfinsettings": "Jellyfin Settings",
|
||||
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your Jellyfin server. Overseerr scans your Jellyfin libraries to see what content is available.",
|
||||
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your Jellyfin server. Jellyseerr scans your Jellyfin libraries to see what content is available.",
|
||||
"components.Settings.hostname": "Hostname or IP Address",
|
||||
"components.Settings.is4k": "4K",
|
||||
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
||||
"components.Settings.locale": "Display Language",
|
||||
"components.Settings.manualscan": "Manual Library Scan",
|
||||
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Normally, this will only be run once every 24 hours. Overseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
||||
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||
"components.Settings.manualscanDescriptionJellyfin": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
||||
"components.Settings.manualscanJellyfin": "Manual Library Scan",
|
||||
"components.Settings.menuGeneralSettings": "General Settings",
|
||||
"components.Settings.menuJellyfinSettings": "Jellyfin",
|
||||
@@ -639,9 +639,9 @@
|
||||
"components.Settings.partialRequestsEnabled": "Allow Partial Series Requests",
|
||||
"components.Settings.plex": "Plex",
|
||||
"components.Settings.plexlibraries": "Plex Libraries",
|
||||
"components.Settings.plexlibrariesDescription": "The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.",
|
||||
"components.Settings.plexlibrariesDescription": "The libraries Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.",
|
||||
"components.Settings.plexsettings": "Plex Settings",
|
||||
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.",
|
||||
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Jellyseerr scans your Plex libraries to determine content availability.",
|
||||
"components.Settings.port": "Port",
|
||||
"components.Settings.radarrsettings": "Radarr Settings",
|
||||
"components.Settings.region": "Discover Region",
|
||||
@@ -676,7 +676,7 @@
|
||||
"components.Settings.toastSettingsFailure": "Something went wrong while saving settings.",
|
||||
"components.Settings.toastSettingsSuccess": "Settings saved successfully!",
|
||||
"components.Settings.trustProxy": "Enable Proxy Support",
|
||||
"components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)",
|
||||
"components.Settings.trustProxyTip": "Allow Jellyseerr to correctly register client IP addresses behind a proxy (Jellyseerr must be reloaded for changes to take effect)",
|
||||
"components.Settings.validationApplicationTitle": "You must provide an application title",
|
||||
"components.Settings.validationApplicationUrl": "You must provide a valid URL",
|
||||
"components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
@@ -702,9 +702,9 @@
|
||||
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
|
||||
"components.Setup.setup": "Setup",
|
||||
"components.Setup.tip": "Tip",
|
||||
"components.Setup.welcome": "Welcome to Overseerr",
|
||||
"components.Setup.welcome": "Welcome to Jellyseerr",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"components.StatusChacker.newversionDescription": "Overseerr has been updated! Please click the button below to reload the page.",
|
||||
"components.StatusChacker.newversionDescription": "Jellyseerr has been updated! Please click the button below to reload the page.",
|
||||
"components.StatusChacker.newversionavailable": "Application Update",
|
||||
"components.StatusChacker.reloadOverseerr": "Reload",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"components.Settings.SettingsAbout.version": "Versión",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Peticiones Totales",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Contenido Total",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Información de Overseerr",
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "Información de Jellyseerr",
|
||||
"components.Settings.SettingsAbout.githubdiscussions": "Discursiones en GitHub",
|
||||
"components.Settings.SettingsAbout.gettingsupport": "Soporte",
|
||||
"components.Settings.RadarrModal.validationRootFolderRequired": "Debes seleccionar una carpeta raíz",
|
||||
@@ -156,7 +156,7 @@
|
||||
"components.TvDetails.manageModalClearMedia": "Borrar los datos de medios",
|
||||
"components.TvDetails.cast": "Reparto",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Reparto completo de la serie",
|
||||
"components.Setup.welcome": "Bienvenido a Overseerr",
|
||||
"components.Setup.welcome": "Bienvenido a Jellyseerr",
|
||||
"components.Setup.signinMessage": "Comience iniciando sesión con su cuenta de Plex",
|
||||
"components.Setup.loginwithplex": "Iniciar sesión con Plex",
|
||||
"components.Setup.finishing": "Finalizando…",
|
||||
@@ -171,9 +171,9 @@
|
||||
"components.Settings.sonarrsettings": "Ajustes de Sonarr",
|
||||
"components.Settings.radarrsettings": "Ajustes de Radarr",
|
||||
"components.Settings.port": "Puerto",
|
||||
"components.Settings.plexsettingsDescription": "Configure los ajustes de su servidor Plex. Overseerr escanea tu biblioteca para determinar la disponibilidad de contenidos.",
|
||||
"components.Settings.plexsettingsDescription": "Configure los ajustes de su servidor Plex. Jellyseerr escanea tu biblioteca para determinar la disponibilidad de contenidos.",
|
||||
"components.Settings.plexsettings": "Ajustes de Plex",
|
||||
"components.Settings.plexlibrariesDescription": "Las bibliotecas en las que Overseerr escanea para buscar títulos. Configure y guarde la configuración de conexión Plex, y después haga clic en el botón de abajo si no aparece ninguna.",
|
||||
"components.Settings.plexlibrariesDescription": "Las bibliotecas en las que Jellyseerr escanea para buscar títulos. Configure y guarde la configuración de conexión Plex, y después haga clic en el botón de abajo si no aparece ninguna.",
|
||||
"components.Settings.plexlibraries": "Bibliotecas Plex",
|
||||
"components.Settings.notrunning": "Sin ejecutarse",
|
||||
"components.Settings.notificationsettings": "Configuración de notificaciones",
|
||||
@@ -183,11 +183,11 @@
|
||||
"components.Settings.menuJobs": "Tareas y Caché",
|
||||
"components.Settings.menuGeneralSettings": "General",
|
||||
"components.Settings.menuAbout": "Acerca de",
|
||||
"components.Settings.manualscanDescription": "Normalmente, esto sólo se ejecutará una vez cada 24 horas. Overseerr comprobará de forma más agresiva los añadidos recientemente de su servidor Plex. ¡Si es la primera vez que configura Plex, se recomienda un escaneo manual completo de la biblioteca!",
|
||||
"components.Settings.manualscanDescription": "Normalmente, esto sólo se ejecutará una vez cada 24 horas. Jellyseerr comprobará de forma más agresiva los añadidos recientemente de su servidor Plex. ¡Si es la primera vez que configura Plex, se recomienda un escaneo manual completo de la biblioteca!",
|
||||
"components.Settings.manualscan": "Escaneo Manual de Biblioteca",
|
||||
"components.Settings.librariesRemaining": "Bibliotecas restantes: {count}",
|
||||
"components.Settings.hostname": "Nombre de host o Dirección IP",
|
||||
"components.Settings.generalsettingsDescription": "Configuración global y ajustes por defecto de Overseerr.",
|
||||
"components.Settings.generalsettingsDescription": "Configuración global y ajustes por defecto de Jellyseerr.",
|
||||
"components.Settings.generalsettings": "Configuración general",
|
||||
"components.Settings.deleteserverconfirm": "¿Está seguro de que desea eliminar este servidor?",
|
||||
"components.Settings.default4k": "4K predeterminado",
|
||||
@@ -223,7 +223,7 @@
|
||||
"components.Settings.SonarrModal.animerootfolder": "Carpeta raíz de anime",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Perfil de calidad de anime",
|
||||
"components.Settings.SettingsAbout.timezone": "Zona horaria",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Apoya a Overseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Apoya a Jellyseerr",
|
||||
"components.Settings.SettingsAbout.helppaycoffee": "Ayúdame invitándome a un café",
|
||||
"components.Settings.SettingsAbout.Releases.viewongithub": "Ver en GitHub",
|
||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "Ver registro de cambios",
|
||||
@@ -262,7 +262,7 @@
|
||||
"components.NotificationTypeSelector.mediarequestedDescription": "Envía una notificación cuando se solicitan medios que requieren ser aprobados.",
|
||||
"components.StatusChacker.reloadOverseerr": "Recargar",
|
||||
"components.StatusChacker.newversionavailable": "Actualización de Aplicación",
|
||||
"components.StatusChacker.newversionDescription": "¡Overseerr se ha actualizado!Haga clic en el botón de abajo para volver a cargar la aplicación.",
|
||||
"components.StatusChacker.newversionDescription": "¡Jellyseerr se ha actualizado!Haga clic en el botón de abajo para volver a cargar la aplicación.",
|
||||
"components.Settings.SettingsAbout.documentation": "Documentación",
|
||||
"components.Settings.Notifications.validationChatIdRequired": "Debes proporcionar un ID de chat válido",
|
||||
"components.Settings.Notifications.validationBotAPIRequired": "Debes proporcionar un token de autenticación del bot",
|
||||
@@ -595,7 +595,7 @@
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Pausar",
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Reanudar",
|
||||
"components.Settings.SettingsLogs.message": "Mensaje",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Puede ver estos logs directamente via <code>stdout</code> o en <code>{configDir}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logsDescription": "Puede ver estos logs directamente via <code>stdout</code> o en <code>{configDir}/logs/jellyseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.logs": "Logs",
|
||||
"components.Settings.SettingsLogs.level": "Severidad",
|
||||
"components.Settings.SettingsLogs.label": "Etiqueta",
|
||||
|
||||