Merge branch 'develop'

pull/2563/head
sct 3 years ago
commit e840489e57
No known key found for this signature in database
GPG Key ID: 77D146606D30DCCD

@ -14,15 +14,15 @@ jobs:
runs-on: ubuntu-20.04
container: node:14.16-alpine
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: install dependencies
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
- name: Lint
run: yarn lint
- name: build
- name: Build
run: yarn build
build_and_push:
@ -70,7 +70,7 @@ jobs:
ghcr.io/sct/overseerr:develop
ghcr.io/sct/overseerr:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- # Temporary fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896

@ -0,0 +1,44 @@
name: Overseerr Preview
on:
push:
tags:
- 'preview-*'
jobs:
build_and_push:
name: Build & Publish Docker Preview Images
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
build-args: |
COMMIT_TAG=${{ github.sha }}
tags: |
sctx/overseerr:${{ steps.get_version.outputs.VERSION }}
ghcr.io/sct/overseerr:${{ steps.get_version.outputs.VERSION }}

@ -11,16 +11,17 @@ jobs:
runs-on: ubuntu-20.04
container: node:14.16-alpine
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: install dependencies
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
- name: Lint
run: yarn lint
- name: build
- name: Build
run: yarn build
semantic-release:
name: Tag and release latest version
needs: test
@ -57,6 +58,7 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: npx semantic-release
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: semantic-release
@ -77,7 +79,6 @@ jobs:
run: git checkout master
- name: Pull latest changes
run: git pull
- name: Prepare
id: prepare
run: |
@ -87,35 +88,31 @@ jobs:
else
echo ::set-output name=RELEASE::edge
fi
- name: Set Up QEMU
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v2
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1.2.0
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: semantic-release
@ -124,7 +121,6 @@ jobs:
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2.1.5
- name: Combine Job Status
id: status
run: |
@ -134,7 +130,6 @@ jobs:
else
echo ::set-output name=status::$WORKFLOW_CONCLUSION
fi
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
with:

@ -22,15 +22,15 @@ jobs:
runs-on: ubuntu-20.04
container: node:14.16-alpine
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: install dependencies
- name: Install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
- name: Lint
run: yarn lint
- name: build
- name: Build
run: yarn build
build-snap:

@ -1,28 +1,30 @@
FROM node:14.16-alpine AS BUILD_IMAGE
WORKDIR /app
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app
WORKDIR /app
RUN \
case "${TARGETPLATFORM}" in \
'linux/arm64') apk add --no-cache python make g++ ;; \
'linux/arm/v7') apk add --no-cache python make g++ ;; \
esac
RUN yarn --frozen-lockfile --network-timeout 1000000 && \
yarn build
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --network-timeout 1000000
COPY . ./
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
RUN yarn build
# remove development dependencies
RUN yarn install --production --ignore-scripts --prefer-offline
RUN rm -rf src && \
rm -rf server
RUN rm -rf src server
RUN touch config/DOCKER
@ -31,11 +33,12 @@ RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:14.16-alpine
WORKDIR /app
RUN apk add --no-cache tzdata tini
# copy from build image
COPY --from=BUILD_IMAGE /app /app
WORKDIR /app
COPY --from=BUILD_IMAGE /app ./
ENTRYPOINT [ "/sbin/tini", "--" ]
CMD [ "yarn", "start" ]

@ -103,8 +103,10 @@ components:
properties:
apiKey:
type: string
example: 'anapikey'
readOnly: true
appLanguage:
type: string
example: en
applicationTitle:
type: string
example: Overseerr
@ -126,6 +128,9 @@ components:
localLogin:
type: boolean
example: true
newPlexLogin:
type: boolean
example: true
defaultPermissions:
type: number
example: 32
@ -1128,6 +1133,15 @@ components:
properties:
webhookUrl:
type: string
WebPushSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
WebhookSettings:
type: object
properties:
@ -1142,6 +1156,8 @@ components:
properties:
webhookUrl:
type: string
authHeader:
type: string
jsonPayload:
type: string
TelegramSettings:
@ -1196,6 +1212,22 @@ components:
type: string
priority:
type: number
LunaSeaSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
webhookUrl:
type: string
profileName:
type: string
NotificationEmailSettings:
type: object
properties:
@ -1223,6 +1255,12 @@ components:
secure:
type: boolean
example: false
ignoreTls:
type: boolean
example: false
requireTls:
type: boolean
example: false
authUser:
type: string
nullable: true
@ -2397,22 +2435,22 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/telegram:
/settings/notifications/lunasea:
get:
summary: Get Telegram notification settings
description: Returns current Telegram notification settings in a JSON object.
summary: Get LunaSea notification settings
description: Returns current LunaSea notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Telegram settings
description: Returned LunaSea settings
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
$ref: '#/components/schemas/LunaSeaSettings'
post:
summary: Update Telegram notification settings
description: Update Telegram notification settings with the provided values.
summary: Update LunaSea notification settings
description: Updates LunaSea notification settings with the provided values.
tags:
- settings
requestBody:
@ -2420,18 +2458,18 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
$ref: '#/components/schemas/LunaSeaSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
/settings/notifications/telegram/test:
$ref: '#/components/schemas/LunaSeaSettings'
/settings/notifications/lunasea/test:
post:
summary: Test Telegram settings
description: Sends a test notification to the Telegram agent.
summary: Test LunaSea settings
description: Sends a test notification to the LunaSea agent.
tags:
- settings
requestBody:
@ -2439,7 +2477,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
$ref: '#/components/schemas/LunaSeaSettings'
responses:
'204':
description: Test notification attempted
@ -2581,6 +2619,98 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/telegram:
get:
summary: Get Telegram notification settings
description: Returns current Telegram notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned Telegram settings
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
post:
summary: Update Telegram notification settings
description: Update Telegram notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
/settings/notifications/telegram/test:
post:
summary: Test Telegram settings
description: Sends a test notification to the Telegram agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/webpush:
get:
summary: Get Web Push notification settings
description: Returns current Web Push notification settings in a JSON object.
tags:
- settings
responses:
'200':
description: Returned web push settings
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
post:
summary: Update Web Push notification settings
description: Updates Web Push notification settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
/settings/notifications/webpush/test:
post:
summary: Test Web Push settings
description: Sends a test notification to the Web Push agent.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WebPushSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/webhook:
get:
summary: Get webhook notification settings
@ -2903,6 +3033,32 @@ paths:
type: array
items:
$ref: '#/components/schemas/User'
/user/registerPushSubscription:
post:
summary: Register a web push /user/registerPushSubscription
description: Registers a web push subscription for the logged-in user
tags:
- users
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
endpoint:
type: string
auth:
type: string
p256dh:
type: string
required:
- endpoint
- auth
- p256dh
responses:
'204':
description: Successfully registered push subscription
/user/{userId}:
get:
summary: Get user by ID

@ -17,11 +17,11 @@
},
"license": "MIT",
"dependencies": {
"@headlessui/react": "^1.0.0",
"@headlessui/react": "^1.1.1",
"@heroicons/react": "^1.0.1",
"@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.62",
"@tanem/react-nprogress": "^3.0.64",
"ace-builds": "^1.4.12",
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
@ -30,35 +30,34 @@
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.5",
"copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.2.9",
"country-flag-icons": "^1.2.10",
"csurf": "^1.11.0",
"email-templates": "^8.0.4",
"express": "^4.17.1",
"express-openapi-validator": "^4.12.7",
"express-openapi-validator": "^4.12.9",
"express-rate-limit": "^5.2.6",
"express-session": "^1.17.1",
"formik": "^2.2.6",
"gravatar-url": "^3.1.0",
"gravatar-url": "3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.21",
"next": "10.1.3",
"node-cache": "^5.1.2",
"node-schedule": "^2.0.0",
"nodemailer": "^6.5.0",
"nookies": "^2.5.2",
"openpgp": "^5.0.0-1",
"nodemailer": "^6.6.0",
"openpgp": "^5.0.0-2",
"plex-api": "^5.3.1",
"pug": "^3.0.2",
"react": "17.0.2",
"react-ace": "^9.3.0",
"react-animate-height": "^2.0.23",
"react-dom": "17.0.2",
"react-intersection-observer": "^8.31.0",
"react-intl": "5.15.8",
"react-markdown": "^6.0.0",
"react-intersection-observer": "^8.31.1",
"react-intl": "5.17.4",
"react-markdown": "^6.0.1",
"react-select": "^4.3.0",
"react-spring": "^8.0.27",
"react-toast-notifications": "^2.4.3",
"react-toast-notifications": "^2.4.4",
"react-transition-group": "^4.4.1",
"react-truncate-markup": "^5.1.0",
"react-use-clipboard": "1.0.7",
@ -66,17 +65,18 @@
"secure-random-password": "^0.2.2",
"sqlite3": "^5.0.2",
"swagger-ui-express": "^4.1.6",
"swr": "^0.5.5",
"swr": "^0.5.6",
"typeorm": "^0.2.32",
"uuid": "^8.3.2",
"web-push": "^3.4.4",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.2",
"winston-daily-rotate-file": "^4.5.3",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.32.9"
},
"devDependencies": {
"@babel/cli": "^7.13.14",
"@babel/cli": "^7.13.16",
"@commitlint/cli": "^12.1.1",
"@commitlint/config-conventional": "^12.1.1",
"@semantic-release/changelog": "^5.0.1",
@ -96,10 +96,10 @@
"@types/express-rate-limit": "^5.1.1",
"@types/express-session": "^1.17.3",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.41",
"@types/node": "^15.0.1",
"@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.1",
"@types/react": "^17.0.3",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/react-select": "^4.0.15",
"@types/react-toast-notifications": "^2.4.0",
@ -107,6 +107,7 @@
"@types/secure-random-password": "^0.2.0",
"@types/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0",
"@types/web-push": "^3.3.0",
"@types/xml2js": "^0.4.8",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.11",
@ -118,9 +119,9 @@
"commitizen": "^4.2.3",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.2.0",
"eslint-plugin-formatjs": "^2.14.6",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-formatjs": "^2.14.10",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.2",
@ -129,11 +130,11 @@
"husky": "4.3.8",
"lint-staged": "^10.5.4",
"nodemon": "^2.0.7",
"postcss": "^8.2.10",
"postcss": "^8.2.13",
"prettier": "^2.2.1",
"semantic-release": "^17.4.2",
"semantic-release-docker-buildx": "^1.0.1",
"tailwindcss": "^2.1.1",
"tailwindcss": "^2.1.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1115.2"><metadata/><defs><path id="a" d="m0 0h1024v1024h-1024z"/></defs><use width="100%" height="100%" fill="#fff" fill-opacity="0" transform="rotate(.704 1914.4 -5491.6)" xlink:href="#a"/><g><path fill="#fff" d="m105.76 154.15-1.263 714.59c-60.261 6.782-105.02-23.858-104.28-84.025l-0.216-594.25c2.312-188.02 175.85-231.02 280.22-154.52l530.2 314.93c74.563 53.572 88.403 151.53 49.965 218.76-6.873-52.739-29.067-83.101-73.823-113.74l-597.52-345.84c-44.756-30.639-82.453-23.58-83.286 44.109zm-54.377 751.54c44.941 15.597 90.16 8.63 128.04-13.47l621.16-353.43c36.958 53.109 28.79 105.66-16.706 135.19l-522.65 294.46c-75.673 36.68-172.98-2.127-209.85-62.757z" transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)"/><path fill="#ffc230" d="m240.52 702.59 365.02-216.68-364.35-197.07z" transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)"/></g></svg>

Before

Width:  |  Height:  |  Size: 969 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="216.7" height="216.9" viewBox="0 0 216.7 216.9"><path fill="#EEE" fill-rule="evenodd" d="M216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95C53.05 10.517 78.617 0 108.45 0c29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" clip-rule="evenodd"/><path fill="#3A3F51" fill-rule="evenodd" d="M194.65 42.5l-22.4 22.4C159.152 77.998 158 89.4 158 109.5c0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05L61.1 43.85C74.102 56.852 90.767 60.2 108.7 60.2c18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" clip-rule="evenodd"/><g clip-rule="evenodd"><path fill="#0CF" fill-rule="evenodd" d="M78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3 0 8.267-2.916 15.35-8.75 21.25-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z"/><path fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="2" d="M157.8 59.75l-15 14.65M30.785 32.526L71.65 73.25m84.6 84.25l27.808 28.78m1.855-153.894L157.8 59.75m-125.45 126l27.35-27.4"/><path fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="7" d="M157.8 59.75l-16.95 17.2M58.97 60.604l17.2 17.15M59.623 158.43l16.75-17.4m61.928-1.396l18.028 17.945"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>You are offline</title>
<!-- Inline the page's stylesheet. -->
<style>
body {
font-family: helvetica, arial, sans-serif;
margin: 2em;
background-color: #111827;
}
h1 {
color: #6366F1;
}
p {
margin-block: 1rem;
}
button {
display: block;
}
</style>
</head>
<body>
<h1>You are offline</h1>
<button type="button">⤾ Reload</button>
<!-- Inline the page's JavaScript file. -->
<script>
// Manual reload feature.
document.querySelector("button").addEventListener("click", () => {
window.location.reload();
});
// Listen to changes in the network state, reload when online.
// This handles the case when the device is completely offline.
window.addEventListener('online', () => {
window.location.reload();
});
// Check if the server is responding and reload the page if it is.
// This handles the case when the device is online, but the server
// is offline or misbehaving.
async function checkNetworkAndReload() {
try {
const response = await fetch('.');
// Verify we get a valid response from the server
if (response.status >= 200 && response.status < 500) {
window.location.reload();
return;
}
} catch {
// Unable to connect to the server, ignore.
}
window.setTimeout(checkNetworkAndReload, 2500);
}
checkNetworkAndReload();
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 455 KiB

