Compare commits

..

12 Commits

Author SHA1 Message Date
Fallenbagel
9428664f2e Delete dependabot.yml 2022-04-13 09:18:45 +05:00
jumail
7e7efc06ba refactor: road to prisma
Converting this to use prisma with postgres instead of using typeorm and sqlite

BREAKING CHANGE: Incomplete index page in server folder
2022-03-27 02:55:17 +05:00
Fallenbagel
8a8953d52e Fixed Readme.md 2022-03-26 03:10:11 +05:00
Fallenbagel
6385c9bcb2 Added buymecoffee link 2022-03-26 03:00:59 +05:00
Fallenbagel
d202f9f618 Update Readme.ME
added installation instructions and supported architectures
2022-03-19 09:06:33 +05:00
Fallenbagel
77d3747267 logo SVG change commit 2022-03-17 00:46:43 +05:00
Fallenbagel
31392856dc First commit 2022-03-17 00:36:35 +05:00
Fallenbagel
5e000abd56 logo & preview changes 2022-03-17 00:23:57 +05:00
Fallenbagel
250cdb969c Merge pull request #23 from doookkie/patch-1
Update README.md
2022-03-17 00:00:52 +05:00
doookkie
71bc90ef89 Update README.md
Change the README to more accurately reflect Jellyseerr.
2022-03-16 12:34:37 -04:00
jumail
7beea396a4 style(linting errors for es): there were some errors when I tried to run the code so I fixed them 2022-03-09 19:29:49 +05:00
Fallenbagel
cdfa938471 first commit 2022-03-09 14:27:04 +05:00
104 changed files with 2150 additions and 2325 deletions

View File

@@ -540,22 +540,11 @@
"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": "jellyseerr",
"projectOwner": "Fallenbagel",
"projectName": "overseerr",
"projectOwner": "sct",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true

7
.env Normal file
View File

@@ -0,0 +1,7 @@
# 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"

View File