@ -4,19 +4,77 @@
"start_url": "./",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-192x192_maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/android-chrome-512x512.png",
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-512x512_maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#2d3748",
"background_color": "#2d3748",
"display": "standalone"
"theme_color": "#1f2937",
"background_color": "#1f2937",
"display": "standalone",
"shortcuts": [
{
"name": "Discover",
"url": "./",
"icons": [
{
"src": "./sparkles-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "Requests",
"url": "./requests",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "Profile",
"url": "./profile",
"icons": [
{
"src": "./user-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "Settings",
"url": "./profile/settings",
"icons": [
{
"src": "./cog-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

@ -0,0 +1,136 @@
// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const OFFLINE_VERSION = 3;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "/offline.html";
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the
// response isn't fulfilled from the HTTP cache; i.e., it will be from
// the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})()
);
// Force the waiting service worker to become the active service worker.
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === "navigate") {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
});
self.addEventListener('push', (event) => {
const payload = event.data ? event.data.json() : {};
const options = {
body: payload.message,
icon: payload.image ? payload.image : 'android-chrome-192x192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: '2',
actionUrl: payload.actionUrl,
requestId: payload.requestId,
},
actions: [],
}
if (payload.actionUrl){
options.actions.push(
{
action: 'viewmedia',
title: 'View Media',
}
);
}
if (payload.notificationType === 'MEDIA_PENDING') {
options.actions.push(
{
action: 'approve',
title: 'Approve',
},
{
action: 'decline',
title: 'Decline',
}
);
}
event.waitUntil(
self.registration.showNotification(payload.subject, options)
);
})
self.addEventListener('notificationclick', (event) => {
const notificationData = event.notification.data;
event.notification.close();
if (event.action === 'viewmedia') {
self.clients.openWindow(notificationData.actionUrl);
} else if (event.action === 'approve') {
fetch(`/api/v1/request/${notificationData.requestId}/approve`, {
method: 'POST',
});
self.clients.openWindow(notificationData.actionUrl);
} else if (event.action === 'decline') {
fetch(`/api/v1/request/${notificationData.requestId}/decline`, {
method: 'POST',
});
self.clients.openWindow(notificationData.actionUrl);
} else if (notificationData.actionUrl) {
self.clients.openWindow(notificationData.actionUrl);
}
}, false);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@ -282,11 +282,7 @@ export class MediaRequest {
media[this.is4k ? 'status4k' : 'status'] !==
MediaStatus.PARTIALLY_AVAILABLE
) {
if (this.is4k) {
media.status4k = MediaStatus.PROCESSING;
} else {
media.status = MediaStatus.PROCESSING;
}
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
mediaRepository.save(media);
}
@ -294,11 +290,7 @@ export class MediaRequest {
media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED
) {
if (this.is4k) {
media.status4k = MediaStatus.UNKNOWN;
} else {
media.status = MediaStatus.UNKNOWN;
}
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
@ -314,9 +306,9 @@ export class MediaRequest {
media.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING
).length === 0 &&
media.status === MediaStatus.PENDING
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
) {
media.status = MediaStatus.UNKNOWN;
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
@ -490,7 +482,7 @@ export class MediaRequest {
await mediaRepository.save(media);
})
.catch(async () => {
media.status = MediaStatus.UNKNOWN;
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added movie request failed to add to Radarr, marking as unknown',
@ -700,7 +692,7 @@ export class MediaRequest {
await mediaRepository.save(media);
})
.catch(async () => {
media.status = MediaStatus.UNKNOWN;
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added series request failed to add to Sonarr, marking as unknown',

@ -29,6 +29,7 @@ import { getSettings } from '../lib/settings';
import logger from '../logger';
import { MediaRequest } from './MediaRequest';
import SeasonRequest from './SeasonRequest';
import { UserPushSubscription } from './UserPushSubscription';
import { UserSettings } from './UserSettings';
@Entity()
@ -105,6 +106,9 @@ export class User {
})
public settings?: UserSettings;
@OneToMany(() => UserPushSubscription, (pushSub) => pushSub.user)
public pushSubscriptions: UserPushSubscription[];
@CreateDateColumn()
public createdAt: Date;

@ -0,0 +1,27 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from './User';
@Entity()
export class UserPushSubscription {
@PrimaryGeneratedColumn()
public id: number;
@ManyToOne(() => User, (user) => user.pushSubscriptions, {
eager: true,
onDelete: 'CASCADE',
})
public user: User;
@Column()
public endpoint: string;
@Column()
public p256dh: string;
@Column({ unique: true })
public auth: string;
constructor(init?: Partial<UserPushSubscription>) {
Object.assign(this, init);
}
}

@ -5,12 +5,15 @@ import {
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import {
hasNotificationAgentEnabled,
NotificationAgentType,
} from '../lib/notifications/agenttypes';
import { NotificationAgentTypes } from '../interfaces/api/userSettingsInterfaces';
import { hasNotificationType, Notification } from '../lib/notifications';
import { NotificationAgentKey } from '../lib/settings';
import { User } from './User';
export const ALL_NOTIFICATIONS = Object.values(Notification)
.filter((v) => !isNaN(Number(v)))
.reduce((a, v) => a + Number(v), 0);
@Entity()
export class UserSettings {
constructor(init?: Partial<UserSettings>) {
@ -24,15 +27,15 @@ export class UserSettings {
@JoinColumn()
public user: User;
@Column({ default: '' })
public locale?: string;
@Column({ nullable: true })
public region?: string;
@Column({ nullable: true })
public originalLanguage?: string;
@Column({ type: 'integer', default: NotificationAgentType.EMAIL })
public notificationAgents = NotificationAgentType.EMAIL;
@Column({ nullable: true })
public pgpKey?: string;
@ -45,7 +48,67 @@ export class UserSettings {
@Column({ nullable: true })
public telegramSendSilently?: boolean;
public hasNotificationAgentEnabled(agent: NotificationAgentType): boolean {
return !!hasNotificationAgentEnabled(agent, this.notificationAgents);
@Column({
type: 'text',
nullable: true,
transformer: {
from: (value: string | null): Partial<NotificationAgentTypes> => {
const defaultTypes = {
email: ALL_NOTIFICATIONS,
discord: 0,
pushbullet: 0,
pushover: 0,
slack: 0,
telegram: 0,
webhook: 0,
webpush: ALL_NOTIFICATIONS,
};
if (!value) {
return defaultTypes;
}
const values = JSON.parse(value) as Partial<NotificationAgentTypes>;
// Something with the migration to this field has caused some issue where
// the value pre-populates with just a raw "2"? Here we check if that's the case
// and return the default notification types if so
if (typeof values !== 'object') {
return defaultTypes;
}
if (values.email == null) {
values.email = ALL_NOTIFICATIONS;
}
if (values.webpush == null) {
values.webpush = ALL_NOTIFICATIONS;
}
return values;
},
to: (value: Partial<NotificationAgentTypes>): string | null => {
if (!value || typeof value !== 'object') {
return null;
}
const allowedKeys = Object.values(NotificationAgentKey);
// Remove any unknown notification agent keys before saving to db
(Object.keys(value) as (keyof NotificationAgentTypes)[]).forEach(
(key) => {
if (!allowedKeys.includes(key)) {
delete value[key];
}
}
);
return JSON.stringify(value);
},
},
})
public notificationTypes: Partial<NotificationAgentTypes>;
public hasNotificationType(key: NotificationAgentKey, type: Notification) {
return hasNotificationType(type, this.notificationTypes[key] ?? 0);
}
}

@ -1,30 +1,32 @@
import express, { Request, Response, NextFunction } from 'express';
import next from 'next';
import path from 'path';
import { createConnection, getRepository } from 'typeorm';
import routes from './routes';
import { getClientIp } from '@supercharge/request-ip';
import bodyParser from 'body-parser';
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 { TypeormStore } from 'connect-typeorm/out';
import YAML from 'yamljs';
import next from 'next';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
import * as OpenApiValidator from 'express-openapi-validator';
import { createConnection, getRepository } from 'typeorm';
import YAML from 'yamljs';
import { Session } from './entity/Session';
import { getSettings } from './lib/settings';
import logger from './logger';
import { startJobs } from './job/schedule';
import notificationManager from './lib/notifications';
import DiscordAgent from './lib/notifications/agents/discord';
import EmailAgent from './lib/notifications/agents/email';
import TelegramAgent from './lib/notifications/agents/telegram';
import { getAppVersion } from './utils/appVersion';
import SlackAgent from './lib/notifications/agents/slack';
import LunaSeaAgent from './lib/notifications/agents/lunasea';
import PushbulletAgent from './lib/notifications/agents/pushbullet';
import PushoverAgent from './lib/notifications/agents/pushover';
import SlackAgent from './lib/notifications/agents/slack';
import TelegramAgent from './lib/notifications/agents/telegram';
import WebhookAgent from './lib/notifications/agents/webhook';
import { getClientIp } from '@supercharge/request-ip';
import PushbulletAgent from './lib/notifications/agents/pushbullet';
import WebPushAgent from './lib/notifications/agents/webpush';
import { getSettings } from './lib/settings';
import logger from './logger';
import routes from './routes';
import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
@ -52,11 +54,13 @@ app
notificationManager.registerAgents([
new DiscordAgent(),
new EmailAgent(),
new LunaSeaAgent(),
new PushbulletAgent(),
new PushoverAgent(),
new SlackAgent(),
new TelegramAgent(),
new WebhookAgent(),
new WebPushAgent(),
]);
// Start Jobs

@ -30,6 +30,9 @@ export interface PublicSettingsResponse {
originalLanguage: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
locale: string;
}
export interface CacheItem {

@ -1,5 +1,8 @@
import { NotificationAgentKey } from '../../lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
locale?: string;
region?: string;
originalLanguage?: string;
movieQuotaLimit?: number;
@ -12,8 +15,8 @@ export interface UserSettingsGeneralResponse {
globalTvQuotaDays?: number;
}
export type NotificationAgentTypes = Record<NotificationAgentKey, number>;
export interface UserSettingsNotificationsResponse {
notificationAgents: number;
emailEnabled?: boolean;
pgpKey?: string;
discordEnabled?: boolean;
@ -22,4 +25,6 @@ export interface UserSettingsNotificationsResponse {
telegramBotUsername?: string;
telegramChatId?: string;
telegramSendSilently?: boolean;
webPushEnabled?: boolean;
notificationTypes: Partial<NotificationAgentTypes>;
}

@ -1,14 +1,20 @@
import Email from 'email-templates';
import nodemailer from 'nodemailer';
import { NotificationAgentEmail } from '../settings';
import { URL } from 'url';
import { getSettings, NotificationAgentEmail } from '../settings';
import { openpgpEncrypt } from './openpgpEncrypt';
class PreparedEmail extends Email {
public constructor(settings: NotificationAgentEmail, pgpKey?: string) {
const { applicationUrl } = getSettings().main;
const transport = nodemailer.createTransport({
name: applicationUrl ? new URL(applicationUrl).hostname : undefined,
host: settings.options.smtpHost,
port: settings.options.smtpPort,
secure: settings.options.secure,
ignoreTLS: settings.options.ignoreTls,
requireTLS: settings.options.requireTls,
tls: settings.options.allowSelfSigned
? {
rejectUnauthorized: false,
@ -22,6 +28,7 @@ class PreparedEmail extends Email {
}
: undefined,
});
if (pgpKey) {
transport.use(
'stream',
@ -32,6 +39,7 @@ class PreparedEmail extends Email {
})
);
}
super({
message: {
from: {

@ -1,6 +1,6 @@
import crypto from 'crypto';
import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream';
import crypto from 'crypto';
interface EncryptorOptions {
signingKey?: string;
@ -54,7 +54,8 @@ class PGPEncryptor extends Transform {
privateKey = await openpgp.readKey({
armoredKey: this._signingKey,
});
await privateKey.decrypt(this._password);
await openpgp.decryptKey({ privateKey, passphrase: this._password });
}
const emailPartDelimiter = '\r\n\r\n';
@ -128,11 +129,12 @@ class PGPEncryptor extends Transform {
.join('\r\n');
const encryptedMessage = await openpgp.encrypt({
message: openpgp.Message.fromText(
contentHeadersRaw +
message: await openpgp.createMessage({
text:
contentHeadersRaw +
emailPartDelimiter +
messageParts.join(emailPartDelimiter)
),
messageParts.join(emailPartDelimiter),
}),
publicKeys: validPublicKeys,
privateKeys: privateKey,
});

@ -4,8 +4,11 @@ import { hasNotificationType, Notification } from '..';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import { getSettings, NotificationAgentDiscord } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentDiscord,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
enum EmbedColors {
@ -227,8 +230,9 @@ class DiscordAgent
if (payload.notifyUser) {
// Mention user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationAgentEnabled(
NotificationAgentType.DISCORD
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
payload.notifyUser.settings?.discordId
) {
@ -243,8 +247,9 @@ class DiscordAgent
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
user.settings?.hasNotificationAgentEnabled(
NotificationAgentType.DISCORD
user.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
user.settings?.discordId
)
@ -267,7 +272,7 @@ class DiscordAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

@ -7,8 +7,11 @@ import { User } from '../../../entity/User';
import logger from '../../../logger';
import PreparedEmail from '../../email';
import { Permission } from '../../permissions';
import { getSettings, NotificationAgentEmail } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentEmail,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class EmailAgent
@ -152,9 +155,13 @@ class EmailAgent
// Send notification to the user who submitted the request
if (
!payload.notifyUser.settings ||
payload.notifyUser.settings.hasNotificationAgentEnabled(
NotificationAgentType.EMAIL
)
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(payload.notifyUser.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true)
) {
logger.debug('Sending email notification', {
label: 'Notifications',
@ -194,9 +201,13 @@ class EmailAgent
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
(!user.settings ||
user.settings.hasNotificationAgentEnabled(
NotificationAgentType.EMAIL
))
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(user.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true))
)
.map(async (user) => {
logger.debug('Sending email notification', {

@ -0,0 +1,104 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaStatus } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentLunaSea } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class LunaSeaAgent
extends BaseAgent<NotificationAgentLunaSea>
implements NotificationAgent {
protected getSettings(): NotificationAgentLunaSea {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.lunasea;
}
private buildPayload(type: Notification, payload: NotificationPayload) {
return {
notification_type: Notification[type],
subject: payload.subject,
message: payload.message,
image: payload.image ?? null,
email: payload.notifyUser?.email,
username: payload.notifyUser?.username,
avatar: payload.notifyUser?.avatar,
media: payload.media
? {
media_type: payload.media.mediaType,
tmdbId: payload.media.tmdbId,
imdbId: payload.media.imdbId,
tvdbId: payload.media.tvdbId,
status: MediaStatus[payload.media.status],
status4k: MediaStatus[payload.media.status4k],
}
: null,
extra: payload.extra ?? [],
request: payload.request
? {
request_id: payload.request.id,
requestedBy_email: payload.request.requestedBy.email,
requestedBy_username: payload.request.requestedBy.displayName,
requestedBy_avatar: payload.request.requestedBy.avatar,
}
: null,
};
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
try {
const { webhookUrl, profileName } = this.getSettings().options;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildPayload(type, payload), {
headers: {
Authorization: `Basic ${Buffer.from(`${profileName}:`).toString(
'base64'
)}`,
},
});
return true;
} catch (e) {
logger.error('Error sending LunaSea notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
});
return false;
}
}
}
export default LunaSeaAgent;

@ -170,7 +170,7 @@ class PushbulletAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

@ -196,7 +196,7 @@ class PushoverAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

@ -254,7 +254,7 @@ class SlackAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

@ -2,8 +2,11 @@ import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentTelegram } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentKey,
NotificationAgentTelegram,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface TelegramMessagePayload {
@ -198,8 +201,9 @@ class TelegramAgent
if (
payload.notifyUser &&
payload.notifyUser.settings?.hasNotificationAgentEnabled(
NotificationAgentType.TELEGRAM
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
payload.notifyUser.settings?.telegramChatId &&
payload.notifyUser.settings?.telegramChatId !==
@ -240,7 +244,7 @@ class TelegramAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

@ -154,7 +154,7 @@ class WebhookAgent
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
response: e.response.data,
response: e.response?.data,
});
return false;

@ -0,0 +1,234 @@
import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentConfig,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushNotificationPayload {
notificationType: string;
mediaType?: 'movie' | 'tv';
tmdbId?: number;
subject: string;
message?: string;
image?: string;
actionUrl?: string;
requestId?: number;
}
class WebPushAgent
extends BaseAgent<NotificationAgentConfig>
implements NotificationAgent {
protected getSettings(): NotificationAgentConfig {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.webpush;
}
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): PushNotificationPayload {
switch (type) {
case Notification.TEST_NOTIFICATION:
return {
notificationType: Notification[type],
subject: payload.subject,
message: payload.message,
};
case Notification.MEDIA_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request has been approved.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AUTO_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Automatically approved a new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AVAILABLE:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request is now available!`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_DECLINED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request was declined.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_FAILED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Failed to process ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_PENDING:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Approval required for new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
}
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending web push notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
const userRepository = getRepository(User);
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
let pushSubs: UserPushSubscription[] = [];
const mainUser = await userRepository.findOne({ where: { id: 1 } });
if (
payload.notifyUser &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true)
) {
const notifySubs = await userPushSubRepository.find({
where: { user: payload.notifyUser.id },
});
pushSubs = notifySubs;
} else if (!payload.notifyUser) {
const users = await userRepository.find();
const manageUsers = users.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(user.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true)
);
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id),
})
.getMany();
pushSubs = allSubs;
}
if (mainUser && pushSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
Promise.all(
pushSubs.map(async (sub) => {
try {
await webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
)
);
} catch (e) {
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
})
);
}
return true;
}
}
export default WebPushAgent;

@ -1,16 +0,0 @@
export enum NotificationAgentType {
NONE = 0,
EMAIL = 2,
DISCORD = 4,
TELEGRAM = 8,
PUSHOVER = 16,
PUSHBULLET = 32,
SLACK = 64,
}
export const hasNotificationAgentEnabled = (
agent: NotificationAgentType,
value: number
): boolean => {
return !!(value & agent);
};

@ -17,6 +17,8 @@ export enum Permission {
AUTO_APPROVE_4K = 32768,
AUTO_APPROVE_4K_MOVIE = 65536,
AUTO_APPROVE_4K_TV = 131072,
REQUEST_MOVIE = 262144,
REQUEST_TV = 524288,
}
export interface PermissionCheckOptions {

@ -72,6 +72,17 @@ class RadarrScanner
}
private async processRadarrMovie(radarrMovie: RadarrMovie): Promise<void> {
if (!radarrMovie.monitored && !radarrMovie.downloaded) {
this.log(
'Title is unmonitored and has not been downloaded. Skipping item.',
'debug',
{
title: radarrMovie.title,
}
);
return;
}
try {
const server4k = this.enable4kMovie && this.currentServer.is4k;
await this.processMovie(radarrMovie.tmdbId, {

@ -2,6 +2,7 @@ import fs from 'fs';
import { merge } from 'lodash';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import webpush from 'web-push';
import { Permission } from './permissions';
export interface Library {
@ -13,6 +14,7 @@ export interface Library {
export interface Region {
iso_3166_1: string;
english_name: string;
name?: string;
}
export interface Language {
@ -81,10 +83,12 @@ export interface MainSettings {
};
hideAvailable: boolean;
localLogin: boolean;
newPlexLogin: boolean;
region: string;
originalLanguage: string;
trustProxy: boolean;
partialRequestsEnabled: boolean;
locale: string;
}
interface PublicSettings {
@ -101,6 +105,9 @@ interface FullPublicSettings extends PublicSettings {
originalLanguage: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
locale: string;
}
export interface NotificationAgentConfig {
@ -128,6 +135,8 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
smtpHost: string;
smtpPort: number;
secure: boolean;
ignoreTls: boolean;
requireTls: boolean;
authUser?: string;
authPass?: string;
allowSelfSigned: boolean;
@ -137,6 +146,13 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
};
}
export interface NotificationAgentLunaSea extends NotificationAgentConfig {
options: {
webhookUrl: string;
profileName: string;
};
}
export interface NotificationAgentTelegram extends NotificationAgentConfig {
options: {
botUsername?: string;
@ -168,14 +184,27 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
};
}
export enum NotificationAgentKey {
DISCORD = 'discord',
EMAIL = 'email',
PUSHBULLET = 'pushbullet',
PUSHOVER = 'pushover',
SLACK = 'slack',
TELEGRAM = 'telegram',
WEBHOOK = 'webhook',
WEBPUSH = 'webpush',
}
interface NotificationAgents {
discord: NotificationAgentDiscord;
email: NotificationAgentEmail;
lunasea: NotificationAgentLunaSea;
pushbullet: NotificationAgentPushbullet;
pushover: NotificationAgentPushover;
slack: NotificationAgentSlack;
telegram: NotificationAgentTelegram;
webhook: NotificationAgentWebhook;
webpush: NotificationAgentConfig;
}
interface NotificationSettings {
@ -184,6 +213,8 @@ interface NotificationSettings {
interface AllSettings {
clientId: string;
vapidPublic: string;
vapidPrivate: string;
main: MainSettings;
plex: PlexSettings;
radarr: RadarrSettings[];
@ -202,6 +233,8 @@ class Settings {
constructor(initialSettings?: AllSettings) {
this.data = {
clientId: uuidv4(),
vapidPrivate: '',
vapidPublic: '',
main: {
apiKey: '',
applicationTitle: 'Overseerr',
@ -215,14 +248,16 @@ class Settings {
},
hideAvailable: false,
localLogin: true,
newPlexLogin: true,
region: '',
originalLanguage: '',
trustProxy: false,
partialRequestsEnabled: true,
locale: 'en',
},
plex: {
name: '',
ip: '127.0.0.1',
ip: '',
port: 32400,
useSsl: false,
libraries: [],
@ -239,9 +274,11 @@ class Settings {
types: 0,
options: {
emailFrom: '',
smtpHost: '127.0.0.1',
smtpHost: '',
smtpPort: 587,
secure: false,
ignoreTls: false,
requireTls: false,
allowSelfSigned: false,
senderName: 'Overseerr',
},
@ -255,6 +292,14 @@ class Settings {
webhookUrl: '',
},
},
lunasea: {
enabled: false,
types: 0,
options: {
webhookUrl: '',
profileName: '',
},
},
slack: {
enabled: false,
types: 0,
@ -298,6 +343,11 @@ class Settings {
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
},
},
webpush: {
enabled: false,
types: 0,
options: {},
},
},
},
};
@ -366,6 +416,9 @@ class Settings {
originalLanguage: this.data.main.originalLanguage,
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
cacheImages: this.data.main.cacheImages,
vapidPublic: this.vapidPublic,
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
locale: this.data.main.locale,
};
}
@ -386,6 +439,18 @@ class Settings {
return this.data.clientId;
}
get vapidPublic(): string {
this.generateVapidKeys();
return this.data.vapidPublic;
}
get vapidPrivate(): string {
this.generateVapidKeys();
return this.data.vapidPrivate;
}
public regenerateApiKey(): MainSettings {
this.main.apiKey = this.generateApiKey();
this.save();
@ -396,6 +461,15 @@ class Settings {
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
}
private generateVapidKeys(force = false): void {
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
const vapidKeys = webpush.generateVAPIDKeys();
this.data.vapidPrivate = vapidKeys.privateKey;
this.data.vapidPublic = vapidKeys.publicKey;
this.save();
}
}
/**
* Settings Load
*

@ -5,6 +5,7 @@ import { getSettings } from '../lib/settings';
export const checkUser: Middleware = async (req, _res, next) => {
const settings = getSettings();
if (req.header('X-API-Key') === settings.main.apiKey) {
const userRepository = getRepository(User);
@ -28,8 +29,12 @@ export const checkUser: Middleware = async (req, _res, next) => {
if (user) {
req.user = user;
req.locale = user.settings?.locale
? user.settings?.locale
: settings.main.locale;
}
}
next();
};

@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserPushSubscriptions1618912653565
implements MigrationInterface {
name = 'CreateUserPushSubscriptions1618912653565';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"))`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"))`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
}
}

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsLocale1619239659754 implements MigrationInterface {
name = 'AddUserSettingsLocale1619239659754';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsNotificationTypes1619339817343
implements MigrationInterface {
name = 'AddUserSettingsNotificationTypes1619339817343';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

@ -1,12 +1,12 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import PlexTvAPI from '../api/plextv';
import { isAuthenticated } from '../middleware/auth';
import { UserType } from '../constants/user';
import { User } from '../entity/User';
import { Permission } from '../lib/permissions';
import logger from '../logger';
import { getSettings } from '../lib/settings';
import { UserType } from '../constants/user';
import logger from '../logger';
import { isAuthenticated } from '../middleware/auth';
const authRoutes = Router();
@ -79,6 +79,24 @@ authRoutes.post('/plex', async (req, res, next) => {
// Double check that we didn't create the first admin user before running this
if (!user) {
if (!settings.main.newPlexLogin) {
logger.info(
'Failed sign-in attempt from user who has not been imported to Overseerr.',
{
label: 'Auth',
account: {
...account,
authentication_token: '__REDACTED__',
authToken: '__REDACTED__',
},
}
);
return next({
status: 403,
message: 'Access denied.',
});
}
// If we get to this point, the user does not already exist so we need to create the
// user _assuming_ they have access to the Plex server
const mainUser = await userRepository.findOneOrFail({
@ -112,7 +130,7 @@ authRoutes.post('/plex', async (req, res, next) => {
);
return next({
status: 403,
message: 'You do not have access to this Plex server.',
message: 'Access denied.',
});
}
}
@ -128,7 +146,7 @@ authRoutes.post('/plex', async (req, res, next) => {
logger.error(e.message, { label: 'Auth' });
return next({
status: 500,
message: 'Something went wrong. Is your auth token valid?',
message: 'Something went wrong.',
});
}
});

@ -11,7 +11,7 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

@ -1,16 +1,16 @@
import { Router } from 'express';
import { sortBy } from 'lodash';
import TheMovieDb from '../api/themoviedb';
import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
import Media from '../entity/Media';
import { isMovie, isPerson } from '../utils/typeHelpers';
import { MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import Media from '../entity/Media';
import { User } from '../entity/User';
import { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { mapProductionCompany } from '../models/Movie';
import { mapMovieResult, mapPersonResult, mapTvResult } from '../models/Search';
import { mapNetwork } from '../models/Tv';
import logger from '../logger';
import { sortBy } from 'lodash';
import { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
import { isMovie, isPerson } from '../utils/typeHelpers';
const createTmdbWithRegionLanaguage = (user?: User): TheMovieDb => {
const settings = getSettings();
@ -42,7 +42,7 @@ discoverRoutes.get('/movies', async (req, res) => {
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
studio: req.query.studio ? Number(req.query.studio) : undefined,
});
@ -83,7 +83,7 @@ discoverRoutes.get<{ language: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
@ -115,7 +115,7 @@ discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const genre = genres.find(
@ -128,7 +128,7 @@ discoverRoutes.get<{ genreId: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
@ -164,7 +164,7 @@ discoverRoutes.get<{ studioId: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
studio: Number(req.params.studioId),
});
@ -204,7 +204,7 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
primaryReleaseDateGte: date,
});
@ -232,7 +232,7 @@ discoverRoutes.get('/tv', async (req, res) => {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
network: req.query.network ? Number(req.query.network) : undefined,
});
@ -273,7 +273,7 @@ discoverRoutes.get<{ language: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
@ -304,7 +304,7 @@ discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const genre = genres.find(
@ -317,7 +317,7 @@ discoverRoutes.get<{ genreId: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
@ -352,7 +352,7 @@ discoverRoutes.get<{ networkId: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
network: Number(req.params.networkId),
});
@ -392,7 +392,7 @@ discoverRoutes.get('/tv/upcoming', async (req, res) => {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
firstAirDateGte: date,
});
@ -420,7 +420,7 @@ discoverRoutes.get('/trending', async (req, res) => {
const data = await tmdb.getAllTrending({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@ -461,7 +461,7 @@ discoverRoutes.get<{ keywordId: string }>(
const data = await tmdb.getMoviesByKeyword({
keywordId: Number(req.params.keywordId),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@ -494,7 +494,7 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
const mappedGenres: GenreSliderItem[] = [];
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
await Promise.all(
@ -535,7 +535,7 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
const mappedGenres: GenreSliderItem[] = [];
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
await Promise.all(

@ -81,10 +81,16 @@ router.get('/status/appdata', (_req, res) => {
});
router.use('/user', isAuthenticated(), user);
router.get('/settings/public', async (_req, res) => {
router.get('/settings/public', async (req, res) => {
const settings = getSettings();
return res.status(200).json(settings.fullPublicSettings);
if (!req.user?.settings?.notificationTypes.webpush) {
return res
.status(200)
.json({ ...settings.fullPublicSettings, enablePushRegistration: false });
} else {
return res.status(200).json(settings.fullPublicSettings);
}
});
router.use(
'/settings',
@ -138,7 +144,7 @@ router.get('/genres/movie', isAuthenticated(), async (req, res) => {
const tmdb = new TheMovieDb();
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);
@ -148,7 +154,7 @@ router.get('/genres/tv', isAuthenticated(), async (req, res) => {
const tmdb = new TheMovieDb();
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);

@ -1,11 +1,11 @@
import { Router } from 'express';
import RottenTomatoes from '../api/rottentomatoes';
import TheMovieDb from '../api/themoviedb';
import { mapMovieDetails } from '../models/Movie';
import { mapMovieResult } from '../models/Search';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import { MediaType } from '../constants/media';
import { mapMovieDetails } from '../models/Movie';
import { mapMovieResult } from '../models/Search';
const movieRoutes = Router();
@ -15,7 +15,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
try {
const tmdbMovie = await tmdb.getMovie({
movieId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
@ -36,7 +36,7 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
const results = await tmdb.getMovieRecommendations({
movieId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@ -64,7 +64,7 @@ movieRoutes.get('/:id/similar', async (req, res) => {
const results = await tmdb.getMovieSimilar({
movieId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

@ -16,7 +16,7 @@ personRoutes.get('/:id', async (req, res, next) => {
try {
const person = await tmdb.getPerson({
personId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapPersonDetails(person));
} catch (e) {
@ -30,7 +30,7 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
const combinedCredits = await tmdb.getPersonCombinedCredits({
personId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const castMedia = await Media.getRelatedMedia(

@ -139,287 +139,289 @@ requestRoutes.get('/', async (req, res, next) => {
}
});
requestRoutes.post(
'/',
isAuthenticated(Permission.REQUEST),
async (req, res, next) => {
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
requestRoutes.post('/', async (req, res, next) => {
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const userRepository = getRepository(User);
try {
let requestUser = req.user;
try {
let requestUser = req.user;
if (
req.body.userId &&
!req.user?.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
return next({
status: 403,
message: 'You do not have permission to modify the request user.',
});
} else if (req.body.userId) {
requestUser = await userRepository.findOneOrFail({
where: { id: req.body.userId },
});
}
if (
req.body.userId &&
!req.user?.hasPermission([
Permission.MANAGE_USERS,
Permission.MANAGE_REQUESTS,
])
) {
return next({
status: 403,
message: 'You do not have permission to modify the request user.',
});
} else if (req.body.userId) {
requestUser = await userRepository.findOneOrFail({
where: { id: req.body.userId },
});
}
if (!requestUser) {
return next({
status: 500,
message: 'User missing from request context.',
});
}
if (!requestUser) {
return next({
status: 500,
message: 'User missing from request context.',
});
}
if (req.body.is4k) {
if (
req.body.mediaType === MediaType.MOVIE &&
!req.user?.hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: 'You do not have permission to make 4K movie requests.',
});
} else if (
req.body.mediaType === MediaType.TV &&
!req.user?.hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_TV],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: 'You do not have permission to make 4K series requests.',
});
if (
req.body.mediaType === MediaType.MOVIE &&
!req.user?.hasPermission(
req.body.is4k
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
{
type: 'or',
}
}
)
) {
return next({
status: 403,
message: `You do not have permission to make ${
req.body.is4k ? '4K ' : ''
}movie requests.`,
});
} else if (
req.body.mediaType === MediaType.TV &&
!req.user?.hasPermission(
req.body.is4k
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
: [Permission.REQUEST, Permission.REQUEST_TV],
{
type: 'or',
}
)
) {
return next({
status: 403,
message: `You do not have permission to make ${
req.body.is4k ? '4K ' : ''
}series requests.`,
});
}
const quotas = await requestUser.getQuota();
const quotas = await requestUser.getQuota();
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
return next({
status: 403,
message: 'Movie Quota Exceeded',
});
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
return next({
status: 403,
message: 'Series Quota Exceeded',
});
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
return next({
status: 403,
message: 'Movie Quota Exceeded',
});
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
return next({
status: 403,
message: 'Series Quota Exceeded',
});
}
const tmdbMedia =
req.body.mediaType === MediaType.MOVIE
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
relations: ['requests'],
});
if (!media) {
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
mediaType: req.body.mediaType,
});
} else {
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
media.status = MediaStatus.PENDING;
}
const tmdbMedia =
req.body.mediaType === MediaType.MOVIE
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
media.status4k = MediaStatus.PENDING;
}
}
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
relations: ['requests'],
if (req.body.mediaType === MediaType.MOVIE) {
const existing = await requestRepository.findOne({
where: {
media: {
tmdbId: tmdbMedia.id,
},
requestedBy: req.user,
is4k: req.body.is4k,
},
});
if (!media) {
media = new Media({
if (existing) {
logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id,
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
mediaType: req.body.mediaType,
});
} else {
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
media.status = MediaStatus.PENDING;
}
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
media.status4k = MediaStatus.PENDING;
}
}
if (req.body.mediaType === MediaType.MOVIE) {
const existing = await requestRepository.findOne({
where: {
media: {
tmdbId: tmdbMedia.id,
},
requestedBy: req.user,
is4k: req.body.is4k,
},
return next({
status: 409,
message: 'Request for this media already exists.',
});
}
if (existing) {
logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id,
mediaType: req.body.mediaType,
});
return next({
status: 409,
message: 'Request for this media already exists.',
});
}
await mediaRepository.save(media);
const request = new MediaRequest({
type: MediaType.MOVIE,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
tags: req.body.tags,
});
await mediaRepository.save(media);
const request = new MediaRequest({
type: MediaType.MOVIE,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_MOVIE
: Permission.AUTO_APPROVE_MOVIE,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
await requestRepository.save(request);
return res.status(201).json(request);
} else if (req.body.mediaType === MediaType.TV) {
const requestedSeasons = req.body.seasons as number[];
let existingSeasons: number[] = [];
// We need to check existing requests on this title to make sure we don't double up on seasons that were
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
// (Unless there are no seasons, in which case we abort)
if (media.requests) {
existingSeasons = media.requests
.filter(
(request) =>
request.is4k === req.body.is4k &&
request.status !== MediaRequestStatus.DECLINED
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
tags: req.body.tags,
});
await requestRepository.save(request);
return res.status(201).json(request);
} else if (req.body.mediaType === MediaType.TV) {
const requestedSeasons = req.body.seasons as number[];
let existingSeasons: number[] = [];
// We need to check existing requests on this title to make sure we don't double up on seasons that were
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
// (Unless there are no seasons, in which case we abort)
if (media.requests) {
existingSeasons = media.requests
.filter(
(request) =>
request.is4k === req.body.is4k &&
request.status !== MediaRequestStatus.DECLINED
)
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
const finalSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
if (finalSeasons.length === 0) {
return next({
status: 202,
message: 'No seasons available to request',
});
}
const finalSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
await mediaRepository.save(media);
const request = new MediaRequest({
type: MediaType.TV,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
languageProfileId: req.body.languageProfileId,
tags: req.body.tags,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
seasonNumber: sn,
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
})
),
if (finalSeasons.length === 0) {
return next({
status: 202,
message: 'No seasons available to request',
});
await requestRepository.save(request);
return res.status(201).json(request);
}
next({ status: 500, message: 'Invalid media type' });
} catch (e) {
next({ status: 500, message: e.message });
await mediaRepository.save(media);
const request = new MediaRequest({
type: MediaType.TV,
media,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
modifiedBy: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
languageProfileId: req.body.languageProfileId,
tags: req.body.tags,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
seasonNumber: sn,
status: req.user?.hasPermission(
[
req.body.is4k
? Permission.AUTO_APPROVE_4K
: Permission.AUTO_APPROVE,
req.body.is4k
? Permission.AUTO_APPROVE_4K_TV
: Permission.AUTO_APPROVE_TV,
Permission.MANAGE_REQUESTS,
],
{ type: 'or' }
)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
})
),
});
await requestRepository.save(request);
return res.status(201).json(request);
}
next({ status: 500, message: 'Invalid media type' });
} catch (e) {
next({ status: 500, message: e.message });
}
);
});
requestRoutes.get('/count', async (_req, res, next) => {
const requestRepository = getRepository(MediaRequest);

@ -1,7 +1,7 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapSearchResults } from '../models/Search';
import Media from '../entity/Media';
import { mapSearchResults } from '../models/Search';
const searchRoutes = Router();
@ -11,7 +11,7 @@ searchRoutes.get('/', async (req, res) => {
const results = await tmdb.searchMulti({
query: req.query.query as string,
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

@ -191,7 +191,7 @@ serviceRoutes.get<{ tmdbId: string }>(
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.tmdbId),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const response = await sonarr.getSeriesByTitle(tv.name);

@ -2,11 +2,13 @@ import { Router } from 'express';
import { Notification } from '../../lib/notifications';
import DiscordAgent from '../../lib/notifications/agents/discord';
import EmailAgent from '../../lib/notifications/agents/email';
import LunaSeaAgent from '../../lib/notifications/agents/lunasea';
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
import PushoverAgent from '../../lib/notifications/agents/pushover';
import SlackAgent from '../../lib/notifications/agents/slack';
import TelegramAgent from '../../lib/notifications/agents/telegram';
import WebhookAgent from '../../lib/notifications/agents/webhook';
import WebPushAgent from '../../lib/notifications/agents/webpush';
import { getSettings } from '../../lib/settings';
const notificationRoutes = Router();
@ -26,23 +28,30 @@ notificationRoutes.post('/discord', (req, res) => {
res.status(200).json(settings.notifications.agents.discord);
});
notificationRoutes.post('/discord/test', (req, res, next) => {
notificationRoutes.post('/discord/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const discordAgent = new DiscordAgent(req.body);
discordAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await discordAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Discord notification.',
});
}
});
notificationRoutes.get('/slack', (_req, res) => {
@ -60,23 +69,30 @@ notificationRoutes.post('/slack', (req, res) => {
res.status(200).json(settings.notifications.agents.slack);
});
notificationRoutes.post('/slack/test', (req, res, next) => {
notificationRoutes.post('/slack/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const slackAgent = new SlackAgent(req.body);
slackAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await slackAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Slack notification.',
});
}
});
notificationRoutes.get('/telegram', (_req, res) => {
@ -94,23 +110,30 @@ notificationRoutes.post('/telegram', (req, res) => {
res.status(200).json(settings.notifications.agents.telegram);
});
notificationRoutes.post('/telegram/test', (req, res, next) => {
notificationRoutes.post('/telegram/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const telegramAgent = new TelegramAgent(req.body);
telegramAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await telegramAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Telegram notification.',
});
}
});
notificationRoutes.get('/pushbullet', (_req, res) => {
@ -128,23 +151,30 @@ notificationRoutes.post('/pushbullet', (req, res) => {
res.status(200).json(settings.notifications.agents.pushbullet);
});
notificationRoutes.post('/pushbullet/test', (req, res, next) => {
notificationRoutes.post('/pushbullet/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const pushbulletAgent = new PushbulletAgent(req.body);
pushbulletAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await pushbulletAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Pushbullet notification.',
});
}
});
notificationRoutes.get('/pushover', (_req, res) => {
@ -162,23 +192,30 @@ notificationRoutes.post('/pushover', (req, res) => {
res.status(200).json(settings.notifications.agents.pushover);
});
notificationRoutes.post('/pushover/test', (req, res, next) => {
notificationRoutes.post('/pushover/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const pushoverAgent = new PushoverAgent(req.body);
pushoverAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await pushoverAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send Pushover notification.',
});
}
});
notificationRoutes.get('/email', (_req, res) => {
@ -196,23 +233,71 @@ notificationRoutes.post('/email', (req, res) => {
res.status(200).json(settings.notifications.agents.email);
});
notificationRoutes.post('/email/test', (req, res, next) => {
notificationRoutes.post('/email/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
const emailAgent = new EmailAgent(req.body);
emailAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
if (
await emailAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send email notification.',
});
}
});
notificationRoutes.get('/webpush', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush', (req, res) => {
const settings = getSettings();
settings.notifications.agents.webpush = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
});
}
const webpushAgent = new WebPushAgent(req.body);
if (
await webpushAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send web push notification.',
});
}
});
notificationRoutes.get('/webhook', (_req, res) => {
@ -260,11 +345,11 @@ notificationRoutes.post('/webhook', (req, res, next) => {
}
});
notificationRoutes.post('/webhook/test', (req, res, next) => {
notificationRoutes.post('/webhook/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
message: 'User information is missing from the request.',
});
}
@ -284,16 +369,64 @@ notificationRoutes.post('/webhook/test', (req, res, next) => {
};
const webhookAgent = new WebhookAgent(testBody);
webhookAgent.send(Notification.TEST_NOTIFICATION, {
if (
await webhookAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
})
) {
return res.status(204).send();
} else {
return next({
status: 500,
message: 'Failed to send webhook notification.',
});
}
} catch (e) {
next({ status: 500, message: e.message });
}
});
notificationRoutes.get('/lunasea', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.lunasea);
});
notificationRoutes.post('/lunasea', (req, res) => {
const settings = getSettings();
settings.notifications.agents.lunasea = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.lunasea);
});
notificationRoutes.post('/lunasea/test', async (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
});
}
const lunaseaAgent = new LunaSeaAgent(req.body);
if (
await lunaseaAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
})
) {
return res.status(204).send();
} catch (e) {
next({ status: 500, message: e.message });
} else {
return next({
status: 500,
message: 'Failed to send web push notification.',
});
}
});

@ -1,11 +1,11 @@
import { Router } from 'express';
import RottenTomatoes from '../api/rottentomatoes';
import TheMovieDb from '../api/themoviedb';
import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
import { mapTvResult } from '../models/Search';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import { MediaType } from '../constants/media';
import { mapTvResult } from '../models/Search';
import { mapSeasonWithEpisodes, mapTvDetails } from '../models/Tv';
const tvRoutes = Router();
@ -14,7 +14,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getMedia(tv.id, MediaType.TV);
@ -35,7 +35,7 @@ tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => {
const season = await tmdb.getTvSeason({
tvId: Number(req.params.id),
seasonNumber: Number(req.params.seasonNumber),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapSeasonWithEpisodes(season));
@ -47,7 +47,7 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
const results = await tmdb.getTvRecommendations({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@ -75,7 +75,7 @@ tvRoutes.get('/:id/similar', async (req, res) => {
const results = await tmdb.getTvSimilar({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

@ -5,6 +5,7 @@ import PlexTvAPI from '../../api/plextv';
import { UserType } from '../../constants/user';
import { MediaRequest } from '../../entity/MediaRequest';
import { User } from '../../entity/User';
import { UserPushSubscription } from '../../entity/UserPushSubscription';
import {
QuotaResponse,
UserRequestsResponse,
@ -127,6 +128,48 @@ router.post(
}
);
router.post<
never,
unknown,
{
endpoint: string;
p256dh: string;
auth: string;
}
>('/registerPushSubscription', async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const existingSubs = await userPushSubRepository.find({
where: { auth: req.body.auth },
});
if (existingSubs.length > 0) {
logger.debug(
'User push subscription already exists. Skipping registration.',
{ label: 'API' }
);
return res.status(204).send();
}
const userPushSubscription = new UserPushSubscription({
auth: req.body.auth,
endpoint: req.body.endpoint,
p256dh: req.body.p256dh,
user: req.user,
});
userPushSubRepository.save(userPushSubscription);
return res.status(204).send();
} catch (e) {
logger.error('Failed to register user push subscription', {
label: 'API',
});
next({ status: 500, message: 'Failed to register subscription.' });
}
});
router.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const userRepository = getRepository(User);

@ -7,7 +7,6 @@ import {
UserSettingsGeneralResponse,
UserSettingsNotificationsResponse,
} from '../../interfaces/api/userSettingsInterfaces';
import { NotificationAgentType } from '../../lib/notifications/agenttypes';
import { Permission } from '../../lib/permissions';
import { getSettings } from '../../lib/settings';
import logger from '../../logger';
@ -52,6 +51,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
return res.status(200).json({
username: user.username,
locale: user.settings?.locale,
region: user.settings?.region,
originalLanguage: user.settings?.originalLanguage,
movieQuotaLimit: user.movieQuotaLimit,
@ -109,17 +109,24 @@ userSettingsRoutes.post<
if (!user.settings) {
user.settings = new UserSettings({
user: req.user,
locale: req.body.locale,
region: req.body.region,
originalLanguage: req.body.originalLanguage,
});
} else {
user.settings.locale = req.body.locale;
user.settings.region = req.body.region;
user.settings.originalLanguage = req.body.originalLanguage;
}
await userRepository.save(user);
return res.status(200).json({ username: user.username });
return res.status(200).json({
username: user.username,
region: user.settings.region,
locale: user.settings.locale,
originalLanguage: user.settings.originalLanguage,
});
} catch (e) {
next({ status: 500, message: e.message });
}
@ -243,8 +250,6 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
}
return res.status(200).json({
notificationAgents:
user.settings?.notificationAgents ?? NotificationAgentType.EMAIL,
emailEnabled: settings?.notifications.agents.email.enabled,
pgpKey: user.settings?.pgpKey,
discordEnabled: settings?.notifications.agents.discord.enabled,
@ -254,6 +259,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
settings?.notifications.agents.telegram.options.botUsername,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
webPushEnabled: settings?.notifications.agents.webpush.enabled,
notificationTypes: user.settings?.notificationTypes ?? {},
});
} catch (e) {
next({ status: 500, message: e.message });
@ -287,30 +294,32 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
if (!user.settings) {
user.settings = new UserSettings({
user: req.user,
notificationAgents:
req.body.notificationAgents ?? NotificationAgentType.EMAIL,
pgpKey: req.body.pgpKey,
discordId: req.body.discordId,
telegramChatId: req.body.telegramChatId,
telegramSendSilently: req.body.telegramSendSilently,
notificationTypes: req.body.notificationTypes,
});
} else {
user.settings.notificationAgents =
req.body.notificationAgents ?? NotificationAgentType.EMAIL;
user.settings.pgpKey = req.body.pgpKey;
user.settings.discordId = req.body.discordId;
user.settings.telegramChatId = req.body.telegramChatId;
user.settings.telegramSendSilently = req.body.telegramSendSilently;
user.settings.notificationTypes = Object.assign(
{},
user.settings.notificationTypes,
req.body.notificationTypes
);
}
userRepository.save(user);
return res.status(200).json({
notificationAgents: user.settings?.notificationAgents,
pgpKey: user.settings?.pgpKey,
discordId: user.settings?.discordId,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
notificationTypes: user.settings.notificationTypes,
});
} catch (e) {
next({ status: 500, message: e.message });

@ -6,6 +6,7 @@ declare global {
namespace Express {
export interface Request {
user?: User;
locale?: string;
}
}

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-icon="discord" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="currentColor" d="M297.216 243.2c0 15.616-11.52 28.416-26.112 28.416-14.336 0-26.112-12.8-26.112-28.416s11.52-28.416 26.112-28.416c14.592 0 26.112 12.8 26.112 28.416zm-119.552-28.416c-14.592 0-26.112 12.8-26.112 28.416s11.776 28.416 26.112 28.416c14.592 0 26.112-12.8 26.112-28.416.256-15.616-11.52-28.416-26.112-28.416zM448 52.736V512c-64.494-56.994-43.868-38.128-118.784-107.776l13.568 47.36H52.48C23.552 451.584 0 428.032 0 398.848V52.736C0 23.552 23.552 0 52.48 0h343.04C424.448 0 448 23.552 448 52.736zm-72.96 242.688c0-82.432-36.864-149.248-36.864-149.248-36.864-27.648-71.936-26.88-71.936-26.88l-3.584 4.096c43.52 13.312 63.744 32.512 63.744 32.512-60.811-33.329-132.244-33.335-191.232-7.424-9.472 4.352-15.104 7.424-15.104 7.424s21.248-20.224 67.328-33.536l-2.56-3.072s-35.072-.768-71.936 26.88c0 0-36.864 66.816-36.864 149.248 0 0 21.504 37.12 78.08 38.912 0 0 9.472-11.52 17.152-21.248-32.512-9.728-44.8-30.208-44.8-30.208 3.766 2.636 9.976 6.053 10.496 6.4 43.21 24.198 104.588 32.126 159.744 8.96 8.96-3.328 18.944-8.192 29.44-15.104 0 0-12.8 20.992-46.336 30.464 7.68 9.728 16.896 20.736 16.896 20.736 56.576-1.792 78.336-38.912 78.336-38.912z"/></svg>
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path d="m81.9 83.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1 0.1-6.1-4.5-11.1-10.2-11.1zm36.5 0c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z" fill="currentColor"/><path d="m167 0h-134c-11.3 0-20.5 9.2-20.5 20.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19v-179.4c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-0.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-0.9-4.8-2-7.3-3.4-0.3-0.2-0.6-0.3-0.9-0.5-0.2-0.1-0.3-0.2-0.4-0.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-0.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3 0.6-0.1 1.1-0.2 1.7-0.2 6.1-0.8 13-1 20.2-0.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-0.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1 @@
<svg viewBox="0 0 750 750" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="m554.69 180.46c-333.63 0-452.75 389.23-556.05 389.23 185.37 0 237.85-247.18 419.12-247.18l47.24-102.05z"/><path d="m749.31 375.08c0 107.48-87.14 194.61-194.62 194.61s-194.62-87.13-194.62-194.61 87.13-194.62 194.62-194.62c7.391-2e-3 14.776 0.412 22.12 1.24-78.731 10.172-136.59 78.893-133.2 158.2 3.393 79.313 66.907 142.84 146.22 146.25 79.311 3.411 148.05-54.43 158.24-133.16 0.826 7.331 1.24 14.703 1.24 22.08z"/></g></svg>

After

Width:  |  Height:  |  Size: 519 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 256 256"><metadata/><path fill="#fff" d="m128 0c-70.692 0-128 57.308-128 128s57.308 128 128 128 128-57.308 128-128-57.308-128-128-128zm-51 64.268h3.334c16.733 0 16.732-5.78e-4 16.732 16.732v91.867c-1e-6 16.733 5.77e-4 16.732-16.732 16.732h-3.334c-16.733 0-16.732 5.8e-4 -16.732-16.732v-91.867c0-16.733-5.78e-4 -16.732 16.732-16.732zm44.041 0h37.537c32.178 0 52.627 32.273 52.627 63.025 0 30.752-20.627 62.307-52.627 62.307h-37.537c-5.699 0-8.5078-2.8088-8.5078-8.5078v-108.32c0-5.698 2.8088-8.5059 8.5078-8.5059z" mask="url(#mask-2)"/></svg>
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path d="m128 0c-70.692 0-128 57.308-128 128s57.308 128 128 128 128-57.308 128-128-57.308-128-128-128zm-51 64.268h3.334c16.733 0 16.732-5.78e-4 16.732 16.732v91.867c-1e-6 16.733 5.77e-4 16.732-16.732 16.732h-3.334c-16.733 0-16.732 5.8e-4 -16.732-16.732v-91.867c0-16.733-5.78e-4 -16.732 16.732-16.732zm44.041 0h37.537c32.178 0 52.627 32.273 52.627 63.025s-20.627 62.307-52.627 62.307h-37.537c-5.699 0-8.5078-2.8088-8.5078-8.5078v-108.32c0-5.698 2.8088-8.5059 8.5078-8.5059z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 563 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="57 57 602 602"><g id="layer1" fill="#1F2937" fill-rule="evenodd" stroke="none" stroke-width="1" opacity=".91" transform="translate(58.964119, 58.887520)"><ellipse style="fill:#fff;fill-rule:evenodd;stroke:#fff;stroke-width:0" cx="216.308" cy="152.076" rx="296.855" ry="296.855" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)"/><path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill-rule:nonzero;white-space:pre"/></g></svg>
<svg viewBox="57 57 593.71002 593.71002" xmlns="http://www.w3.org/2000/svg"><g transform="translate(55.201 55.147)"><circle transform="rotate(132.42 225.16 243.54)" cx="216.31" cy="152.08" r="296.86" fill="currentColor"/><path d="m280.95 172.51 74.48-9.8-72.52 163.66c12.74-0.98 25.233-5.307 37.48-12.98 12.253-7.68 23.527-17.317 33.82-28.91 10.287-11.6 19.187-24.503 26.7-38.71 7.513-14.213 12.903-28.18 16.17-41.9 1.96-8.493 2.86-16.66 2.7-24.5-0.167-7.84-2.21-14.7-6.13-20.58s-9.883-10.617-17.89-14.21c-8-3.593-18.86-5.39-32.58-5.39-16.007 0-31.77 2.613-47.29 7.84-15.513 5.227-29.887 12.823-43.12 22.79-13.227 9.96-24.74 22.373-34.54 37.24-9.8 14.86-16.823 31.763-21.07 50.71-1.633 6.207-2.613 11.187-2.94 14.94-0.327 3.76-0.407 6.863-0.24 9.31 0.16 2.453 0.483 4.333 0.97 5.64 0.493 1.307 0.903 2.613 1.23 3.92-16.66 0-28.83-3.35-36.51-10.05-7.673-6.693-9.55-18.37-5.63-35.03 3.92-17.313 12.823-33.81 26.71-49.49 13.88-15.68 30.373-29.483 49.48-41.41 19.113-11.92 40.02-21.39 62.72-28.41 22.707-7.027 44.84-10.54 66.4-10.54 18.947 0 34.87 2.693 47.77 8.08 12.907 5.393 22.953 12.5 30.14 21.32s11.677 19.11 13.47 30.87c1.8 11.76 1.23 24.01-1.71 36.75-3.593 15.353-10.373 30.79-20.34 46.31-9.96 15.513-22.453 29.56-37.48 42.14-15.027 12.573-32.26 22.78-51.7 30.62-19.433 7.84-40.093 11.76-61.98 11.76h-2.45l-62.23 139.65h-70.56z"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" data-icon="slack" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="currentColor" d="M94.12 315.1c0 25.9-21.16 47.06-47.06 47.06S0 341 0 315.1c0-25.9 21.16-47.06 47.06-47.06h47.06v47.06zm23.72 0c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06v117.84c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06V315.1zm47.06-188.98c-25.9 0-47.06-21.16-47.06-47.06S139 32 164.9 32s47.06 21.16 47.06 47.06v47.06H164.9zm0 23.72c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06H47.06C21.16 243.96 0 222.8 0 196.9s21.16-47.06 47.06-47.06H164.9zm188.98 47.06c0-25.9 21.16-47.06 47.06-47.06 25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06h-47.06V196.9zm-23.72 0c0 25.9-21.16 47.06-47.06 47.06-25.9 0-47.06-21.16-47.06-47.06V79.06c0-25.9 21.16-47.06 47.06-47.06 25.9 0 47.06 21.16 47.06 47.06V196.9zM283.1 385.88c25.9 0 47.06 21.16 47.06 47.06 0 25.9-21.16 47.06-47.06 47.06-25.9 0-47.06-21.16-47.06-47.06v-47.06h47.06zm0-23.72c-25.9 0-47.06-21.16-47.06-47.06 0-25.9 21.16-47.06 47.06-47.06h117.84c25.9 0 47.06 21.16 47.06 47.06 0 25.9-21.16 47.06-47.06 47.06H283.1z"/></svg>
<svg viewBox="0 0 448 448" xmlns="http://www.w3.org/2000/svg"><path d="m94.12 283.1c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06 21.16-47.06 47.06-47.06h47.06zm23.72 0c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06v117.84c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06zm47.06-188.98c-25.9 0-47.06-21.16-47.06-47.06s21.16-47.06 47.06-47.06 47.06 21.16 47.06 47.06v47.06zm0 23.72c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06h-117.84c-25.9 0-47.06-21.16-47.06-47.06s21.16-47.06 47.06-47.06zm188.98 47.06c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06-21.16 47.06-47.06 47.06h-47.06zm-23.72 0c0 25.9-21.16 47.06-47.06 47.06s-47.06-21.16-47.06-47.06v-117.84c0-25.9 21.16-47.06 47.06-47.06s47.06 21.16 47.06 47.06zm-47.06 188.98c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06-47.06-21.16-47.06-47.06v-47.06zm0-23.72c-25.9 0-47.06-21.16-47.06-47.06s21.16-47.06 47.06-47.06h117.84c25.9 0 47.06 21.16 47.06 47.06s-21.16 47.06-47.06 47.06z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1014 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="currentColor" fill-rule="evenodd" d="M0 24C0 37.2548 10.7452 48 24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24ZM19.6 35L20.0083 28.8823L20.008 28.882L31.1369 18.839C31.6253 18.4055 31.0303 18.1941 30.3819 18.5873L16.6473 27.2523L10.7147 25.4007C9.4335 25.0084 9.4243 24.128 11.0023 23.4951L34.1203 14.5809C35.1762 14.1015 36.1953 14.8345 35.7922 16.4505L31.8552 35.0031C31.5803 36.3215 30.7837 36.6368 29.68 36.0278L23.6827 31.5969L20.8 34.4C20.7909 34.4088 20.7819 34.4176 20.7729 34.4264C20.4505 34.7403 20.1837 35 19.6 35Z" clip-rule="evenodd"/></svg>
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="m0 24c0 13.255 10.745 24 24 24s24-10.745 24-24-10.745-24-24-24-24 10.745-24 24zm19.6 11l0.4083-6.1177-3e-4 -3e-4 11.129-10.043c0.4884-0.4335-0.1066-0.6449-0.755-0.2517l-13.735 8.665-5.9326-1.8516c-1.2812-0.3923-1.2904-1.2727 0.2876-1.9056l23.118-8.9142c1.0559-0.4794 2.075 0.2536 1.6719 1.8696l-3.937 18.553c-0.2749 1.3184-1.0715 1.6337-2.1752 1.0247l-5.9973-4.4309-2.8827 2.8031c-0.0091 0.0088-0.0181 0.0176-0.0271 0.0264-0.3224 0.3139-0.5892 0.5736-1.1729 0.5736z" clip-rule="evenodd" fill="currentColor" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 603 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"><g fill="none"><path fill="#FFF" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></g></svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -1,8 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560">
<g fill="none" transform="translate(33 140)">
<path fill="#FFF" d="M43.8020066,267.3152 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.3152 Z M266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 C26.3585902,221.21181 24.03,207.991848 22.1723803,189.066133 L266.684852,192.017143 Z M275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.325333 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 L275.03101,89.3083429 Z M302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 Z"/>
<path fill="#00641E" d="M303.565869,264.667352 C306.720846,256.846476 317.903331,252.081752 325.93259,252.63901 C334.515108,253.234819 343.631626,262.264838 345.224872,271.145905 C345.520761,270.823467 345.830656,270.518552 346.145803,270.218895 C348.901593,267.583314 352.35421,265.834438 356.132479,265.352533 C355.554708,262.790552 355.416393,260.048076 355.812079,257.233752 C357.207482,247.3328 365.145698,239.907962 374.234203,239.981562 C380.099449,240.028876 385.245108,243.032457 388.597928,247.646476 C388.897318,247.271467 389.228223,246.928 389.550374,246.577524 C393.384669,226.586362 395.814807,203.999924 396.368066,180.030857 C398.267705,97.6111238 377.375174,30.2776381 349.70522,29.6397714 C322.033515,29.0001524 298.061292,95.2962286 296.161652,177.715962 C296.161652,177.715962 294.696216,207.778057 303.565869,264.667352"/>
<path fill="#FFD700" d="M490.910577,354.797562 C492.545843,352.0656 493.45977,348.7904 493.396741,345.310171 C493.927239,334.065143 486.214879,323.871543 475.484105,325.000076 C475.794,323.713829 475.997095,322.376762 476.075882,320.997638 C476.718433,309.733333 468.957049,300.025143 458.739266,299.315429 C458.515161,299.30141 458.294557,299.2944 458.072203,299.28739 C459.134951,296.492343 459.661948,293.371352 459.47461,290.06461 C458.945862,280.692876 452.488839,272.796648 444.088407,271.242286 C441.054236,270.681524 438.111108,270.960152 435.416597,271.894171 C432.870905,265.617143 427.525652,260.961067 421.023108,259.977981 C420.508367,249.786133 413.153174,241.397486 403.68299,240.740343 C397.728452,240.326781 392.257141,243.064 388.597928,247.646476 C385.245108,243.032457 380.099449,240.030629 374.234203,239.983314 C365.145698,239.907962 357.207482,247.3328 355.812079,257.235505 C355.416393,260.048076 355.554708,262.790552 356.132479,265.354286 C352.35421,265.834438 348.901593,267.585067 346.145803,270.218895 C345.830656,270.518552 345.520761,270.823467 345.224872,271.145905 C343.631626,262.264838 334.515108,253.236571 325.93259,252.63901 C317.903331,252.081752 306.575528,256.963886 303.565869,264.667352 C304.885987,278.101105 313.275915,314.72061 343.621121,347.570743 L343.890748,347.590019 C346.816367,350.239619 350.636656,351.699352 354.709062,351.33661 C357.233744,351.110552 359.558833,350.206324 361.56177,348.811429 L362.050249,348.844724 C364.720249,350.700495 367.929502,351.671314 371.320839,351.369905 C372.616446,351.254248 373.852525,350.942324 375.027325,350.497219 C377.933685,356.51139 384.41522,360.398171 391.649607,359.760305 C397.25223,359.266133 402.014459,356.164419 404.81402,351.799238 L405.720944,351.862324 C408.517003,354.636343 412.233993,356.252038 416.24337,356.131124 C419.557672,361.149943 425.645272,364.237638 432.363167,363.647086 C434.893102,363.424533 437.254957,362.692038 439.361193,361.582781 C442.875089,365.90941 448.642289,368.481905 454.969751,367.924648 C461.22543,367.376152 466.509403,363.904686 469.384249,359.104914 C472.208321,361.374248 475.746728,362.592152 479.503987,362.257448 C483.149193,361.931505 486.38821,360.208914 488.821849,357.603124 L489.228039,357.631162 C489.781298,356.828571 490.25402,356.006705 490.693475,355.177829 C490.70398,355.162057 490.712734,355.144533 490.721489,355.128762 C490.781016,355.018362 490.854551,354.909714 490.910577,354.797562"/>
<path fill="#04A53C" d="M281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 Z M302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.316952 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 Z M33.8766098,243.950705 C26.3585902,221.21181 24.03,207.9936 22.1723803,189.066133 L266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 Z M33.8766098,108.391771 L275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.327086 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 Z M378.597246,25.7126857 C363.342354,7.93478095 352.390977,-0.411809524 343.010085,0.672914286 C341.261016,0.895466667 76.1168852,37.8952381 76.1168852,37.8952381 C34.0429377,42.1780571 0.416695082,103.32739 0,176.172114 C0.416695082,249.015086 34.0429377,310.164419 76.1168852,314.44899 C76.1168852,314.44899 341.758249,351.583695 343.010085,351.669562 C345.228374,351.655543 347.418649,351.357638 349.57741,350.803886 C347.476426,350.178286 345.538269,349.083048 343.890748,347.590019 L343.621121,347.570743 C313.275915,314.722362 304.885987,278.101105 303.565869,264.667352 C303.56937,264.656838 303.576374,264.648076 303.579875,264.637562 C303.576374,264.648076 303.56937,264.656838 303.565869,264.667352 C294.696216,207.778057 296.161652,177.715962 296.161652,177.715962 C298.061292,95.2962286 322.033515,29.0001524 349.70522,29.638019 C377.375174,30.2776381 398.267705,97.6111238 396.368066,180.030857 C395.814807,203.999924 393.384669,226.586362 389.550374,246.577524 C393.489718,242.226362 398.773692,240.393371 403.68299,240.740343 C404.586413,240.805181 405.465325,240.957638 406.326728,241.155657 C423.131095,149.125867 405.514348,59.262019 378.597246,25.7126857 Z"/>
</g>
</svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><g transform="translate(33 140)"><path d="m43.802 267.32l237.94 23.482c4.7937 14.937 11.149 29.517 20.259 40.256l-230.27-30.125c-10.248-6.7519-20.861-19.877-27.936-33.612zm222.88-75.298c0.60053 20.906 3.4316 47.964 8.3462 71.017l-241.15-19.083c-7.518-22.739-9.8466-35.959-11.704-54.885l244.51 2.951zm8.3462-102.71c-4.9146 23.053-7.7456 50.111-8.3462 71.017l-244.51 2.951c1.8576-18.926 4.1862-32.146 11.704-54.885l241.15-19.083zm26.973-68.019c-9.1095 10.74-15.465 25.318-20.259 40.257l-237.94 23.48c7.0751-13.735 17.689-26.859 27.936-33.612l230.27-30.125z" fill="#fff"/><path d="m303.57 264.67c3.155-7.8209 14.337-12.586 22.367-12.028 8.5825 0.59581 17.699 9.6258 19.292 18.507 0.29589-0.32244 0.60578-0.62735 0.92093-0.92701 2.7558-2.6356 6.2084-4.3845 9.9867-4.8664-0.57777-2.562-0.71609-5.3045-0.3204-8.1188 1.3954-9.901 9.3336-17.326 18.422-17.252 5.8652 0.047314 11.011 3.0509 14.364 7.6649 0.29939-0.37501 0.6303-0.71848 0.95245-1.069 3.8343-19.991 6.2644-42.578 6.8177-66.547 1.8996-82.42-18.993-149.75-46.663-150.39-27.672-0.63962-51.644 65.656-53.544 148.08 0 0-1.4654 30.062 7.4042 86.951" fill="#00641E"/><path d="m490.91 354.8c1.6353-2.732 2.5492-6.0072 2.4862-9.4874 0.5305-11.245-7.1819-21.439-17.913-20.31 0.3099-1.2862 0.51299-2.6233 0.59178-4.0024 0.64255-11.264-7.1188-20.972-17.337-21.682-0.2241-0.014019-0.44471-0.021029-0.66706-0.028039 1.0627-2.795 1.5897-5.916 1.4024-9.2228-0.52875-9.3717-6.9858-17.268-15.386-18.822-3.0342-0.56076-5.9773-0.28213-8.6718 0.65188-2.5457-6.277-7.8909-10.933-14.393-11.916-0.51474-10.192-7.8699-18.58-17.34-19.238-5.9545-0.41356-11.426 2.3237-15.085 6.9061-3.3528-4.614-8.4985-7.6158-14.364-7.6632-9.0885-0.075352-17.027 7.3495-18.422 17.252-0.39569 2.8126-0.25737 5.555 0.3204 8.1188-3.7783 0.48015-7.2309 2.2308-9.9867 4.8646-0.31515 0.29966-0.62504 0.60457-0.92093 0.92701-1.5932-8.8811-10.71-17.909-19.292-18.507-8.0293-0.55726-19.357 4.3249-22.367 12.028 1.3201 13.434 9.71 50.053 40.055 82.903l0.26963 0.019276c2.9256 2.6496 6.7459 4.1093 10.818 3.7466 2.5247-0.22606 4.8498-1.1303 6.8527-2.5252l0.48848 0.033295c2.67 1.8558 5.8793 2.8266 9.2706 2.5252 1.2956-0.11566 2.5317-0.42758 3.7065-0.87269 2.9064 6.0142 9.3879 9.901 16.622 9.2631 5.6026-0.49417 10.365-3.5959 13.164-7.9611l0.90692 0.063086c2.7961 2.774 6.513 4.3897 10.522 4.2688 3.3143 5.0188 9.4019 8.1065 16.12 7.516 2.5299-0.22255 4.8918-0.95505 6.998-2.0643 3.5139 4.3266 9.2811 6.8991 15.609 6.3419 6.2557-0.5485 11.54-4.02 14.414-8.8197 2.8241 2.2693 6.3625 3.4872 10.12 3.1525 3.6452-0.32594 6.8842-2.0485 9.3179-4.6543l0.40619 0.028038c0.55326-0.80259 1.026-1.6245 1.4654-2.4533 0.010505-0.015772 0.019259-0.033296 0.028014-0.049067 0.059527-0.1104 0.13306-0.21905 0.18909-0.3312" fill="#FFD700"/><path d="m281.75 61.547l-237.94 23.48c7.0751-13.735 17.689-26.859 27.936-33.612l230.27-30.125c-9.1095 10.74-15.465 25.318-20.259 40.257zm20.259 269.51l-230.27-30.125c-10.248-6.7519-20.861-19.877-27.936-33.611l237.94 23.48c4.7937 14.937 11.149 29.517 20.259 40.256zm-268.13-87.102c-7.518-22.739-9.8466-35.957-11.704-54.885l244.51 2.951c0.60053 20.906 3.4316 47.964 8.3462 71.017l-241.15-19.083zm0-135.56l241.15-19.083c-4.9146 23.053-7.7456 50.111-8.3462 71.019l-244.51 2.9493c1.8576-18.926 4.1862-32.146 11.704-54.885zm344.72-82.679c-15.255-17.778-26.206-26.124-35.587-25.04-1.7491 0.22255-266.89 37.222-266.89 37.222-42.074 4.2828-75.7 65.432-76.117 138.28 0.4167 72.843 34.043 133.99 76.117 138.28 0 0 265.64 37.135 266.89 37.221 2.2183-0.014019 4.4086-0.31192 6.5673-0.86568-2.101-0.6256-4.0391-1.7208-5.6867-3.2139l-0.26963-0.019276c-30.345-32.848-38.735-69.47-40.055-82.903 0.003501-0.010514 0.010505-0.019276 0.014006-0.02979-0.003501 0.010514-0.010505 0.019276-0.014006 0.02979-8.8697-56.889-7.4042-86.951-7.4042-86.951 1.8996-82.42 25.872-148.72 53.544-148.08 27.67 0.63962 48.562 67.973 46.663 150.39-0.55326 23.969-2.9834 46.556-6.8177 66.547 3.9393-4.3512 9.2233-6.1842 14.133-5.8372 0.90342 0.064838 1.7823 0.2173 2.6437 0.41531 16.804-92.03-0.81238-181.89-27.729-215.44z" fill="#04A53C"/></g></svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"><g fill="none"><path fill="#FA320A" d="M478.29 296.976c-3.99-63.966-36.52-111.823-85.468-138.579.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.265 6.78 108.735 108.63 170.89 211.193 164.49 102.556-6.395 193.466-80.572 186.683-189.307"/><path fill="#00912D" d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023"/></g></svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path d="m478.29 296.98c-3.99-63.966-36.52-111.82-85.468-138.58 0.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.28 7.584 0.285 8.519-1.378 50.072-59.914 52.483-1.382 0.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.26 6.78 108.74 108.63 170.89 211.19 164.49 102.56-6.395 193.47-80.572 186.68-189.31" fill="#FA320A"/><path d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023" fill="#00912D"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"><path fill="#0AC855" d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878"/></svg>
<svg viewBox="0 0 560 560" xmlns="http://www.w3.org/2000/svg"><path d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878" fill="#0AC855"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" version="1.1" viewBox="0 0 575 289.83"><defs><path id="d1pwhf9wy2" d="M575 24.91C573.44 12.15 563.97 1.98 551.91 0C499.05 0 76.18 0 23.32 0C10.11 2.17 0 14.16 0 28.61C0 51.84 0 237.64 0 260.86C0 276.86 12.37 289.83 27.64 289.83C79.63 289.83 495.6 289.83 547.59 289.83C561.65 289.83 573.26 278.82 575 264.57C575 216.64 575 48.87 575 24.91Z"/><path id="g5jjnq26yS" d="M69.35 58.24L114.98 58.24L114.98 233.89L69.35 233.89L69.35 58.24Z"/><path id="i3Prh1JpXt" d="M201.2 139.15C197.28 112.38 195.1 97.5 194.67 94.53C192.76 80.2 190.94 67.73 189.2 57.09C185.25 57.09 165.54 57.09 130.04 57.09L130.04 232.74L170.01 232.74L170.15 116.76L186.97 232.74L215.44 232.74L231.39 114.18L231.54 232.74L271.38 232.74L271.38 57.09L211.77 57.09L201.2 139.15Z"/><path id="a4ov9rRGQm" d="M346.71 93.63C347.21 95.87 347.47 100.95 347.47 108.89C347.47 115.7 347.47 170.18 347.47 176.99C347.47 188.68 346.71 195.84 345.2 198.48C343.68 201.12 339.64 202.43 333.09 202.43C333.09 190.9 333.09 98.66 333.09 87.13C338.06 87.13 341.45 87.66 343.25 88.7C345.05 89.75 346.21 91.39 346.71 93.63ZM367.32 230.95C372.75 229.76 377.31 227.66 381.01 224.67C384.7 221.67 387.29 217.52 388.77 212.21C390.26 206.91 391.14 196.38 391.14 180.63C391.14 174.47 391.14 125.12 391.14 118.95C391.14 102.33 390.49 91.19 389.48 85.53C388.46 79.86 385.93 74.71 381.88 70.09C377.82 65.47 371.9 62.15 364.12 60.13C356.33 58.11 343.63 57.09 321.54 57.09C319.27 57.09 307.93 57.09 287.5 57.09L287.5 232.74L342.78 232.74C355.52 232.34 363.7 231.75 367.32 230.95Z"/><path id="fk968BpsX" d="M464.76 204.7C463.92 206.93 460.24 208.06 457.46 208.06C454.74 208.06 452.93 206.98 452.01 204.81C451.09 202.65 450.64 197.72 450.64 190C450.64 185.36 450.64 148.22 450.64 143.58C450.64 135.58 451.04 130.59 451.85 128.6C452.65 126.63 454.41 125.63 457.13 125.63C459.91 125.63 463.64 126.76 464.6 129.03C465.55 131.3 466.03 136.15 466.03 143.58C466.03 146.58 466.03 161.58 466.03 188.59C465.74 197.84 465.32 203.21 464.76 204.7ZM406.68 231.21L447.76 231.21C449.47 224.5 450.41 220.77 450.6 220.02C454.32 224.52 458.41 227.9 462.9 230.14C467.37 232.39 474.06 233.51 479.24 233.51C486.45 233.51 492.67 231.62 497.92 227.83C503.16 224.05 506.5 219.57 507.92 214.42C509.34 209.26 510.05 201.42 510.05 190.88C510.05 185.95 510.05 146.53 510.05 141.6C510.05 131 509.81 124.08 509.34 120.83C508.87 117.58 507.47 114.27 505.14 110.88C502.81 107.49 499.42 104.86 494.98 102.98C490.54 101.1 485.3 100.16 479.26 100.16C474.01 100.16 467.29 101.21 462.81 103.28C458.34 105.35 454.28 108.49 450.64 112.7C450.64 108.89 450.64 89.85 450.64 55.56L406.68 55.56L406.68 231.21Z"/></defs><g><g><g><use fill="#f6c700" fill-opacity="1" opacity="1" xlink:href="#d1pwhf9wy2"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#d1pwhf9wy2"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#g5jjnq26yS"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#g5jjnq26yS"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#i3Prh1JpXt"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#i3Prh1JpXt"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#a4ov9rRGQm"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#a4ov9rRGQm"/></g></g><g><use fill="#000" fill-opacity="1" opacity="1" xlink:href="#fk968BpsX"/><g><use fill-opacity="0" stroke="#000" stroke-opacity="0" stroke-width="1" opacity="1" xlink:href="#fk968BpsX"/></g></g></g></g></svg>
<svg version="1.1" viewBox="0 0 575 289.83" xmlns="http://www.w3.org/2000/svg"><path fill="#f6c700" d="m575 24.91c-1.56-12.76-11.03-22.93-23.09-24.91h-528.59c-13.21 2.17-23.32 14.16-23.32 28.61v232.25c0 16 12.37 28.97 27.64 28.97h519.95c14.06 0 25.67-11.01 27.41-25.26v-239.66z"/><path stroke="#000" d="m69.35 58.24h45.63v175.65h-45.63v-175.65z"/><path stroke="#000" d="m201.2 139.15c-3.92-26.77-6.1-41.65-6.53-44.62-1.91-14.33-3.73-26.8-5.47-37.44h-59.16v175.65h39.97l0.14-115.98 16.82 115.98h28.47l15.95-118.56 0.15 118.56h39.84v-175.65h-59.61l-10.57 82.06z"/><path stroke="#000" d="m346.71 93.63c0.5 2.24 0.76 7.32 0.76 15.26v68.1c0 11.69-0.76 18.85-2.27 21.49-1.52 2.64-5.56 3.95-12.11 3.95v-115.3c4.97 0 8.36 0.53 10.16 1.57 1.8 1.05 2.96 2.69 3.46 4.93zm20.61 137.32c5.43-1.19 9.99-3.29 13.69-6.28 3.69-3 6.28-7.15 7.76-12.46 1.49-5.3 2.37-15.83 2.37-31.58v-61.68c0-16.62-0.65-27.76-1.66-33.42-1.02-5.67-3.55-10.82-7.6-15.44-4.06-4.62-9.98-7.94-17.76-9.96-7.79-2.02-20.49-3.04-42.58-3.04h-34.04v175.65h55.28c12.74-0.4 20.92-0.99 24.54-1.79z"/><path stroke="#000" d="m464.76 204.7c-0.84 2.23-4.52 3.36-7.3 3.36-2.72 0-4.53-1.08-5.45-3.25-0.92-2.16-1.37-7.09-1.37-14.81v-46.42c0-8 0.4-12.99 1.21-14.98 0.8-1.97 2.56-2.97 5.28-2.97 2.78 0 6.51 1.13 7.47 3.4 0.95 2.27 1.43 7.12 1.43 14.55v45.01c-0.29 9.25-0.71 14.62-1.27 16.11zm-58.08 26.51h41.08c1.71-6.71 2.65-10.44 2.84-11.19 3.72 4.5 7.81 7.88 12.3 10.12 4.47 2.25 11.16 3.37 16.34 3.37 7.21 0 13.43-1.89 18.68-5.68 5.24-3.78 8.58-8.26 10-13.41 1.42-5.16 2.13-13 2.13-23.54v-49.28c0-10.6-0.24-17.52-0.71-20.77s-1.87-6.56-4.2-9.95-5.72-6.02-10.16-7.9-9.68-2.82-15.72-2.82c-5.25 0-11.97 1.05-16.45 3.12-4.47 2.07-8.53 5.21-12.17 9.42v-57.14h-43.96v175.65z"/></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" data-name="Layer 1" viewBox="0 0 320.03 103.61"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#radial-gradient)}.cls-3{fill:#e5a00d}</style><radialGradient id="radial-gradient" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#f9be03"/><stop offset=".51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><title>plex-logo</title><polygon id="X" points="320.03 -.09 289.96 -.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79 320.03 -.09" class="cls-1"/><g id="chevron"><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -.09" class="cls-2"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -.09" class="cls-3"/></g><path id="E" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z" class="cls-1"/><path id="L" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z" class="cls-1"/><path id="P" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z" class="cls-1"/></svg>
<svg viewBox="0 0 320.03 103.61" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#a)}.cls-3{fill:#e5a00d}</style><radialGradient id="a" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop stop-color="#f9be03" offset=".17"/><stop stop-color="#e8a50b" offset=".51"/><stop stop-color="#cc7c19" offset="1"/></radialGradient></defs><polygon class="cls-1" points="320.03 -0.09 289.96 -0.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79"/><polygon class="cls-2" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76"/><polygon class="cls-3" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76"/><path class="cls-1" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z"/><path class="cls-1" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z"/><path class="cls-1" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1 @@
<svg version="1.1" viewBox="0 0 1000 1115.2" xmlns="http://www.w3.org/2000/svg"><path transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)" d="m105.76 154.15-1.263 714.59c-60.261 6.782-105.02-23.858-104.28-84.025l-0.216-594.25c2.312-188.02 175.85-231.02 280.22-154.52l530.2 314.93c74.563 53.572 88.403 151.53 49.965 218.76-6.873-52.739-29.067-83.101-73.823-113.74l-597.52-345.84c-44.756-30.639-82.453-23.58-83.286 44.109zm-54.377 751.54c44.941 15.597 90.16 8.63 128.04-13.47l621.16-353.43c36.958 53.109 28.79 105.66-16.706 135.19l-522.65 294.46c-75.673 36.68-172.98-2.127-209.85-62.757z" fill="#fff"/><path transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)" d="m240.52 702.59 365.02-216.68-364.35-197.07z" fill="#ffc230"/></svg>

After

Width:  |  Height:  |  Size: 737 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -0,0 +1 @@
<svg viewBox="0 0 216.7 216.9" xmlns="http://www.w3.org/2000/svg"><path d="M216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95C53.05 10.517 78.617 0 108.45 0c29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" clip-rule="evenodd" fill="#EEE" fill-rule="evenodd"/><path d="M194.65 42.5l-22.4 22.4C159.152 77.998 158 89.4 158 109.5c0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05L61.1 43.85C74.102 56.852 90.767 60.2 108.7 60.2c18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" clip-rule="evenodd" fill="#3A3F51" fill-rule="evenodd"/><g clip-rule="evenodd"><path d="M78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3 0 8.267-2.916 15.35-8.75 21.25-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z" fill="#0CF" fill-rule="evenodd"/><path d="M157.8 59.75l-15 14.65M30.785 32.526L71.65 73.25m84.6 84.25l27.808 28.78m1.855-153.894L157.8 59.75m-125.45 126l27.35-27.4" fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="2"/><path d="M157.8 59.75l-16.95 17.2M58.97 60.604l17.2 17.15M59.623 158.43l16.75-17.4m61.928-1.396l18.028 17.945" fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="7"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><style>.cls-1{fill:url(#linear-gradient)}</style><linearGradient id="linear-gradient" x2="185.04" y1="66.7" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z" class="cls-1"/></g></g></svg>
<svg viewBox="0 0 185.04 133.4" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:url(#a)}</style><linearGradient id="a" x2="185.04" y1="66.7" y2="66.7" gradientUnits="userSpaceOnUse"><stop stop-color="#90cea1" offset="0"/><stop stop-color="#3cbec9" offset=".56"/><stop stop-color="#00b3e5" offset="1"/></linearGradient></defs><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 49.994 27.765"><metadata/><g stroke-width=".265" transform="translate(-80.836 -134.28)"><ellipse cx="104.39" cy="140.67" fill="#fff" rx="3.04" ry="2.572"/><path fill="#1b7d3d" d="m85.973 162.05c-0.2783-9e-3 -0.76695-0.12188-1.0859-0.25035-0.31894-0.12847-0.75831-0.50788-0.97638-0.84312-0.27168-0.41768-0.81789-4.2912-1.7352-12.306-0.7363-6.4328-1.3387-11.951-1.3387-12.263 0-0.31189 0.36978-0.9125 0.82173-1.3347 0.55888-0.52208 1.0976-0.76762 1.6843-0.76762 0.4744 0 5.7899 0.65473 11.812 1.4549 6.0223 0.80022 11.395 1.5787 11.94 1.73 0.67007 0.18601 1.1196 0.52432 1.3891 1.0455 0.23816 0.46053 0.39842 1.4812 0.39842 2.5375 0 1.6585-0.0587 1.832-0.95642 2.8256-0.52604 0.58226-1.2554 1.7118-1.6209 2.51-0.56065 1.2246-0.6448 1.7259-0.5387 3.2089 0.0692 0.96664 0.33525 2.1733 0.59131 2.6815 0.25605 0.50816 0.9453 1.4214 1.5317 2.0295 1.0049 1.0421 1.0581 1.1768 0.92626 2.3462-0.0925 0.82058-0.35123 1.4496-0.76415 1.8577-0.57063 0.56398-1.5248 0.74328-11.099 2.0855-5.7608 0.80767-10.702 1.461-10.98 1.4517zm5.8777-7.1419c0.64371 9e-3 1.2219-0.14144 1.4082-0.36594 0.17385-0.20948 0.27773-0.71409 0.23083-1.1214-0.07317-0.63544-0.23231-0.76418-1.122-0.90763-0.5702-0.0919-1.1992-0.38942-1.3979-0.66108-0.25954-0.35495-0.34007-1.2022-0.2862-3.011l0.07497-2.517h3.7011l1.7217 4.2333 1.7217 4.2333 1.4787 0.0781c0.97936 0.0517 1.5672-0.0376 1.7409-0.26458 0.14423-0.18847 1.1888-2.5136 2.3212-5.1671 1.1324-2.6534 1.9897-5.0049 1.905-5.2255-0.0964-0.25118-0.49511-0.40113-1.0666-0.40113-0.50198 0-1.0918 0.0687-1.3108 0.15275-0.21893 0.084-1.0155 1.6914-1.7701 3.5719-0.75462 1.8805-1.4697 3.4191-1.589 3.4191-0.1193 0-0.45209-0.62507-0.73953-1.3891-0.28745-0.76398-0.8376-2.2224-1.2226-3.2408s-0.81858-1.971-0.9636-2.1167-1.5382-0.32442-3.0959-0.39718l-2.8323-0.13229-0.13229-1.8521-0.13229-1.8521h-2.6458l-0.13229 1.8365c-0.13098 1.8183-0.14096 1.8388-1.0075 2.062-0.81451 0.20989-0.86954 0.29343-0.79375 1.205 0.07128 0.85741 0.18035 0.99633 0.87518 1.1147l0.79375 0.13524 0.13229 3.2208c0.11196 2.726 0.2174 3.3355 0.68624 3.967 0.30467 0.4104 0.95952 0.88829 1.4552 1.062s1.3927 0.32252 1.9933 0.33073zm12.666-11.891c0.45112 0 0.96309-0.14287 1.1377-0.3175 0.17463-0.17462 0.3175-0.64237 0.3175-1.0394 0-0.39707-0.1871-0.90903-0.41577-1.1377-0.22868-0.22868-0.69642-0.41577-1.0394-0.41577-0.34301 0-0.81075 0.18709-1.0394 0.41577s-0.41577 0.74064-0.41577 1.1377c0 0.39706 0.14287 0.86481 0.3175 1.0394 0.17462 0.17463 0.68659 0.3175 1.1377 0.3175z" transform="translate(-8.3333e-8)"/><path fill="#fff" d="m114.46 154.52c-3.0274 0.0544-3.7379-0.0118-4.6609-0.43422-0.59897-0.27412-1.3995-0.90545-1.779-1.403-0.37946-0.49751-0.83257-1.5225-1.0069-2.2778-0.24624-1.0668-0.24624-1.6796 0-2.7464 0.17434-0.75526 0.61766-1.7674 0.98516-2.2492 0.3675-0.48183 1.049-1.0738 1.5144-1.3155 0.48523-0.25197 1.8056-0.5058 3.0952-0.59503l2.249-0.15561 0.13229-1.8521 0.1323-1.8521h2.9104v14.817zm-0.92604-2.3736 1.3229-0.0718v-5.8208l-1.6062-0.0775c-1.4422-0.0696-1.6785-5e-3 -2.3151 0.6314-0.38991 0.38991-0.72779 1.0201-0.75085 1.4004-0.023 0.38034-0.0238 0.92965-2e-3 1.2207 0.0222 0.29104 0.10377 0.80075 0.18131 1.1327 0.0775 0.33193 0.52474 0.8405 0.99377 1.1302 0.615 0.37978 1.2217 0.50661 2.1757 0.45481zm9.7151 2.4359c-2.5936 4e-3 -3.5477-0.0819-3.6425-0.32887-0.0706-0.18407-0.0967-3.5476-0.0578-7.4745l0.0706-7.1398h2.9104l0.13229 1.8521 0.13229 1.8521 2.2687 0.15541c1.6342 0.11195 2.5591 0.31887 3.3073 0.73989 0.57124 0.32147 1.3303 1.0254 1.6868 1.5644 0.45872 0.69347 0.67992 1.4556 0.75677 2.6074 0.0597 0.89511-0.0338 2.059-0.20789 2.5864-0.17406 0.52742-0.62874 1.33-1.0104 1.7836-0.38165 0.45358-1.1751 1.0432-1.7632 1.3104-0.86286 0.39194-1.7476 0.48683-4.5833 0.49152zm0.84267-2.3754c0.64994 0 1.4871-0.1161 1.8603-0.25799 0.3732-0.14189 0.92337-0.64791 1.2226-1.1245 0.41645-0.66331 0.5175-1.173 0.43097-2.1738-0.0877-1.0148-0.27505-1.4467-0.83705-1.9301-0.61948-0.53286-0.95861-0.6115-2.3491-0.54474l-1.6251 0.078-0.0763 2.627c-0.042 1.4448-0.0159 2.7843 0.0578 2.9766 0.0835 0.21749 0.5806 0.34956 1.3158 0.34956z" transform="translate(-8.3333e-8)"/></g></svg>
<svg version="1.1" viewBox="0 0 49.994 27.765" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-80.836 -134.28)" stroke-width=".265"><ellipse cx="104.39" cy="140.67" rx="3.04" ry="2.572" fill="#fff"/><path transform="translate(-8.3333e-8)" d="m85.973 162.05c-0.2783-9e-3 -0.76695-0.12188-1.0859-0.25035-0.31894-0.12847-0.75831-0.50788-0.97638-0.84312-0.27168-0.41768-0.81789-4.2912-1.7352-12.306-0.7363-6.4328-1.3387-11.951-1.3387-12.263 0-0.31189 0.36978-0.9125 0.82173-1.3347 0.55888-0.52208 1.0976-0.76762 1.6843-0.76762 0.4744 0 5.7899 0.65473 11.812 1.4549 6.0223 0.80022 11.395 1.5787 11.94 1.73 0.67007 0.18601 1.1196 0.52432 1.3891 1.0455 0.23816 0.46053 0.39842 1.4812 0.39842 2.5375 0 1.6585-0.0587 1.832-0.95642 2.8256-0.52604 0.58226-1.2554 1.7118-1.6209 2.51-0.56065 1.2246-0.6448 1.7259-0.5387 3.2089 0.0692 0.96664 0.33525 2.1733 0.59131 2.6815 0.25605 0.50816 0.9453 1.4214 1.5317 2.0295 1.0049 1.0421 1.0581 1.1768 0.92626 2.3462-0.0925 0.82058-0.35123 1.4496-0.76415 1.8577-0.57063 0.56398-1.5248 0.74328-11.099 2.0855-5.7608 0.80767-10.702 1.461-10.98 1.4517zm5.8777-7.1419c0.64371 9e-3 1.2219-0.14144 1.4082-0.36594 0.17385-0.20948 0.27773-0.71409 0.23083-1.1214-0.07317-0.63544-0.23231-0.76418-1.122-0.90763-0.5702-0.0919-1.1992-0.38942-1.3979-0.66108-0.25954-0.35495-0.34007-1.2022-0.2862-3.011l0.07497-2.517h3.7011l1.7217 4.2333 1.7217 4.2333 1.4787 0.0781c0.97936 0.0517 1.5672-0.0376 1.7409-0.26458 0.14423-0.18847 1.1888-2.5136 2.3212-5.1671 1.1324-2.6534 1.9897-5.0049 1.905-5.2255-0.0964-0.25118-0.49511-0.40113-1.0666-0.40113-0.50198 0-1.0918 0.0687-1.3108 0.15275-0.21893 0.084-1.0155 1.6914-1.7701 3.5719-0.75462 1.8805-1.4697 3.4191-1.589 3.4191s-0.45209-0.62507-0.73953-1.3891c-0.28745-0.76398-0.8376-2.2224-1.2226-3.2408s-0.81858-1.971-0.9636-2.1167-1.5382-0.32442-3.0959-0.39718l-2.8323-0.13229-0.13229-1.8521-0.13229-1.8521h-2.6458l-0.13229 1.8365c-0.13098 1.8183-0.14096 1.8388-1.0075 2.062-0.81451 0.20989-0.86954 0.29343-0.79375 1.205 0.07128 0.85741 0.18035 0.99633 0.87518 1.1147l0.79375 0.13524 0.13229 3.2208c0.11196 2.726 0.2174 3.3355 0.68624 3.967 0.30467 0.4104 0.95952 0.88829 1.4552 1.062s1.3927 0.32252 1.9933 0.33073zm12.666-11.891c0.45112 0 0.96309-0.14287 1.1377-0.3175 0.17463-0.17462 0.3175-0.64237 0.3175-1.0394 0-0.39707-0.1871-0.90903-0.41577-1.1377-0.22868-0.22868-0.69642-0.41577-1.0394-0.41577-0.34301 0-0.81075 0.18709-1.0394 0.41577s-0.41577 0.74064-0.41577 1.1377 0.14287 0.86481 0.3175 1.0394c0.17462 0.17463 0.68659 0.3175 1.1377 0.3175z" fill="#1b7d3d"/><path transform="translate(-8.3333e-8)" d="m114.46 154.52c-3.0274 0.0544-3.7379-0.0118-4.6609-0.43422-0.59897-0.27412-1.3995-0.90545-1.779-1.403-0.37946-0.49751-0.83257-1.5225-1.0069-2.2778-0.24624-1.0668-0.24624-1.6796 0-2.7464 0.17434-0.75526 0.61766-1.7674 0.98516-2.2492 0.3675-0.48183 1.049-1.0738 1.5144-1.3155 0.48523-0.25197 1.8056-0.5058 3.0952-0.59503l2.249-0.15561 0.13229-1.8521 0.1323-1.8521h2.9104v14.817zm-0.92604-2.3736 1.3229-0.0718v-5.8208l-1.6062-0.0775c-1.4422-0.0696-1.6785-5e-3 -2.3151 0.6314-0.38991 0.38991-0.72779 1.0201-0.75085 1.4004-0.023 0.38034-0.0238 0.92965-2e-3 1.2207 0.0222 0.29104 0.10377 0.80075 0.18131 1.1327 0.0775 0.33193 0.52474 0.8405 0.99377 1.1302 0.615 0.37978 1.2217 0.50661 2.1757 0.45481zm9.7151 2.4359c-2.5936 4e-3 -3.5477-0.0819-3.6425-0.32887-0.0706-0.18407-0.0967-3.5476-0.0578-7.4745l0.0706-7.1398h2.9104l0.13229 1.8521 0.13229 1.8521 2.2687 0.15541c1.6342 0.11195 2.5591 0.31887 3.3073 0.73989 0.57124 0.32147 1.3303 1.0254 1.6868 1.5644 0.45872 0.69347 0.67992 1.4556 0.75677 2.6074 0.0597 0.89511-0.0338 2.059-0.20789 2.5864-0.17406 0.52742-0.62874 1.33-1.0104 1.7836-0.38165 0.45358-1.1751 1.0432-1.7632 1.3104-0.86286 0.39194-1.7476 0.48683-4.5833 0.49152zm0.84267-2.3754c0.64994 0 1.4871-0.1161 1.8603-0.25799s0.92337-0.64791 1.2226-1.1245c0.41645-0.66331 0.5175-1.173 0.43097-2.1738-0.0877-1.0148-0.27505-1.4467-0.83705-1.9301-0.61948-0.53286-0.95861-0.6115-2.3491-0.54474l-1.6251 0.078-0.0763 2.627c-0.042 1.4448-0.0159 2.7843 0.0578 2.9766 0.0835 0.21749 0.5806 0.34956 1.3158 0.34956z" fill="#fff"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" stroke="currentColor" viewBox="-2 -2 42 42"><g fill="none" fillRule="evenodd"><g stroke-width="6" transform="translate(1 1)"><circle cx="18" cy="18" r="18" stroke-opacity=".5"/><path d="M36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" dur="1s" from="0 18 18" repeatCount="indefinite" to="360 18 18" type="rotate"/></path></g></g></svg>
<svg stroke="currentColor" viewBox="-2 -2 42 42" xmlns="http://www.w3.org/2000/svg"><g transform="translate(1 1)" stroke-width="6" fill="none"><circle cx="18" cy="18" r="18" stroke-opacity=".5"/><path d="m36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" dur="1s" from="0 18 18" repeatCount="indefinite" to="360 18 18" type="rotate"/></path></g></svg>

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 373 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><g data-name="Layer 2"><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)" data-name="Layer 1"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -3,14 +3,13 @@ import axios from 'axios';
import { uniq } from 'lodash';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useContext, useState } from 'react';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { MediaStatus } from '../../../server/constants/media';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { Collection } from '../../../server/models/Collection';
import { LanguageContext } from '../../context/LanguageContext';
import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
@ -48,14 +47,13 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
const router = useRouter();
const settings = useSettings();
const { addToast } = useToasts();
const { locale } = useContext(LanguageContext);
const { hasPermission } = useUser();
const [requestModal, setRequestModal] = useState(false);
const [isRequesting, setRequesting] = useState(false);
const [is4k, setIs4k] = useState(false);
const { data, error, revalidate } = useSWR<Collection>(
`/api/v1/collection/${router.query.collectionId}?language=${locale}`,
`/api/v1/collection/${router.query.collectionId}`,
{
initialData: collection,
revalidateOnMount: true,
@ -63,7 +61,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
);
const { data: genres } = useSWR<{ id: number; name: string }[]>(
`/api/v1/genres/movie?language=${locale}`
`/api/v1/genres/movie`
);
if (!data && !error) {
@ -110,11 +108,18 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}
const hasRequestable =
hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], {
type: 'or',
}) &&
data.parts.filter(
(part) => !part.mediaInfo || part.mediaInfo.status === MediaStatus.UNKNOWN
).length > 0;
const hasRequestable4k =
settings.currentSettings.movie4kEnabled &&
hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], {
type: 'or',
}) &&
data.parts.filter(
(part) =>
!part.mediaInfo || part.mediaInfo.status4k === MediaStatus.UNKNOWN
@ -249,7 +254,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
title={intl.formatMessage(
is4k ? messages.requestcollection4k : messages.requestcollection
)}
iconSvg={<DuplicateIcon className="w-6 h-6" />}
iconSvg={<DuplicateIcon />}
>
<p>
{intl.formatMessage(
@ -325,55 +330,42 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
</span>
</div>
<div className="media-actions">
{hasPermission(Permission.REQUEST) &&
(hasRequestable ||
(settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
hasRequestable4k)) && (
<ButtonWithDropdown
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(!hasRequestable);
}}
text={
<>
<DownloadIcon className="w-5 h-5 mr-1" />
<span>
{intl.formatMessage(
hasRequestable
? messages.requestcollection
: messages.requestcollection4k
)}
</span>
</>
}
>
{settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
hasRequestable &&
hasRequestable4k && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<DownloadIcon className="w-5 h-5 mr-1" />
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
)}
{(hasRequestable || hasRequestable4k) && (
<ButtonWithDropdown
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(!hasRequestable);
}}
text={
<>
<DownloadIcon />
<span>
{intl.formatMessage(
hasRequestable
? messages.requestcollection
: messages.requestcollection4k
)}
</span>
</>
}
>
{hasRequestable && hasRequestable4k && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<DownloadIcon />
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
)}
</div>
</div>
{data.overview && (

@ -81,16 +81,14 @@ function Button<P extends ElementTypes = 'button'>(
switch (buttonSize) {
case 'sm':
buttonStyle.push('px-2.5 py-1.5 text-xs');
break;
case 'md':
buttonStyle.push('px-4 py-2 text-sm');
buttonStyle.push('px-2.5 py-1.5 text-xs button-sm');
break;
case 'lg':
buttonStyle.push('px-6 py-3 text-base');
buttonStyle.push('px-6 py-3 text-base button-lg');
break;
case 'md':
default:
buttonStyle.push('px-4 py-2 text-sm');
buttonStyle.push('px-4 py-2 text-sm button-md');
}
buttonStyle.push(className ?? '');

@ -19,16 +19,16 @@ const DropdownItem: React.FC<DropdownItemProps> = ({
buttonType = 'primary',
...props
}) => {
let styleClass = '';
let styleClass = 'button-md text-white';
switch (buttonType) {
case 'ghost':
styleClass =
'text-white bg-gray-700 hover:bg-gray-600 hover:text-white focus:border-gray-500 focus:text-white';
styleClass +=
' bg-gray-700 hover:bg-gray-600 focus:border-gray-500 focus:text-white';
break;
default:
styleClass =
'text-white bg-indigo-600 hover:bg-indigo-500 hover:text-white focus:border-indigo-700 focus:text-white';
styleClass +=
' bg-indigo-600 hover:bg-indigo-500 focus:border-indigo-700 focus:text-white';
}
return (
<a
@ -60,9 +60,9 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
useClickOutside(buttonRef, () => setIsOpen(false));
const styleClasses = {
mainButtonClasses: 'text-white border',
dropdownSideButtonClasses: 'border',
dropdownClasses: '',
mainButtonClasses: 'button-md text-white border',
dropdownSideButtonClasses: 'button-md border',
dropdownClasses: 'button-md',
};
switch (buttonType) {
@ -70,14 +70,14 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
styleClasses.mainButtonClasses +=
' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
styleClasses.dropdownSideButtonClasses = styleClasses.mainButtonClasses;
styleClasses.dropdownClasses = 'bg-gray-700';
styleClasses.dropdownClasses += ' bg-gray-700';
break;
default:
styleClasses.mainButtonClasses +=
' bg-indigo-600 border-indigo-600 hover:bg-indigo-500 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue';
styleClasses.dropdownSideButtonClasses +=
' bg-indigo-700 border-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue';
styleClasses.dropdownClasses = 'bg-indigo-600';
styleClasses.dropdownClasses += ' bg-indigo-600';
}
return (
@ -100,11 +100,7 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
aria-label="Expand"
onClick={() => setIsOpen((state) => !state)}
>
{dropdownIcon ? (
dropdownIcon
) : (
<ChevronDownIcon className="w-5 h-5" />
)}
{dropdownIcon ? dropdownIcon : <ChevronDownIcon />}
</button>
<Transition
show={isOpen}

@ -108,11 +108,7 @@ const Modal: React.FC<ModalProps> = ({
}}
>
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto text-white bg-gray-600 rounded-full sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
)}
{iconSvg && <div className="modal-icon">{iconSvg}</div>}
<div
className={`mt-3 text-center sm:mt-0 sm:text-left ${
iconSvg ? 'sm:ml-4' : 'sm:mb-4'

@ -22,7 +22,7 @@ const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
text={
<>
{links[0].svg}
{links[0].text}
<span>{links[0].text}</span>
</>
}
onClick={() => {
@ -40,7 +40,7 @@ const PlayButton: React.FC<PlayButtonProps> = ({ links }) => {
buttonType="ghost"
>
{link.svg}
{link.text}
<span>{link.text}</span>
</ButtonWithDropdown.Item>
);
})}

@ -0,0 +1,54 @@
import { EyeIcon, EyeOffIcon } from '@heroicons/react/solid';
import { Field } from 'formik';
import React, { useState } from 'react';
interface CustomInputProps extends React.ComponentProps<'input'> {
as?: 'input';
}
interface CustomFieldProps extends React.ComponentProps<typeof Field> {
as?: 'field';
}
type SensitiveInputProps = CustomInputProps | CustomFieldProps;
const SensitiveInput: React.FC<SensitiveInputProps> = ({
as = 'input',
...props
}) => {
const [isHidden, setHidden] = useState(true);
const Component = as === 'input' ? 'input' : Field;
const componentProps =
as === 'input'
? props
: {
...props,
as: props.type === 'textarea' && !isHidden ? 'textarea' : undefined,
};
return (
<>
<Component
{...componentProps}
className={`rounded-l-only ${componentProps.className ?? ''}`}
type={
isHidden
? 'password'
: props.type !== 'password'
? props.type ?? 'text'
: 'text'
}
/>
<button
onClick={(e) => {
e.preventDefault();
setHidden(!isHidden);
}}
className="input-action"
>
{isHidden ? <EyeOffIcon /> : <EyeIcon />}
</button>
</>
);
};
export default SensitiveInput;

@ -1,24 +1,22 @@
import React, { useContext } from 'react';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import GenreCard from '../../GenreCard';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext';
import { genreColorMap } from '../constants';
import PageTitle from '../../Common/PageTitle';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import Error from '../../../pages/_error';
import PageTitle from '../../Common/PageTitle';
import GenreCard from '../../GenreCard';
import { genreColorMap } from '../constants';
const messages = defineMessages({
moviegenres: 'Movie Genres',
});
const MovieGenreList: React.FC = () => {
const { locale } = useContext(LanguageContext);
const intl = useIntl();
const { data, error } = useSWR<GenreSliderItem[]>(
`/api/v1/discover/genreslider/movie?language=${locale}`
`/api/v1/discover/genreslider/movie`
);
if (!data && !error) {

@ -1,10 +1,9 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext } from 'react';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { genreColorMap } from '../constants';
@ -14,10 +13,9 @@ const messages = defineMessages({
});
const MovieGenreSlider: React.FC = () => {
const { locale } = useContext(LanguageContext);
const intl = useIntl();
const { data, error } = useSWR<GenreSliderItem[]>(
`/api/v1/discover/genreslider/movie?language=${locale}`,
`/api/v1/discover/genreslider/movie`,
{
refreshInterval: 0,
revalidateOnFocus: false,
@ -30,7 +28,7 @@ const MovieGenreSlider: React.FC = () => {
<Link href="/discover/movies/genres">
<a className="slider-title">
<span>{intl.formatMessage(messages.moviegenres)}</span>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
<ArrowCircleRightIcon />
</a>
</Link>
</div>

@ -1,24 +1,22 @@
import React, { useContext } from 'react';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import GenreCard from '../../GenreCard';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext';
import { genreColorMap } from '../constants';
import PageTitle from '../../Common/PageTitle';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import Error from '../../../pages/_error';
import PageTitle from '../../Common/PageTitle';
import GenreCard from '../../GenreCard';
import { genreColorMap } from '../constants';
const messages = defineMessages({
seriesgenres: 'Series Genres',
});
const TvGenreList: React.FC = () => {
const { locale } = useContext(LanguageContext);
const intl = useIntl();
const { data, error } = useSWR<GenreSliderItem[]>(
`/api/v1/discover/genreslider/tv?language=${locale}`
`/api/v1/discover/genreslider/tv`
);
if (!data && !error) {

@ -1,10 +1,9 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext } from 'react';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import { LanguageContext } from '../../../context/LanguageContext';
import GenreCard from '../../GenreCard';
import Slider from '../../Slider';
import { genreColorMap } from '../constants';
@ -14,10 +13,9 @@ const messages = defineMessages({
});
const TvGenreSlider: React.FC = () => {
const { locale } = useContext(LanguageContext);
const intl = useIntl();
const { data, error } = useSWR<GenreSliderItem[]>(
`/api/v1/discover/genreslider/tv?language=${locale}`,
`/api/v1/discover/genreslider/tv`,
{
refreshInterval: 0,
revalidateOnFocus: false,
@ -30,7 +28,7 @@ const TvGenreSlider: React.FC = () => {
<Link href="/discover/tv/genres">
<a className="slider-title">
<span>{intl.formatMessage(messages.tvgenres)}</span>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
<ArrowCircleRightIcon />
</a>
</Link>
</div>

@ -67,7 +67,7 @@ const Discover: React.FC = () => {
<Link href="/requests?filter=all">
<a className="slider-title">
<span>{intl.formatMessage(messages.recentrequests)}</span>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
<ArrowCircleRightIcon />
</a>
</Link>
</div>

@ -1,8 +1,12 @@
import React from 'react';
import { FormattedRelativeTime } from 'react-intl';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import { DownloadingItem } from '../../../server/lib/downloadtracker';
import Badge from '../Common/Badge';
const messages = defineMessages({
estimatedtime: 'Estimated {time}',
});
interface DownloadBlockProps {
downloadItem: DownloadingItem;
is4k?: boolean;
@ -12,6 +16,8 @@ const DownloadBlock: React.FC<DownloadBlockProps> = ({
downloadItem,
is4k = false,
}) => {
const intl = useIntl();
return (
<div className="p-4">
<div className="w-56 mb-2 text-sm truncate sm:w-80 md:w-full">
@ -48,27 +54,30 @@ const DownloadBlock: React.FC<DownloadBlockProps> = ({
<div className="flex items-center justify-between text-xs">
<span>
{is4k && (
<Badge badgeType="warning" className="mr-1">
<Badge badgeType="warning" className="mr-2">
4K
</Badge>
)}
<Badge className="capitalize">{downloadItem.status}</Badge>
</span>
<span>
ETA{' '}
{downloadItem.estimatedCompletionTime ? (
<FormattedRelativeTime
value={Math.floor(
(new Date(downloadItem.estimatedCompletionTime).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
) : (
'N/A'
)}
{downloadItem.estimatedCompletionTime
? intl.formatMessage(messages.estimatedtime, {
time: (
<FormattedRelativeTime
value={Math.floor(
(new Date(
downloadItem.estimatedCompletionTime
).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
),
})
: ''}
</span>
</div>
</div>

@ -1,11 +1,11 @@
import React, { useContext } from 'react';
import React from 'react';
import { MediaType } from '../../../server/constants/media';
import ImdbLogo from '../../assets/services/imdb.svg';
import PlexLogo from '../../assets/services/plex.svg';
import RTLogo from '../../assets/services/rt.svg';
import TmdbLogo from '../../assets/services/tmdb.svg';
import TvdbLogo from '../../assets/services/tvdb.svg';
import { LanguageContext } from '../../context/LanguageContext';
import useLocale from '../../hooks/useLocale';
interface ExternalLinkBlockProps {
mediaType: 'movie' | 'tv';
@ -24,7 +24,7 @@ const ExternalLinkBlock: React.FC<ExternalLinkBlockProps> = ({
rtUrl,
plexUrl,
}) => {
const { locale } = useContext(LanguageContext);
const { locale } = useLocale();
return (
<div className="flex items-center justify-center w-full space-x-5">

@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { sortBy } from 'lodash';
import dynamic from 'next/dynamic';
import React from 'react';
import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import type { OptionsType, OptionTypeBase } from 'react-select';
import useSWR from 'swr';
import { Language } from '../../../server/lib/settings';
import globalMessages from '../../i18n/globalMessages';
@ -29,7 +31,6 @@ const selectStyles = {
};
interface LanguageSelectorProps {
languages: Language[];
value?: string;
setFieldValue: (property: string, value: string) => void;
serverValue?: string;
@ -37,26 +38,33 @@ interface LanguageSelectorProps {
}
const LanguageSelector: React.FC<LanguageSelectorProps> = ({
languages,
value,
setFieldValue,
serverValue,
isUserSettings = false,
}) => {
const intl = useIntl();
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
const defaultLanguageNameFallback = serverValue
? languages.find((language) => language.iso_639_1 === serverValue)
?.english_name ?? serverValue
: undefined;
const options: OptionType[] =
languages.map((language) => ({
label:
const sortedLanguages = useMemo(() => {
languages?.forEach((language) => {
language.name =
intl.formatDisplayName(language.iso_639_1, {
type: 'language',
fallback: 'none',
}) ?? language.english_name,
}) ?? language.english_name;
});
return sortBy(languages, 'name');
}, [intl, languages]);
const languageName = (languageCode: string) =>
sortedLanguages?.find((language) => language.iso_639_1 === languageCode)
?.name ?? languageCode;
const options: OptionType[] =
sortedLanguages?.map((language) => ({
label: language.name,
value: language.iso_639_1,
})) ?? [];
@ -67,13 +75,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
language: serverValue
? serverValue
.split('|')
.map(
(value) =>
intl.formatDisplayName(value, {
type: 'language',
fallback: 'none',
}) ?? defaultLanguageNameFallback
)
.map((value) => languageName(value))
.reduce((prev, curr) =>
intl.formatMessage(globalMessages.delimitedlist, {
a: prev,
@ -112,13 +114,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
language: serverValue
? serverValue
.split('|')
.map(
(value) =>
intl.formatDisplayName(value, {
type: 'language',
fallback: 'none',
}) ?? defaultLanguageNameFallback
)
.map((value) => languageName(value))
.reduce((prev, curr) =>
intl.formatMessage(globalMessages.delimitedlist, {
a: prev,
@ -130,7 +126,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
isFixed: true,
}
: value?.split('|').map((code) => {
const matchedLanguage = languages.find(
const matchedLanguage = sortedLanguages?.find(
(lang) => lang.iso_639_1 === code
);
@ -139,11 +135,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
}
return {
label:
intl.formatDisplayName(matchedLanguage.iso_639_1, {
type: 'language',
fallback: 'none',
}) ?? matchedLanguage.english_name,
label: matchedLanguage.name,
value: matchedLanguage.iso_639_1,
};
}) ?? undefined

@ -1,93 +1,22 @@
import { TranslateIcon } from '@heroicons/react/solid';
import React, { useContext, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
availableLanguages,
AvailableLocales,
LanguageContext,
} from '../../../context/LanguageContext';
import useClickOutside from '../../../hooks/useClickOutside';
import useLocale from '../../../hooks/useLocale';
import Transition from '../../Transition';
const messages = defineMessages({
changelanguage: 'Change Language',
});
type AvailableLanguageObject = Record<
string,
{ code: AvailableLocales; display: string }
>;
const availableLanguages: AvailableLanguageObject = {
ca: {
code: 'ca',
display: 'Català',
},
de: {
code: 'de',
display: 'Deutsch',
},
en: {
code: 'en',
display: 'English',
},
es: {
code: 'es',
display: 'Español',
},
fr: {
code: 'fr',
display: 'Français',
},
it: {
code: 'it',
display: 'Italiano',
},
hu: {
code: 'hu',
display: 'Magyar',
},
nl: {
code: 'nl',
display: 'Nederlands',
},
'nb-NO': {
code: 'nb-NO',
display: 'Norsk Bokmål',
},
'pt-BR': {
code: 'pt-BR',
display: 'Português (Brasil)',
},
'pt-PT': {
code: 'pt-PT',
display: 'Português (Portugal)',
},
sv: {
code: 'sv',
display: 'Svenska',
},
ru: {
code: 'ru',
display: 'pусский',
},
sr: {
code: 'sr',
display: 'српски језик‬',
},
ja: {
code: 'ja',
display: '日本語',
},
'zh-TW': {
code: 'zh-TW',
display: '中文(臺灣)',
},
};
const LanguagePicker: React.FC = () => {
const intl = useIntl();
const dropdownRef = useRef<HTMLDivElement>(null);
const { locale, setLocale } = useContext(LanguageContext);
const { locale, setLocale } = useLocale();
const [isDropdownOpen, setDropdownOpen] = useState(false);
useClickOutside(dropdownRef, () => setDropdownOpen(false));

@ -27,6 +27,7 @@ const SearchInput: React.FC = () => {
className="block w-full py-2 pl-10 text-white placeholder-gray-300 bg-gray-900 border border-gray-600 rounded-full focus:border-gray-500 hover:border-gray-500 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
placeholder={intl.formatMessage(messages.searchPlaceholder)}
type="search"
inputMode="search"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => setIsOpen(true)}

@ -1,10 +1,9 @@
import { MenuAlt2Icon } from '@heroicons/react/outline';
import { InformationCircleIcon } from '@heroicons/react/solid';
import { ArrowLeftIcon, InformationCircleIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Permission, useUser } from '../../hooks/useUser';
import LanguagePicker from './LanguagePicker';
import SearchInput from './SearchInput';
import Sidebar from './Sidebar';
import UserDropdown from './UserDropdown';
@ -23,7 +22,7 @@ const Layout: React.FC = ({ children }) => {
useEffect(() => {
const updateScrolled = () => {
if (window.pageYOffset > 60) {
if (window.pageYOffset > 20) {
setIsScrolled(true);
} else {
setIsScrolled(false);
@ -55,16 +54,25 @@ const Layout: React.FC = ({ children }) => {
}}
>
<button
className="px-4 text-gray-200 focus:outline-none focus:bg-gray-300 focus:text-gray-600 md:hidden"
className={`px-4 ${
isScrolled ? 'text-gray-200' : 'text-gray-400'
} focus:outline-none md:hidden transition duration-300`}
aria-label="Open sidebar"
onClick={() => setSidebarOpen(true)}
>
<MenuAlt2Icon className="w-6 h-6" />
</button>
<div className="flex justify-between flex-1 pr-4 md:pr-4 md:pl-4">
<div className="flex items-center justify-between flex-1 pr-4 md:pr-4 md:pl-4">
<button
className={`mr-2 ${
isScrolled ? 'text-gray-200' : 'text-gray-400'
} transition duration-300 hover:text-white pwa-only focus:outline-none focus:text-white`}
onClick={() => router.back()}
>
<ArrowLeftIcon className="w-7" />
</button>
<SearchInput />
<div className="flex items-center ml-2">
<LanguagePicker />
<UserDropdown />
</div>
</div>

@ -6,6 +6,7 @@ import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup';
import Button from '../Common/Button';
import SensitiveInput from '../Common/SensitiveInput';
const messages = defineMessages({
email: 'Email Address',
@ -69,7 +70,7 @@ const LocalLogin: React.FC<LocalLoginProps> = ({ revalidate }) => {
id="email"
name="email"
type="text"
placeholder="name@example.com"
inputMode="email"
/>
</div>
{errors.email && touched.email && (
@ -81,12 +82,12 @@ const LocalLogin: React.FC<LocalLoginProps> = ({ revalidate }) => {
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="form-input-field">
<Field
<SensitiveInput
as="field"
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder={intl.formatMessage(messages.password)}
/>
</div>
{errors.password && touched.password && (
@ -104,8 +105,10 @@ const LocalLogin: React.FC<LocalLoginProps> = ({ revalidate }) => {
<span className="inline-flex rounded-md shadow-sm">
<Link href="/resetpassword" passHref>
<Button as="a" buttonType="ghost">
<SupportIcon className="w-5 h-5 mr-1" />
{intl.formatMessage(messages.forgotpassword)}
<SupportIcon />
<span>
{intl.formatMessage(messages.forgotpassword)}
</span>
</Button>
</Link>
</span>
@ -115,10 +118,12 @@ const LocalLogin: React.FC<LocalLoginProps> = ({ revalidate }) => {
type="submit"
disabled={isSubmitting || !isValid}
>
<LoginIcon className="w-5 h-5 mr-1" />
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
<LoginIcon />
<span>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</span>
</Button>
</span>
</div>

@ -1,6 +1,6 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useContext, useEffect } from 'react';
import React, { useEffect } from 'react';
import { useSWRInfinite } from 'swr';
import { MediaStatus } from '../../../server/constants/media';
import type {
@ -8,7 +8,6 @@ import type {
PersonResult,
TvResult,
} from '../../../server/models/Search';
import { LanguageContext } from '../../context/LanguageContext';
import useSettings from '../../hooks/useSettings';
import PersonCard from '../PersonCard';
import Slider from '../Slider';
@ -38,14 +37,13 @@ const MediaSlider: React.FC<MediaSliderProps> = ({
hideWhenEmpty = false,
}) => {
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, setSize, size } = useSWRInfinite<MixedResult>(
(pageIndex: number, previousPageData: MixedResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `${url}?page=${pageIndex + 1}&language=${locale}`;
return `${url}?page=${pageIndex + 1}`;
},
{
initialSize: 2,
@ -141,7 +139,7 @@ const MediaSlider: React.FC<MediaSliderProps> = ({
<Link href={linkUrl}>
<a className="slider-title">
<span>{title}</span>
<ArrowCircleRightIcon className="w-6 h-6 ml-2" />
<ArrowCircleRightIcon />
</a>
</Link>
) : (

@ -1,15 +1,14 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useContext } from 'react';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullcast: 'Full Cast',
@ -18,9 +17,8 @@ const messages = defineMessages({
const MovieCast: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const { data, error } = useSWR<MovieDetails>(
`/api/v1/movie/${router.query.movieId}?language=${locale}`
`/api/v1/movie/${router.query.movieId}`
);
if (!data && !error) {

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

Loading…
Cancel
Save