@@ -7,6 +7,7 @@ module.exports = {
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
parserOptions: {
ecmaVersion: 6,
@@ -25,6 +26,7 @@ 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'],
@@ -38,7 +40,7 @@ module.exports = {
},
},
],
plugins: ['jsx-a11y', 'react-hooks', 'formatjs'],
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
settings: {
react: {
pragma: 'React',

11
.github/CODEOWNERS vendored
View File

@@ -1,7 +1,12 @@
# Global code ownership
* @sct
- @Fallenbagel
# Documentation
docs/ @TheCatLady @samwiseg0
# Snap-related files
.github/workflows/snap.yaml @samwiseg0
snap/ @samwiseg0
# i18n locale files
src/i18n/locale/ @Fallenbagel
src/i18n/locale/ @sct @TheCatLady

View File

@@ -4,4 +4,10 @@
#### To-Dos
- [ ] Successful build `yarn build`
- [ ] Translation keys `yarn i18n:extract`
- [ ] Database migration (if required)
#### Issues Fixed or Closed
- Fixes #XXXX

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm test

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec < /dev/tty && git cz --hook || true

View File

@@ -1,8 +1,8 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/stable/public/logo.png" alt="Overseerr" style="margin: 20px 0;">
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
</p>
<p align="center">
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
<a href="https://discord.gg/BHak4GCk"><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 | armv7 |
|------------------|---------|
| x86-64 | latest |
| ARM64 | arm |
| ARMv7 | arm |
## Getting Started
@@ -37,14 +37,13 @@ https://hub.docker.com/r/fallenbagel/jellyseerr
## Support
- You can get support on [Discord](https://discord.gg/ckbvBtDJgC).
- You can get support on [Discord](https://discord.gg/VpVnZ92yQK).
- 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!
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/fallen.bagel)

View File

@@ -19,6 +19,7 @@
"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",
@@ -46,6 +47,7 @@
"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",

View File

@@ -0,0 +1,163 @@
-- 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;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

127
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,127 @@
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)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,45 +1 @@
<?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>
<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>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 504 KiB

View File

@@ -1,3 +0,0 @@
[ZoneTransfer]
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
ZoneId=3

View File

@@ -1,6 +1,6 @@
{
"name": "Jellyseerr",
"short_name": "Jellyseerr",
"name": "Overseerr",
"short_name": "Overseerr",
"start_url": "./",
"icons": [
{

View File

@@ -83,7 +83,7 @@ class GithubAPI extends ExternalAPI {
} = {}): Promise<GitHubRelease[]> {
try {
const data = await this.get<GitHubRelease[]>(
'/repos/Fallenbagel/jellyseerr/releases',
'/repos/sct/overseerr/releases',
{
params: {
per_page: take,
@@ -110,7 +110,7 @@ class GithubAPI extends ExternalAPI {
} = {}): Promise<GithubCommit[]> {
try {
const data = await this.get<GithubCommit[]>(
'/repos/Fallenbagel/jellyseerr/commits',
'/repos/sct/overseerr/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. Jellyseerr can't check if it's on the latest version.",
"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.",
{ label: 'GitHub API', errorMessage: e.message }
);
return [];

View File

@@ -81,9 +81,9 @@ class JellyfinAPI {
let authHeaderVal = '';
if (this.authToken) {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
} else {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
}
this.axios = axios.create({

View File

@@ -122,9 +122,9 @@ class PlexAPI {
// },
options: {
identifier: settings.clientId,
product: 'Jellyseerr',
deviceName: 'Jellyseerr',
platform: 'Jellyseerr',
product: 'Overseerr',
deviceName: 'Overseerr',
platform: 'Overseerr',
},
});
}

View File

@@ -1,18 +1,14 @@
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';
@@ -31,22 +27,21 @@ import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
logger.info(`Starting Overseerr 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 () => {
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');
}
// // 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();
@@ -56,14 +51,23 @@ app
settings.plex.libraries.length > 1 &&
!settings.plex.libraries[0].type
) {
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
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' },
// });
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',
@@ -129,22 +133,24 @@ app
}
// Set up sessions
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 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 apiDocs = YAML.load(API_SPEC_PATH);
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
server.use(

View File

@@ -43,8 +43,7 @@ interface SlackBlockEmbed {
class SlackAgent
extends BaseAgent<NotificationAgentSlack>
implements NotificationAgent
{
implements NotificationAgent {
protected getSettings(): NotificationAgentSlack {
if (this.settings) {
return this.settings;
@@ -203,7 +202,7 @@ class SlackAgent
action_id: 'button-action',
type: 'button',
url: actionUrl,
value: 'open_jellyseerr',
value: 'open_overseerr',
text: {
type: 'plain_text',
text: `Open in ${settings.main.applicationTitle}`,

View File

@@ -254,7 +254,7 @@ class Settings {
vapidPublic: '',
main: {
apiKey: '',
applicationTitle: 'Jellyseerr',
applicationTitle: 'Overseerr',
applicationUrl: '',
csrfProtection: false,
cacheImages: false,
@@ -303,7 +303,7 @@ class Settings {
ignoreTls: false,
requireTls: false,
allowSelfSigned: false,
senderName: 'Jellyseerr',
senderName: 'Overseerr',
},
},
discord: {

View File

@@ -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/Jellyseerr.log');
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/overseerr.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/Jellyseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/Jellyseerr-%DATE%.log'),
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '7d',
createSymlink: true,
symlinkName: 'Jellyseerr.log',
symlinkName: 'overseerr.log',
}),
],
});

View File

@@ -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, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, 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, 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", "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"`
`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"`
);
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, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, 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, 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", "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"`
`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"`
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
}

View File

@@ -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 Jellyseerr.',
'Failed sign-in attempt from user who has not been imported to Overseerr.',
{
label: 'Auth',
account: {
@@ -214,7 +214,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
if (user) {
deviceId = user.jellyfinDeviceId ?? '';
} else {
deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString(
deviceId = Buffer.from(`BOT_overseerr_${body.username ?? ''}`).toString(
'base64'
);
}

View File

@@ -162,7 +162,7 @@ router.get('/genres/tv', isAuthenticated(), async (req, res) => {
router.get('/', (_req, res) => {
return res.status(200).json({
api: 'Jellyseerr API',
api: 'Overseerr API',
version: '1.0',
});
});

View File

@@ -325,8 +325,8 @@ settingsRoutes.get(
}
const logFile = process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/logs/jellyseerr.log`
: path.join(__dirname, '../../../config/logs/jellyseerr.log');
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr.log`
: path.join(__dirname, '../../../config/logs/overseerr.log');
const logs: LogMessage[] = [];
try {

View File

@@ -1,15 +1,15 @@
name: jellyseerr
adopt-info: jellyseerr
name: overseerr
adopt-info: overseerr
license: MIT
summary: Request management and media discovery tool for the Plex ecosystem.
description: >
Jellyseerr is a free and open source software application for managing requests for your media library.
Overseerr 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:
jellyseerr:
overseerr:
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'
JELLYSEERR_SNAP: 'True'
OVERSEERR_SNAP: 'True'
CONFIG_DIRECTORY: $SNAP_USER_COMMON
LOG_LEVEL: 'debug'
NODE_ENV: 'production'

View File

@@ -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: 'Jellyseerr Develop',
streamstable: 'Jellyseerr Stable',
streamdevelop: 'Overseerr Develop',
streamstable: 'Overseerr 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.streamstable)
? intl.formatMessage(messages.streamdevelop)
: intl.formatMessage(messages.streamstable);
return (
@@ -52,16 +52,14 @@ 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-gray-900 text-gray-300 hover:bg-gray-800'
? 'bg-yellow-500 text-white hover:bg-yellow-400'
: '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" />
<CodeIcon className="w-6 h-6" />
<BeakerIcon className="w-6 h-6" />
) : (
<ServerIcon className="w-6 h-6" />
)}
@@ -74,8 +72,7 @@ const VersionStatus: React.FC<VersionStatusProps> = ({ onClick }) => {
intl.formatMessage(messages.commitsbehind, {
commitsBehind: data.commitsBehind,
})
) : // ) : data.commitsBehind === -1 ? (
data.commitsBehind === 0 ? (
) : data.commitsBehind === -1 ? (
intl.formatMessage(messages.outofdate)
) : (
<code className="p-0 bg-transparent">

View File

@@ -164,13 +164,10 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
href="/site.webmanifest"
crossOrigin="use-credentials"
/>
<meta
name="application-name"
content={applicationTitle ?? 'Jellyseerr'}
/>
<meta name="application-name" content={applicationTitle ?? 'Overseerr'} />
<meta
name="apple-mobile-web-app-title"
content={applicationTitle ?? 'Jellyseerr'}
content={applicationTitle ?? 'Overseerr'}
/>
<meta
name="description"

View File

@@ -9,13 +9,13 @@ export const messages = defineMessages({
'Full administrator access. Bypasses all other permission checks.',
users: 'Manage Users',
usersDescription:
'Grant permission to manage Jellyseerr users. Users with this permission cannot modify users with or grant the Admin privilege.',
'Grant permission to manage Overseerr users. Users with this permission cannot modify users with or grant the Admin privilege.',
settings: 'Manage Settings',
settingsDescription:
'Grant permission to modify Jellyseerr settings. A user must have this permission to grant it to others.',
'Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.',
managerequests: 'Manage Requests',
managerequestsDescription:
'Grant permission to manage Jellyseerr requests. All requests made by a user with this permission will be automatically approved.',
'Grant permission to manage Overseerr 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',

View File

@@ -1,38 +1,24 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect } 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 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 axios from 'axios';
import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage';
import RequestModal from '../RequestModal';
import { withProperties } from '../../utils/typeHelpers';
import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl';
import globalMessages from '../../i18n/globalMessages';
import StatusBadge from '../StatusBadge';
const messages = defineMessages({
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',
seasons: 'Seasons',
all: 'All',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -41,7 +27,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
const RequestCardPlaceholder: React.FC = () => {
return (
<div className="relative w-72 animate-pulse rounded-xl bg-gray-700 p-4 sm:w-96">
<div className="relative p-4 bg-gray-700 rounded-lg w-72 sm:w-96 animate-pulse">
<div className="w-20 sm:w-28">
<div className="w-full" style={{ paddingBottom: '150%' }} />
</div>
@@ -49,45 +35,6 @@ 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;
@@ -98,21 +45,19 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
triggerOnce: true,
});
const intl = useIntl();
const { user, hasPermission } = useUser();
const { addToast } = useToasts();
const [isRetrying, setRetrying] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const { hasPermission } = useUser();
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}` : null
inView ? `${url}?language=${locale}` : null
);
const {
data: requestData,
error: requestError,
mutate: revalidate,
revalidate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
initialData: request,
});
@@ -125,30 +70,6 @@ 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);
@@ -164,251 +85,157 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
}
if (!requestData && !requestError) {
return <RequestCardError />;
return <RequestCardPlaceholder />;
}
if (!title || !requestData) {
return <RequestCardError mediaId={requestData?.media.id} />;
return <RequestCardPlaceholder />;
}
return (
<>
<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
<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}
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
layout="fill"
objectFit="cover"
className="w-4 mr-1 rounded-full sm:mr-2 sm:w-5"
/>
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
}}
<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>
)}
<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>
<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,
})}
{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>
{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 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>
<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) && (
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="flex items-end flex-1">
<span className="mr-2">
<Button
buttonType="primary"
buttonType="success"
buttonSize="sm"
disabled={isRetrying}
onClick={() => retryRequest()}
onClick={() => modifyRequest('approve')}
>
<RefreshIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ marginRight: '0', animationDirection: 'reverse' }}
/>
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.retry)}
<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>
)}
{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 && (
</span>
<span>
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => deleteRequest()}
onClick={() => modifyRequest('decline')}
>
<XIcon style={{ marginRight: '0' }} />
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.cancel)}
<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>
)}
</div>
</div>
</span>
</div>
)}
</div>
<div className="flex-shrink-0 w-20 sm:w-28">
<Link
href={
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
as={
request.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<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>
<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"
/>
</Link>
</div>
</>
</div>
);
};

View File

@@ -1,15 +1,13 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import {
defineMessages,
FormattedDate,
FormattedRelativeTime,
useIntl,
} from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import {
@@ -19,70 +17,25 @@ 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 CachedImage from '../../Common/CachedImage';
import ConfirmButton from '../../Common/ConfirmButton';
import Table from '../../Common/Table';
import RequestModal from '../../RequestModal';
import StatusBadge from '../../StatusBadge';
const messages = defineMessages({
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
seasons: 'Seasons',
notavailable: 'N/A',
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;
@@ -97,21 +50,23 @@ const RequestItem: React.FC<RequestItemProps> = ({
});
const { addToast } = useToasts();
const intl = useIntl();
const { user, hasPermission } = useUser();
const { 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 : null
);
const { data: requestData, mutate: revalidate } = useSWR<MediaRequest>(
`/api/v1/request/${request.id}`,
{
initialData: request,
}
inView ? `${url}?language=${locale}` : null
);
const {
data: requestData,
revalidate,
mutate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
initialData: request,
});
const [isRetrying, setRetrying] = useState(false);
@@ -134,7 +89,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
try {
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
revalidate(result.data);
mutate(result.data);
} catch (e) {
addToast(intl.formatMessage(messages.failedretry), {
autoDismiss: true,
@@ -147,24 +102,22 @@ const RequestItem: React.FC<RequestItemProps> = ({
if (!title && !error) {
return (
<div
className="h-64 w-full animate-pulse rounded-xl bg-gray-800 xl:h-28"
ref={ref}
/>
<tr className="w-full h-24 animate-pulse" ref={ref}>
<td colSpan={6}></td>
</tr>
);
}
if (!title || !requestData) {
return (
<RequestItemError
mediaId={requestData?.media.id}
revalidateList={revalidateList}
/>
<tr className="w-full h-24 animate-pulse">
<td colSpan={6}></td>
</tr>
);
}
return (
<>
<tr className="relative w-full h-24 p-2">
<RequestModal
show={showEditModal}
tmdbId={request.media.tmdbId}
@@ -177,26 +130,28 @@ const RequestItem: React.FC<RequestItemProps> = ({
setShowEditModal(false);
}}
/>
<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">
<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">
<Link
href={
requestData.type === 'movie'
@@ -204,295 +159,219 @@ const RequestItem: React.FC<RequestItemProps> = ({
: `/tv/${requestData.media.tmdbId}`
}
>
<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=""
layout="responsive"
width={600}
height={900}
objectFit="cover"
/>
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
<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'
]
}
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="flex items-center mt-1">
<img
src={requestData.requestedBy.avatar}
alt=""
className="w-5 mr-2 rounded-full"
/>
)}
</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 className="text-sm hover:underline">
{requestData.requestedBy.displayName}
</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>
),
})}
</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>
</span>
))}
</div>
)}
</div>
</div>
<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' }}
</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"
/>
<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 className="text-sm">
{requestData.modifiedBy.displayName} (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.updatedAt).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
)
</span>
</div>
)}
{requestData.status === MediaRequestStatus.PENDING &&
(hasPermission(Permission.MANAGE_REQUESTS) ||
(requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
<span className="w-full">
</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
className="w-full"
buttonType="primary"
onClick={() => setShowEditModal(true)}
buttonType="success"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<PencilIcon />
<span>{intl.formatMessage(messages.editrequest)}</span>
<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>
)}
{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>
</>
<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>
<Button
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>
</Button>
</span>
</>
)}
</Table.TD>
</tr>
);
};

View File

@@ -1,98 +1,51 @@
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 React, { useState } from 'react';
import useSWR from 'swr';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button';
import Header from '../Common/Header';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import RequestItem from './RequestItem';
import Header from '../Common/Header';
import Table from '../Common/Table';
import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../Common/PageTitle';
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: 'Most Recent',
sortAdded: 'Request Date',
sortModified: 'Last Modified',
});
enum Filter {
ALL = 'all',
PENDING = 'pending',
APPROVED = 'approved',
PROCESSING = 'processing',
AVAILABLE = 'available',
UNAVAILABLE = 'unavailable',
}
type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available';
type Sort = 'added' | 'modified';
const RequestList: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const { user } = useUser({
id: Number(router.query.userId),
});
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
const [pageIndex, setPageIndex] = useState(0);
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
const [currentSort, setCurrentSort] = useState<Sort>('added');
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
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>(
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
`/api/v1/request?take=${currentPageSize}&skip=${
pageIndex * currentPageSize
}&filter=${currentFilter}&sort=${currentSort}${
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
}`
}&filter=${currentFilter}&sort=${currentSort}`
);
// 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 />;
}
@@ -106,81 +59,73 @@ const RequestList: React.FC = () => {
return (
<>
<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" />
<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>
</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(globalMessages.all)}
{intl.formatMessage(messages.filterAll)}
</option>
<option value="pending">
{intl.formatMessage(globalMessages.pending)}
{intl.formatMessage(messages.filterPending)}
</option>
<option value="approved">
{intl.formatMessage(globalMessages.approved)}
{intl.formatMessage(messages.filterApproved)}
</option>
<option value="processing">
{intl.formatMessage(globalMessages.processing)}
{intl.formatMessage(messages.filterProcessing)}
</option>
<option value="available">
{intl.formatMessage(globalMessages.available)}
</option>
<option value="unavailable">
{intl.formatMessage(globalMessages.unavailable)}
{intl.formatMessage(messages.filterAvailable)}
</option>
</select>
</div>
<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" />
<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>
</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"
@@ -195,104 +140,114 @@ const RequestList: React.FC = () => {
</div>
</div>
</div>
{data.results.map((request) => {
return (
<div className="py-2" key={`request-list-${request.id}`}>
<RequestItem
request={request}
revalidateList={() => revalidate()}
/>
</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.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)}
>
{intl.formatMessage(messages.showallrequests)}
</Button>
</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>
)}
</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"
<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"
>
<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)}
>
<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>
{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>
</>
);
};

View File

@@ -23,14 +23,12 @@ 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.',
});
@@ -62,10 +60,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
const intl = useIntl();
const { user, hasPermission } = useUser();
const { data: quota } = useSWR<QuotaResponse>(
user &&
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
: null
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
);
useEffect(() => {
@@ -161,7 +156,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
}
};
const updateRequest = async (alsoApproveRequest = false) => {
const updateRequest = async () => {
setIsUpdating(true);
try {
@@ -174,23 +169,14 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
tags: requestOverrides?.tags,
});
if (alsoApproveRequest) {
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
}
addToast(
<span>
{intl.formatMessage(
alsoApproveRequest
? messages.requestApproved
: messages.requestedited,
{
title: data?.title,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
}
)}
{intl.formatMessage(messages.requestedited, {
title: data?.title,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})}
</span>,
{
appearance: 'success',
@@ -213,6 +199,12 @@ 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
@@ -223,44 +215,20 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
is4k ? messages.pending4krequest : messages.pendingrequest,
{ title: data?.title }
)}
onOk={() =>
hasPermission(Permission.MANAGE_REQUESTS)
? updateRequest(true)
: hasPermission(Permission.REQUEST_ADVANCED)
? updateRequest()
: cancelRequest()
}
onOk={() => (showEditButton ? updateRequest() : cancelRequest())}
okDisabled={isUpdating}
okText={
hasPermission(Permission.MANAGE_REQUESTS)
? intl.formatMessage(messages.approve)
: hasPermission(Permission.REQUEST_ADVANCED)
showEditButton
? intl.formatMessage(messages.edit)
: intl.formatMessage(messages.cancel)
}
okButtonType={
hasPermission(Permission.MANAGE_REQUESTS)
? 'success'
: hasPermission(Permission.REQUEST_ADVANCED)
? 'primary'
: 'danger'
}
okButtonType={showEditButton ? 'primary' : 'danger'}
onSecondary={
isOwner &&
hasPermission(
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
{ type: 'or' }
)
? () => cancelRequest()
: undefined
isOwner && showEditButton ? () => cancelRequest() : undefined
}
secondaryDisabled={isUpdating}
secondaryText={
isOwner &&
hasPermission(
[Permission.REQUEST_ADVANCED, Permission.MANAGE_REQUESTS],
{ type: 'or' }
)
isOwner && showEditButton
? intl.formatMessage(messages.cancel)
: undefined
}
@@ -276,20 +244,22 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
})}
{(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && (
<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 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>
)}
</Modal>
);

View File

@@ -30,15 +30,13 @@ 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}}',
requestseasons4k:
'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K',
requestall: 'Request All Seasons',
alreadyrequested: 'Already Requested',
selectseason: 'Select Season(s)',
season: 'Season',
@@ -47,7 +45,6 @@ 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.',
@@ -91,10 +88,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
});
const [tvdbId, setTvdbId] = useState<number | undefined>(undefined);
const { data: quota } = useSWR<QuotaResponse>(
user &&
(!requestOverrides?.user?.id || hasPermission(Permission.MANAGE_USERS))
? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota`
: null
user ? `/api/v1/user/${requestOverrides?.user?.id ?? user.id}/quota` : null
);
const currentlyRemaining =
@@ -102,7 +96,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
selectedSeasons.length +
(editRequest?.seasons ?? []).length;
const updateRequest = async (alsoApproveRequest = false) => {
const updateRequest = async () => {
if (!editRequest) {
return;
}
@@ -123,10 +117,6 @@ 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}`);
}
@@ -134,17 +124,12 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
addToast(
<span>
{selectedSeasons.length > 0
? intl.formatMessage(
alsoApproveRequest
? messages.requestApproved
: messages.requestedited,
{
title: data?.name,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
}
)
? intl.formatMessage(messages.requestedited, {
title: data?.name,
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
})
: intl.formatMessage(messages.requestcancelled, {
title: data?.name,
strong: function strong(msg) {
@@ -383,13 +368,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
loading={!data && !error}
backgroundClickable
onCancel={tvdbId ? () => setSearchModal({ show: true }) : onCancel}
onOk={() =>
editRequest
? hasPermission(Permission.MANAGE_REQUESTS)
? updateRequest(true)
: updateRequest()
: sendRequest()
}
onOk={() => (editRequest ? updateRequest() : sendRequest())}
title={intl.formatMessage(
editRequest
? is4k
@@ -404,23 +383,16 @@ 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(
is4k ? globalMessages.request4k : globalMessages.request
)
? intl.formatMessage(messages.requestall)
: selectedSeasons.length === 0
? intl.formatMessage(messages.selectseason)
: intl.formatMessage(
is4k ? messages.requestseasons4k : messages.requestseasons,
{
seasonCount: selectedSeasons.length,
}
)
: intl.formatMessage(messages.requestseasons, {
seasonCount: selectedSeasons.length,
})
}
okDisabled={
editRequest
@@ -434,14 +406,11 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
selectedSeasons.length === 0)
}
okButtonType={
editRequest
? settings.currentSettings.partialRequestsEnabled &&
selectedSeasons.length === 0
? 'danger'
: hasPermission(Permission.MANAGE_REQUESTS)
? 'success'
: 'primary'
: 'primary'
editRequest &&
settings.currentSettings.partialRequestsEnabled &&
selectedSeasons.length === 0
? 'danger'
: `primary`
}
cancelText={
editRequest
@@ -471,7 +440,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
!(
quota?.tv.limit &&
!settings.currentSettings.partialRequestsEnabled &&
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
) &&
getAllRequestedSeasons().length < getAllSeasons().length &&
!editRequest && (
@@ -488,7 +457,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
quota={quota?.tv}
remaining={
!settings.currentSettings.partialRequestsEnabled &&
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
? 0
: currentlyRemaining
}
@@ -499,7 +468,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
}
overLimit={
!settings.currentSettings.partialRequestsEnabled &&
unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
unrequestedSeasons.length > (quota?.tv.limit ?? 0)
? unrequestedSeasons.length
: undefined
}
@@ -513,7 +482,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
<thead>
<tr>
<th
className={`w-16 bg-gray-500 px-4 py-3 ${
className={`w-16 px-4 py-3 bg-gray-500 ${
!settings.currentSettings.partialRequestsEnabled &&
'hidden'
}`}
@@ -528,7 +497,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
toggleAllSeasons();
}
}}
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
className={`relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 pt-2 cursor-pointer focus:outline-none ${
quota?.tv.remaining &&
quota.tv.limit &&
quota.tv.remaining < unrequestedSeasons.length
@@ -540,28 +509,28 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
aria-hidden="true"
className={`${
isAllSeasons() ? 'bg-indigo-500' : 'bg-gray-800'
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
></span>
<span
aria-hidden="true"
className={`${
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
} 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`}
} 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`}
></span>
</span>
</th>
<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">
<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">
{intl.formatMessage(messages.season)}
</th>
<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">
<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">
{intl.formatMessage(messages.numberofepisodes)}
</th>
<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">
<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">
{intl.formatMessage(globalMessages.status)}
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700 bg-gray-600">
<tbody className="bg-gray-600 divide-y divide-gray-700">
{data?.seasons
.filter((season) => season.seasonNumber !== 0)
.map((season) => {
@@ -577,7 +546,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
return (
<tr key={`season-${season.id}`}>
<td
className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
className={`px-4 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap ${
!settings.currentSettings
.partialRequestsEnabled && 'hidden'
}`}
@@ -599,7 +568,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
toggleSeason(season.seasonNumber);
}
}}
className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
className={`pt-2 relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${
mediaSeason ||
(quota?.tv.limit &&
currentlyRemaining <= 0 &&
@@ -621,7 +590,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
isSelectedSeason(season.seasonNumber)
? 'bg-indigo-500'
: 'bg-gray-800'
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
} absolute h-4 w-9 mx-auto rounded-full transition-colors ease-in-out duration-200`}
></span>
<span
aria-hidden="true"
@@ -634,21 +603,21 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
isSelectedSeason(season.seasonNumber)
? 'translate-x-5'
: 'translate-x-0'
} 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`}
} 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`}
></span>
</span>
</td>
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
<td className="px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 whitespace-nowrap">
{season.seasonNumber === 0
? intl.formatMessage(messages.extras)
: intl.formatMessage(messages.seasonnumber, {
number: season.seasonNumber,
})}
</td>
<td className="whitespace-nowrap px-5 py-4 text-sm leading-5 text-gray-200 md:px-6">
<td className="px-5 py-4 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
{season.episodeCount}
</td>
<td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
<td className="py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6 whitespace-nowrap">
{!seasonRequest && !mediaSeason && (
<Badge>
{intl.formatMessage(
@@ -698,26 +667,28 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
</div>
{(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && (
<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 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>
)}
</Modal>
);

View File

@@ -1,9 +1,9 @@
import React from 'react';
import type { MediaStatus } from '../../../server/constants/media';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import Transition from '../Transition';
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';
interface RequestModalProps {
show: boolean;
@@ -26,6 +26,29 @@ 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"
@@ -36,25 +59,14 @@ const RequestModal: React.FC<RequestModalProps> = ({
leaveTo="opacity-0"
show={show}
>
{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}
/>
)}
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
is4k={is4k}
editRequest={editRequest}
/>
</Transition>
);
};

View File

@@ -15,7 +15,7 @@ const messages = defineMessages({
agentenabled: 'Enable Agent',
accessToken: 'Application API Token',
accessTokenTip:
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr',
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr',
userToken: 'User or Group Key',
userTokenTip:
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',

View File

@@ -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 Jellyseerr',
'<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr',
chatId: 'Chat ID',
chatIdTip:
'Start a chat with your bot, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',

View File

@@ -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, Jellyseerr must be served over HTTPS.',
'In order to receive web push notifications, Overseerr must be served over HTTPS.',
});
const NotificationsWebPush: React.FC = () => {

View File

@@ -20,11 +20,11 @@ const messages = defineMessages({
currentversion: 'Current Version',
viewchangelog: 'View Changelog',
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.',
'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.',
});
const REPO_RELEASE_API =
'https://api.github.com/repos/Fallenbagel/jellyseerr/releases?per_page=20';
'https://api.github.com/repos/sct/overseerr/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/Fallenbagel/jellyseerr"
href="https://github.com/sct/overseerr"
target="_blank"
rel="noreferrer"
className="text-yellow-100 underline transition duration-300 hover:text-white"

View File

@@ -16,14 +16,14 @@ import Releases from './Releases';
const messages = defineMessages({
about: 'About',
overseerrinformation: 'Jellyseerr Information',
overseerrinformation: 'Overseerr Information',
version: 'Version',
totalmedia: 'Total Media',
totalrequests: 'Total Requests',
gettingsupport: 'Getting Support',
githubdiscussions: 'GitHub Discussions',
timezone: 'Time Zone',
supportoverseerr: 'Support Jellyseerr',
supportoverseerr: 'Support Overseerr',
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="https://github.com/Fallenbagel/jellyseerr"
href="http://github.com/sct/overseerr"
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="success" className="ml-2">
{intl.formatMessage(messages.uptodate)}
<Badge badgeType="warning" className="ml-2">
{intl.formatMessage(messages.outofdate)}
</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://github.com/Fallenbagel/jellyseerr/blob/main/README.md"
href="https://docs.overseerr.dev"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://github.com/Fallenbagel/jellyseerr/blob/main/README.md
https://docs.overseerr.dev
</a>
</List.Item>
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
<a
href="https://github.com/Fallenbagel/jellyseerr/discussions"
href="https://github.com/sct/overseerr/discussions"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://github.com/Fallenbagel/jellyseerr/discussions
https://github.com/sct/overseerr/discussions
</a>
</List.Item>
<List.Item title="Discord">
<a
href="https://discord.gg/XDyAd3AuUV"
href="https://discord.gg/overseerr"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://discord.gg/XDyAd3AuUV
https://discord.gg/overseerr
</a>
</List.Item>
</List>
@@ -151,17 +151,27 @@ const SettingsAbout: React.FC = () => {
title={`${intl.formatMessage(messages.helppaycoffee)} ☕️`}
>
<a
href="https://www.buymeacoffee.com/fallen.bagel"
href="https://github.com/sponsors/sct"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://www.buymeacoffee.com/fallen.bagel
https://github.com/sponsors/sct
</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">

View File

@@ -1,28 +1,28 @@
import axios from 'axios';
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 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 { defineMessages, FormattedMessage, useIntl } from 'react-intl';
const messages = defineMessages({
jellyfinsettings: 'Jellyfin Settings',
jellyfinsettingsDescription:
'Configure the settings for your Jellyfin server. Jellyseerr scans your Jellyfin libraries to see what content is available.',
'Configure the settings for your Jellyfin server. Overseerr scans your Jellyfin libraries to see what content is available.',
timeout: 'Timeout',
save: 'Save Changes',
saving: 'Saving…',
jellyfinlibraries: 'Jellyfin Libraries',
jellyfinlibrariesDescription:
'The libraries Jellyseerr scans for titles. Click the button below if no libraries are listed.',
'The libraries Overseerr 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. 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!",
"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!",
notrunning: 'Not Running',
currentlibrary: 'Current Library: {name}',
librariesRemaining: 'Libraries Remaining: {count}',

View File

@@ -23,7 +23,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
jobsandcache: 'Jobs & Cache',
jobs: 'Jobs',
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.',
'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.',
jobname: 'Job Name',
jobtype: 'Type',
nextexecution: 'Next Execution',
@@ -35,7 +35,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
command: 'Command',
cache: 'Cache',
cacheDescription:
'Jellyseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
'Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
cacheflushed: '{cachename} cache flushed.',
cachename: 'Cache Name',
cachehits: 'Hits',

View File

@@ -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/jellyseerr.log</code>.',
'You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.',
time: 'Timestamp',
level: 'Severity',
label: 'Label',

View File

@@ -29,7 +29,7 @@ const messages = defineMessages({
general: 'General',
generalsettings: 'General Settings',
generalsettingsDescription:
'Configure global and default settings for Jellyseerr.',
'Configure global and default settings for Overseerr.',
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 Jellyseerr must be reloaded for changes to take effect)',
'Set external API access to read-only (requires HTTPS, and Overseerr 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 Jellyseerr to correctly register client IP addresses behind a proxy (Jellyseerr must be reloaded for changes to take effect)',
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr 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',

View File

@@ -22,7 +22,7 @@ const messages = defineMessages({
plex: 'Plex',
plexsettings: 'Plex Settings',
plexsettingsDescription:
'Configure the settings for your Plex server. Jellyseerr scans your Plex libraries to determine content availability.',
'Configure the settings for your Plex server. Overseerr 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 Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.',
'The libraries Overseerr 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. 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!",
"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!",
notrunning: 'Not Running',
currentlibrary: 'Current Library: {name}',
librariesRemaining: 'Libraries Remaining: {count}',

View File

@@ -5,7 +5,7 @@ import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
const messages = defineMessages({
welcome: 'Welcome to Jellyseerr',
welcome: 'Welcome to Overseerr',
signinMessage: 'Get started by signing in with your Plex account',
});

View File

@@ -1,14 +1,14 @@
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
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';
import JellyfinLogin from '../Login/JellyfinLogin';
import axios from 'axios';
import { defineMessages, FormattedMessage } from 'react-intl';
import Accordion from '../Common/Accordion';
import { MediaServerType } from '../../../server/constants/server';
const messages = defineMessages({
welcome: 'Welcome to Jellyseerr',
welcome: 'Welcome to Overseerr',
signinMessage: 'Get started by signing in',
signinWithJellyfin: 'Use your Jellyfin account',
signinWithPlex: 'Use your Plex account',

View File

@@ -13,11 +13,8 @@ interface StatusBadgeProps {
status?: MediaStatus;
is4k?: boolean;
inProgress?: boolean;
plexUrl?: string;
tmdbId?: number;
mediaUrl?: string;
mediaUrl4k?: string;
mediaType?: 'movie' | 'tv';
}
const StatusBadge: React.FC<StatusBadgeProps> = ({

View File

@@ -9,7 +9,7 @@ export interface SettingsContextProps {
const defaultSettings = {
initialized: false,
applicationTitle: 'Jellyseerr',
applicationTitle: 'Overseerr',
applicationUrl: '',
hideAvailable: false,
localLogin: true,

View File

@@ -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'Jellyseerr. 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'Overseerr. 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 Jellyseerr. 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 Overseerr. 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'Jellyseerr. Totes les sol·licituds que faci un usuari amb aquest permís saprovaran automàticament.",
"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 saprovaran automàticament.",
"components.PermissionEdit.managerequests": "Gestiona les sol·licituds",
"components.PermissionEdit.autoapproveSeriesDescription": "Concedeix laprovació 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": "Jellyseerr s'ha actualitzat! Feu clic al botó següent per tornar a carregar la pàgina.",
"components.StatusChacker.newversionDescription": "Overseerr 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 Jellyseerr",
"components.Setup.welcome": "Benvingut a Overseerr",
"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 Jellyseerr registrar correctament les adreces IP del client darrere dun servidor intermediari (sha de tornar a carregar Jellyseerr perquè els canvis tinguin efecte)",
"components.Settings.trustProxyTip": "Permet a Overseerr registrar correctament les adreces IP del client darrere dun servidor intermediari (sha de tornar a carregar Overseerr 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. Jellyseerr explora les vostres biblioteques Plex per determinar la disponibilitat de continguts.",
"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.plexsettings": "Configuració de Plex",
"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.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.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 sexecutarà 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.manualscanDescription": "Normalment, només sexecutarà 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.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 Jellyseerr.",
"components.Settings.generalsettingsDescription": "Configureu els paràmetres globals i predeterminats per a Overseerr.",
"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 Jellyseerr 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 Overseerr 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/jellyseerr.log</code>.",
"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.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": "Jellyseerr realitza certes tasques de manteniment com a feines programades regularment, però també es poden activar manualment a continuació. Lexecució manual dun treball no alterarà la seva programació.",
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr realitza certes tasques de manteniment com a feines programades regularment, però també es poden activar manualment a continuació. Lexecució manual dun 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": "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.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.cache": "Memòria cau",
"components.Settings.SettingsAbout.version": "Versió",
"components.Settings.SettingsAbout.timezone": "Zona horària",
"components.Settings.SettingsAbout.supportoverseerr": "Dóna suport a Jellyseerr",
"components.Settings.SettingsAbout.overseerrinformation": "Informació d'Jellyseerr",
"components.Settings.SettingsAbout.supportoverseerr": "Dóna suport a Overseerr",
"components.Settings.SettingsAbout.overseerrinformation": "Informació d'Overseerr",
"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 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.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.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": "Jellyseer (Estable)",
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr (Desenvolupament)",
"components.Layout.VersionStatus.streamstable": "Overseer (Estable)",
"components.Layout.VersionStatus.streamdevelop": "Overseerr (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 Jellyseerr",
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Per tal de rebre notificacions push web, Jellyseerr s'ha de servir mitjançant HTTPS.",
"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.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 Jellyseerr",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Registreu una aplicació</ApplicationRegistrationLink> per utilitzar-la amb Overseerr",
"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",

View File

@@ -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 Jellyseerr-Einstellungen.",
"components.Settings.generalsettingsDescription": "Konfiguriere Globale und Standard Overseerr-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. 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.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.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 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.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.plexsettings": "Plex-Einstellungen",
"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.plexsettingsDescription": "Konfiguriere die Einstellungen für deinen Plex-Server. Overseerr 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 Jellyseerr",
"components.Setup.welcome": "Willkommen bei Overseerr",
"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": "Jellyseerr-Informationen",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr-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 Jellyseerr",
"components.Settings.SettingsAbout.supportoverseerr": "Unterstütze Overseerr",
"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 Jellyseerr 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 Overseerr 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": "Jellyseerr neu laden",
"components.StatusChacker.reloadOverseerr": "Overseerr neu laden",
"components.StatusChacker.newversionavailable": "Anwendungsaktualisierung",
"components.StatusChacker.newversionDescription": "Jellyseerr wurde aktualisiert! Bitte klicke auf die Schaltfläche unten, um die Seite neu zu laden.",
"components.StatusChacker.newversionDescription": "Overseerr 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 Jellyseerr-Anfragen. Alle Anfragen eines Benutzers mit dieser Berechtigung werden automatisch genehmigt.",
"components.PermissionEdit.managerequestsDescription": "Gewähre Berechtigung zum Verwalten von Overseerr-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 Jellyseerr muss neu gestartet werden, damit die Änderungen wirksam werden)",
"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.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 Jellyseerr-Benutzern. Benutzer mit dieser Berechtigung können Benutzer mit Adminrechten nicht bearbeiten oder Adminrechte erteilen.",
"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.users": "Benutzer verwalten",
"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.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.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 Jellyseerr Client IP Adressen hinter einem Proxy korrekt zu registrieren (Jellyseerr muss neu gestartet werden, damit die Änderungen wirksam werden)",
"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.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": "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.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.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": "Jellyseerr speichert Anfragen an externe API Endpunkte zwischen, um die Leistung zu optimieren und unnötige API Aufrufe zu minimieren.",
"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.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/jellyseerr.log</code> anzeigen.",
"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.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": "Jellyseerr Stabil",
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Entwicklung",
"components.Layout.VersionStatus.streamstable": "Overseerr Stabil",
"components.Layout.VersionStatus.streamdevelop": "Overseerr 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 Jellyseerr über HTTPS gehen.",
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Um web push Benachrichtigungen zu erhalten, muss Overseerr ü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 Jellyseerr",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Registriere eine Anwendung</ApplicationRegistrationLink> für die Benutzung mit Overseerr",
"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 Jellyseerr",
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Erstelle einen Bot</CreateBotLink> für die Verwendung mit Overseerr",
"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.",

View File

@@ -1,6 +1,6 @@
{
"components.PermissionEdit.users": "Διαχείριση Χρηστών",
"components.PermissionEdit.settingsDescription": "Εκχώρηση άδειας για τροποποίηση των ρυθμίσεων Jellyseerr. Ένας χρήστης χρειάζεται να έχει αυτό το δικαίωμα για να το εκχωρήσει σε άλλους.",
"components.PermissionEdit.settingsDescription": "Εκχώρηση άδειας για τροποποίηση των ρυθμίσεων Overseerr. Ένας χρήστης χρειάζεται να έχει αυτό το δικαίωμα για να το εκχωρήσει σε άλλους.",
"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": "Εκχώρηση άδειας για τη διαχείριση αιτημάτων Jellyseerr. Όλα τα αιτήματα που υποβάλλει ένας χρήστης με αυτήν την άδεια θα εγκριθούν αυτόματα.",
"components.PermissionEdit.managerequestsDescription": "Εκχώρηση άδειας για τη διαχείριση αιτημάτων Overseerr. Όλα τα αιτήματα που υποβάλλει ένας χρήστης με αυτήν την άδεια θα εγκριθούν αυτόματα.",
"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": "Εκχώρηση άδειας για διαχείρηση των Jellyseerr χρηστών. Οι χρήστες με αυτή την άδεια δεν μπορούν να τροποποιήσουν τους χρήστες με το προνόμιο του διαχειριστή ή να κάνουν κάποιον διαχειριστή.",
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
"components.PermissionEdit.usersDescription": "Εκχώρηση άδειας για διαχείρηση των Overseerr χρηστών. Οι χρήστες με αυτή την άδεια δεν μπορούν να τροποποιήσουν τους χρήστες με το προνόμιο του διαχειριστή ή να κάνουν κάποιον διαχειριστή.",
"components.Layout.VersionStatus.streamdevelop": "Overseerr 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> για χρήση με Jellyseerr",
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Δημιουργία bot</CreateBotLink> για χρήση με Overseerr",
"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, το Jellyseerr πρέπει να προβάλλεται μέσω HTTPS.",
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "Για να λάβεις ειδοποιήσεις push, το Overseerr πρέπει να προβάλλεται μέσω 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> για χρήση με Jellyseerr",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Καταχώρηση εφαρμογής</ApplicationRegistrationLink> για χρήση με Overseerr",
"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/jellyseerr.log </code>.",
"components.Settings.SettingsLogs.logsDescription": "Μπορείτε επίσης να δείτε αυτά τα αρχεία καταγραφής απευθείας μέσω του <code> stdout </code> ή στο <code> {configDir} /logs/overseerr.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": "Το Jellyseerr αποθηκεύει προσωρινά αιτήματα σε εξωτερικά τελικά σημεία API για τη βελτιστοποίηση της απόδοσης και την αποφυγή περιττών κλήσεων API.",
"components.Settings.SettingsJobsCache.cacheDescription": "Το Overseerr αποθηκεύει προσωρινά αιτήματα σε εξωτερικά τελικά σημεία API για τη βελτιστοποίηση της απόδοσης και την αποφυγή περιττών κλήσεων API.",
"components.Settings.SettingsJobsCache.cache": "Κρυφή μνήμη",
"components.Settings.SettingsAbout.version": "Έκδοση",
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Οι τελευταίες αλλαγές στο <code>develop</code> branch του Jellyseerr δεν φαίνονται παρακάτω. Παρακαλώ ανάτρεξε στο ιστορικό δευσμεύσεων του branch στο <GithubLink>GitHub</GithubLink> για λεπτομέρειες.",
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "Οι τελευταίες αλλαγές στο <code>develop</code> branch του Overseerr δεν φαίνονται παρακάτω. Παρακαλώ ανάτρεξε στο ιστορικό δευσμεύσεων του 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": "Υποστήριξε το Jellyseerr",
"components.Settings.SettingsAbout.supportoverseerr": "Υποστήριξε το Overseerr",
"components.Settings.SettingsAbout.preferredmethod": "Προτιμώνενο",
"components.Settings.SettingsAbout.overseerrinformation": "Πληροφορίες Jellyseerr",
"components.Settings.SettingsAbout.overseerrinformation": "Πληροφορίες Overseerr",
"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": "Το Jellyseerr έχει ενημερωθεί! Κάνε κλικ στο παρακάτω κουμπί για να φορτώσει ξανά η σελίδα.",
"components.StatusChacker.newversionDescription": "Το Overseerr έχει ενημερωθεί! Κάνε κλικ στο παρακάτω κουμπί για να φορτώσει ξανά η σελίδα.",
"components.StatusBadge.status4k": "4K {status}",
"components.Setup.welcome": "Καλώς ήρθες στο Jellyseerr",
"components.Setup.welcome": "Καλώς ήρθες στο Overseerr",
"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": "Επίτρεψε στο Jellyseerr να καταχωρίζει σωστά τις διευθύνσεις IP του πελάτη πίσω από έναν διακομιστή μεσολάβησης (το Jellyseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
"components.Settings.trustProxyTip": "Επίτρεψε στο Overseerr να καταχωρίζει σωστά τις διευθύνσεις IP του πελάτη πίσω από έναν διακομιστή μεσολάβησης (το Overseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
"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 διακομιστή σου. Το Jellyseerr σαρώνει τις βιβλιοθήκες του Plex για να προσδιορίσει τη διαθεσιμότητα περιεχομένου.",
"components.Settings.plexsettingsDescription": "Διαμόρφωσε τις ρυθμίσεις του Plex διακομιστή σου. Το Overseerr σαρώνει τις βιβλιοθήκες του Plex για να προσδιορίσει τη διαθεσιμότητα περιεχομένου.",
"components.Settings.plexsettings": "Ρυθμίσεις Plex",
"components.Settings.plexlibrariesDescription": "Οι βιβλιοθήκες που το Jellyseerr θα σαρώνει για τίτλους. Ρύθμισε και αποθήκευσε τις ρυθμίσεις της σύνδεσης του Plex και, στη συνέχεια, κάνε κλικ στο παρακάτω κουμπί, εάν δεν εμφανιστύν οι βιβλιοθήκες.",
"components.Settings.plexlibrariesDescription": "Οι βιβλιοθήκες που το Overseerr θα σαρώνει για τίτλους. Ρύθμισε και αποθήκευσε τις ρυθμίσεις της σύνδεσης του 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 ώρες. Το Jellyseerr θα ελέγχει πιο επιθετικά τις προσθήκες που έγιναν πρόσφατα στον διακομιστή Plex. Εάν αυτή είναι η πρώτη φορά που ρυθμίζεις το Plex, συνιστάται μια εφάπαξ πλήρης χειροκίνητη σάρωση βιβλιοθήκης!",
"components.Settings.manualscanDescription": "Κανονικά, αυτό εκτελείται μόνο μία φορά κάθε 24 ώρες. Το Overseerr θα ελέγχει πιο επιθετικά τις προσθήκες που έγιναν πρόσφατα στον διακομιστή 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 και το Jellyseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
"components.Settings.csrfProtectionTip": "Ορισμός εξωτερικής πρόσβασης API σε μόνο για ανάγνωση (απαιτείται HTTPS και το Overseerr πρέπει να φορτωθεί ξανά για να εφαρμοστούν οι αλλαγές)",
"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": "Διαμόρφωση των γενικών και προεπιλεγμένων ρυθμίσεων του Jellyseerr.",
"components.Settings.generalsettingsDescription": "Διαμόρφωση των γενικών και προεπιλεγμένων ρυθμίσεων του Overseerr.",
"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": "Το Jellyseerr εκτελεί ορισμένες εργασίες συντήρησης ως τακτικά προγραμματισμένες εργασίες, αλλά μπορούν επίσης να ενεργοποιηθούν χειροκίνητα παρακάτω. Η χειροκίνητη εκτέλεση μιας εργασίας δεν θα αλλάξει το χρονοδιάγραμμα του.",
"components.Settings.SettingsJobsCache.jobsDescription": "Το Overseerr εκτελεί ορισμένες εργασίες συντήρησης ως τακτικά προγραμματισμένες εργασίες, αλλά μπορούν επίσης να ενεργοποιηθούν χειροκίνητα παρακάτω. Η χειροκίνητη εκτέλεση μιας εργασίας δεν θα αλλάξει το χρονοδιάγραμμα του.",
"components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Template Variable Help",
"components.Settings.Notifications.NotificationsLunaSea.webhookUrlTip": "Ο χρήστης σου ή η συσκευή <LunaSeaLink>ειδοποίηση webhook URL</LunaSeaLink>",
"components.RequestModal.numberofepisodes": "# Αριθμός Επεισοδίων",

View File

@@ -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": "Jellyseerr Develop",
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
"components.Layout.VersionStatus.streamdevelop": "Overseerr Develop",
"components.Layout.VersionStatus.streamstable": "Overseerr 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 Jellyseerr requests. All requests made by a user with this permission will be automatically approved.",
"components.PermissionEdit.managerequestsDescription": "Grant permission to manage Overseerr 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 Jellyseerr settings. A user must have this permission to grant it to others.",
"components.PermissionEdit.settingsDescription": "Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.",
"components.PermissionEdit.users": "Manage Users",
"components.PermissionEdit.usersDescription": "Grant permission to manage Jellyseerr users. Users with this permission cannot modify users with or grant the Admin privilege.",
"components.PermissionEdit.usersDescription": "Grant permission to manage Overseerr 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 Jellyseerr",
"components.Settings.Notifications.NotificationsPushover.accessTokenTip": "<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr",
"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, Jellyseerr must be served over HTTPS.",
"components.Settings.Notifications.NotificationsWebPush.httpsRequirement": "In order to receive web push notifications, Overseerr 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 Jellyseerr",
"components.Settings.Notifications.botApiTip": "<CreateBotLink>Create a bot</CreateBotLink> for use with Overseerr",
"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 Jellyseerr 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 Overseerr 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": "Jellyseerr Information",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr Information",
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
"components.Settings.SettingsAbout.supportoverseerr": "Support Jellyseerr",
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
"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": "Jellyseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr 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": "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.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.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/jellyseerr.log</code>.",
"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.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 Jellyseerr must be reloaded for changes to take effect)",
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr 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 Jellyseerr.",
"components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.",
"components.Settings.hideAvailable": "Hide Available Media",
"components.Settings.jellyfinlibraries": "Jellyfin Libraries",
"components.Settings.jellyfinlibrariesDescription": "The libraries Jellyseerr scans for titles. Click the button below if no libraries are listed.",
"components.Settings.jellyfinlibrariesDescription": "The libraries Overseerr 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. Jellyseerr scans your Jellyfin libraries to see what content is available.",
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your Jellyfin server. Overseerr 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. 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.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.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 Jellyseerr 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 Overseerr 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. Jellyseerr scans your Plex libraries to determine content availability.",
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Overseerr 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 Jellyseerr to correctly register client IP addresses behind a proxy (Jellyseerr must be reloaded for changes to take effect)",
"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.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 Jellyseerr",
"components.Setup.welcome": "Welcome to Overseerr",
"components.StatusBadge.status4k": "4K {status}",
"components.StatusChacker.newversionDescription": "Jellyseerr has been updated! Please click the button below to reload the page.",
"components.StatusChacker.newversionDescription": "Overseerr 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",

View File

@@ -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 Jellyseerr",
"components.Settings.SettingsAbout.overseerrinformation": "Información de Overseerr",
"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 Jellyseerr",
"components.Setup.welcome": "Bienvenido a Overseerr",
"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. Jellyseerr escanea tu biblioteca para determinar la disponibilidad de contenidos.",
"components.Settings.plexsettingsDescription": "Configure los ajustes de su servidor Plex. Overseerr escanea tu biblioteca para determinar la disponibilidad de contenidos.",
"components.Settings.plexsettings": "Ajustes de Plex",
"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.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.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. 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.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.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 Jellyseerr.",
"components.Settings.generalsettingsDescription": "Configuración global y ajustes por defecto de Overseerr.",
"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 Jellyseerr",
"components.Settings.SettingsAbout.supportoverseerr": "Apoya a Overseerr",
"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": Jellyseerr se ha actualizado!Haga clic en el botón de abajo para volver a cargar la 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.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/jellyseerr.log</code>.",
"components.Settings.SettingsLogs.logsDescription": "Puede ver estos logs directamente via <code>stdout</code> o en <code>{configDir}/logs/overseerr.log</code>.",
"components.Settings.SettingsLogs.logs": "Logs",
"components.Settings.SettingsLogs.level": "Severidad",
"components.Settings.SettingsLogs.label": "Etiqueta",

Some files were not shown because too many files have changed in this diff Show More