Merge branch 'develop'

pull/2563/head
sct 3 years ago
commit 21cdf7d113
No known key found for this signature in database
GPG Key ID: 845B4E54293A6F5E

@ -503,6 +503,42 @@
"contributions": [
"translation"
]
},
{
"login": "iceHtwoO",
"name": "Alexander Neuhäuser",
"avatar_url": "https://avatars.githubusercontent.com/u/27020492?v=4",
"profile": "https://github.com/iceHtwoO",
"contributions": [
"translation"
]
},
{
"login": "liviokanone",
"name": "Livio",
"avatar_url": "https://avatars.githubusercontent.com/u/37431541?v=4",
"profile": "http://www.unext.co.jp",
"contributions": [
"design"
]
},
{
"login": "tangentThought",
"name": "tangentThought",
"avatar_url": "https://avatars.githubusercontent.com/u/25516090?v=4",
"profile": "https://github.com/tangentThought",
"contributions": [
"code"
]
},
{
"login": "nicospz",
"name": "Nicolás Espinoza",
"avatar_url": "https://avatars.githubusercontent.com/u/31373060?v=4",
"profile": "https://github.com/nicospz",
"contributions": [
"code"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

@ -21,7 +21,7 @@ docker-compose.yml
docs
LICENSE
node_modules
public/os_logo_square.png
public/os_logo_filled.png
public/preview.jpg
snap
stylelint.config.js

@ -12,7 +12,7 @@ jobs:
test:
name: Lint & Test Build
runs-on: ubuntu-20.04
container: node:14.16-alpine
container: node:14.17-alpine
steps:
- name: Checkout
uses: actions/checkout@v2.3.4

@ -9,7 +9,7 @@ jobs:
test:
name: Lint & Test Build
runs-on: ubuntu-20.04
container: node:14.16-alpine
container: node:14.17-alpine
steps:
- name: Checkout
uses: actions/checkout@v2.3.4

@ -20,7 +20,7 @@ jobs:
name: Lint & Test Build
needs: jobs
runs-on: ubuntu-20.04
container: node:14.16-alpine
container: node:14.17-alpine
steps:
- name: Checkout
uses: actions/checkout@v2.3.4

@ -1,4 +1,4 @@
FROM node:14.16-alpine AS BUILD_IMAGE
FROM node:14.17-alpine AS BUILD_IMAGE
WORKDIR /app
@ -31,7 +31,7 @@ RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:14.16-alpine
FROM node:14.17-alpine
WORKDIR /app

@ -1,4 +1,4 @@
FROM node:14.16-alpine
FROM node:14.17-alpine
COPY . /app
WORKDIR /app

@ -1,5 +1,5 @@
<p align="center">
<img src="https://i.imgur.com/TMoEG7g.png" alt="Overseerr">
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
</p>
<p align="center">
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" />
@ -12,7 +12,7 @@
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-54-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-58-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
@ -143,6 +143,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/littlerooster"><img src="https://avatars.githubusercontent.com/u/83890654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>littlerooster</b></sub></a><br /><a href="#translation-littlerooster" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/dphildebrandt"><img src="https://avatars.githubusercontent.com/u/154459?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dustin Hildebrandt</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=dphildebrandt" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Generator"><img src="https://avatars.githubusercontent.com/u/44146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bruno Guerreiro</b></sub></a><br /><a href="#translation-Generator" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/iceHtwoO"><img src="https://avatars.githubusercontent.com/u/27020492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Neuhäuser</b></sub></a><br /><a href="#translation-iceHtwoO" title="Translation">🌍</a></td>
<td align="center"><a href="http://www.unext.co.jp"><img src="https://avatars.githubusercontent.com/u/37431541?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Livio</b></sub></a><br /><a href="#design-liviokanone" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tangentThought"><img src="https://avatars.githubusercontent.com/u/25516090?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tangentThought</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=tangentThought" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nicospz"><img src="https://avatars.githubusercontent.com/u/31373060?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolás Espinoza</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=nicospz" title="Code">💻</a></td>
</tr>
</table>

@ -145,7 +145,8 @@ location ^~ /overseerr {
sub_filter '/android-' '/$app/android-';
sub_filter '/apple-' '/$app/apple-';
sub_filter '/favicon' '/$app/favicon';
sub_filter '/logo.png' '/$app/logo.png';
sub_filter '/logo_full.svg' '/$app/logo_full.svg';
sub_filter '/logo_stacked.svg' '/$app/logo_stacked.svg';
sub_filter '/site.webmanifest' '/$app/site.webmanifest';
}
```

@ -16,7 +16,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
```bash
docker run -d \
--name overseerr \
-e LOG_LEVEL=info \
-e LOG_LEVEL=debug \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
@ -39,7 +39,7 @@ services:
image: sctx/overseerr:latest
container_name: overseerr
environment:
- LOG_LEVEL=info
- LOG_LEVEL=debug
- TZ=Asia/Tokyo
ports:
- 5055:5055
@ -56,7 +56,7 @@ services:
docker run -d \
--name overseerr \
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
-e LOG_LEVEL=info \
-e LOG_LEVEL=debug \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
@ -99,20 +99,41 @@ Use a 3rd party updating mechanism such as [Watchtower](https://github.com/conta
## Windows
Please refer to the [Docker Desktop for Windows user manual](https://docs.docker.com/docker-for-windows/) for details on how to install Docker on Windows.
Please refer to the [Docker Desktop for Windows user manual](https://docs.docker.com/docker-for-windows/) for details on how to install Docker on Windows. There is no need to install a Linux distro if using named volumes like in the example below.
{% hint style="danger" %}
**WSL2 will need to be installed to prevent DB corruption!** Please see the [Docker Desktop WSL 2 backend documentation](https://docs.docker.com/docker-for-windows/wsl/) for instructions on how to enable WSL2. The command below will only work with WSL2 installed!
**WSL2 will need to be installed to prevent DB corruption!** Please see the [Docker Desktop WSL 2 backend documentation](https://docs.docker.com/docker-for-windows/wsl/) for instructions on how to enable WSL2. The commands below will only work with WSL2 installed!
{% endhint %}
First, create a volume to store the configuration data for Overseerr using using either the Docker CLI:
```bash
docker volume create overseerr-data
```
or the Docker Desktop app:
1. Open the Docker Desktop app
2. Head to the Volumes tab
3. Click on the "New Volume" button near the top right
4. Enter a name for the volume (example: `overseerr-data`) and hit "Create"
Then, create and start the Overseerr container:
```bash
docker run -d -e LOG_LEVEL=info -e TZ=Asia/Tokyo -p 5055:5055 -v "/your/path/here:/app/config" --restart unless-stopped sctx/overseerr
docker run -d -e LOG_LEVEL=debug -e TZ=Asia/Tokyo -p 5055:5055 -v "overseerr-data:/app/config" --restart unless-stopped sctx/overseerr
```
If using a named volume like above, you can safely ignore the warning about the `/app/config` folder being incorrectly mounted on the setup page.
To access the files inside the volume created above, navigate to `\\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\overseerr-data\_data` using File Explorer.
{% hint style="info" %}
Docker on Windows works differently than it does on Linux; it runs Docker inside of a stripped-down Linux VM. Volume mounts are exposed to Docker inside this VM via SMB mounts. While this is fine for media, it is unacceptable for the `/app/config` directory because SMB does not support file locking. This will eventually corrupt your database, which can lead to slow behavior and crashes.
**If you must run Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.)
Named volumes, like in the example commands above, are automatically mounted inside the VM.
{% endhint %}
## Linux

@ -35,6 +35,6 @@ Try to answer the following questions:
## How can I share my logs?
1. Locate the current log file at `<your Overseeerr config directory>/logs/overseerr.log`.
1. Locate the current log file at `<your Overseerr config directory>/logs/overseerr.log`.
2. Open the log file and **copy its contents** into a [**secret gist** on GitHub](https://gist.github.com/). If you upload your logs elsewhere, we may ask you to share them again via GitHub Gist.
3. **Share the link/URL to your secret gist** in the [`#support` channel in our Discord server](https://discord.gg/overseerr).

@ -24,11 +24,11 @@ Set this to the hostname or IP address of your SMTP host/server.
Set this to a supported port number for your SMTP host. `465` and `587` are commonly used.
### Enable SSL (optional)
### Encryption Method
This setting should only be enabled for ports that use [implicit SSL/TLS](https://tools.ietf.org/html/rfc8314) (e.g., port `465` in most cases).
In most cases, [Use Implicit TLS](https://tools.ietf.org/html/rfc8314) should be selected for port 465, and [Use STARTTLS if available](https://en.wikipedia.org/wiki/Opportunistic_TLS) for port 587. Please refer to your email provider's documentations for details on how to configure this setting.
For servers that support [opportunistic TLS/STARTTLS](https://en.wikipedia.org/wiki/Opportunistic_TLS) (typically via port `587`), this setting should **not** be enabled.
The default value for this setting is **Use STARTTLS if available**.
### SMTP Username & Password

1
next-env.d.ts vendored

@ -1,2 +1,3 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

@ -5,9 +5,6 @@ module.exports = {
images: {
domains: ['image.tmdb.org'],
},
future: {
webpack5: true,
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,

@ -52,9 +52,15 @@ components:
email:
type: string
example: 'hey@itsme.com'
readOnly: true
username:
type: string
plexToken:
type: string
readOnly: true
plexUsername:
type: string
readOnly: true
userType:
type: integer
example: 1
@ -77,13 +83,6 @@ components:
type: number
example: 5
readOnly: true
requests:
type: array
readOnly: true
items:
$ref: '#/components/schemas/MediaRequest'
settings:
$ref: '#/components/schemas/UserSettings'
required:
- id
- email
@ -92,11 +91,11 @@ components:
UserSettings:
type: object
properties:
discordId:
locale:
type: string
region:
type: string
language:
originalLanguage:
type: string
MainSettings:
type: object
@ -398,7 +397,6 @@ components:
activeLanguageProfileId:
type: number
example: 1
nullable: true
activeAnimeProfileId:
type: number
nullable: true
@ -408,6 +406,7 @@ components:
activeAnimeProfileName:
type: string
example: 720p/1080p
nullable: true
activeAnimeDirectory:
type: string
nullable: true
@ -769,6 +768,10 @@ components:
$ref: '#/components/schemas/ExternalIds'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
watchProviders:
type: array
items:
$ref: '#/components/schemas/WatchProviders'
Episode:
type: object
properties:
@ -943,6 +946,10 @@ components:
$ref: '#/components/schemas/Keyword'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
watchProviders:
type: array
items:
$ref: '#/components/schemas/WatchProviders'
MediaRequest:
type: object
properties:
@ -953,7 +960,7 @@ components:
status:
type: number
example: 0
description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED, 4 = AVAILABLE
description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED
readOnly: true
media:
$ref: '#/components/schemas/MediaInfo'
@ -1587,9 +1594,8 @@ components:
UserSettingsNotifications:
type: object
properties:
notificationAgents:
type: number
example: 0
notificationTypes:
$ref: '#/components/schemas/NotificationAgentTypes'
emailEnabled:
type: boolean
pgpKey:
@ -1614,6 +1620,52 @@ components:
telegramSendSilently:
type: boolean
nullable: true
NotificationAgentTypes:
type: object
properties:
discord:
type: number
email:
type: number
pushbullet:
type: number
pushover:
type: number
slack:
type: number
telegram:
type: number
webhook:
type: number
webpush:
type: number
WatchProviders:
type: array
items:
type: object
properties:
iso_3166_1:
type: string
link:
type: string
buy:
type: array
items:
$ref: '#/components/schemas/WatchProviderDetails'
flatrate:
items:
$ref: '#/components/schemas/WatchProviderDetails'
WatchProviderDetails:
type: object
properties:
displayPriority:
type: number
logoPath:
type: string
id:
type: number
name:
type: string
securitySchemes:
cookieAuth:
type: apiKey
@ -1842,8 +1894,8 @@ paths:
$ref: '#/components/schemas/PlexLibrary'
/settings/plex/devices/servers:
get:
summary: Gets the user's available plex servers
description: Returns a list of available plex servers and their connectivity state
summary: Gets the user's available Plex servers
description: Returns a list of available Plex servers and their connectivity state
tags:
- settings
responses:
@ -2980,7 +3032,15 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
type: object
properties:
email:
type: string
example: 'hey@itsme.com'
username:
type: string
permissions:
type: number
responses:
'201':
description: The created user
@ -2991,7 +3051,7 @@ paths:
put:
summary: Update batch of users
description: |
Update users with given IDs with provided values in request `body.settings`. You cannot update users' plex tokens through this request.
Update users with given IDs with provided values in request `body.settings`. You cannot update users' Plex tokens through this request.
Requires the `MANAGE_USERS` permission.
tags:
@ -3018,7 +3078,6 @@ paths:
type: array
items:
$ref: '#/components/schemas/User'
/user/import-from-plex:
post:
summary: Import all users from Plex
@ -3067,7 +3126,7 @@ paths:
get:
summary: Get user by ID
description: |
Retrieves user details in a JSON object.. Requires the `MANAGE_USERS` permission.
Retrieves user details in a JSON object. Requires the `MANAGE_USERS` permission.
tags:
- users
parameters:
@ -4180,6 +4239,9 @@ paths:
type: string
languageProfileId:
type: number
userId:
type: number
nullable: true
required:
- mediaType
- mediaId

@ -17,11 +17,11 @@
},
"license": "MIT",
"dependencies": {
"@headlessui/react": "^1.2.0",
"@headlessui/react": "^1.3.0",
"@heroicons/react": "^1.0.1",
"@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.67",
"@tanem/react-nprogress": "^3.0.70",
"ace-builds": "^1.4.12",
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
@ -33,18 +33,18 @@
"csurf": "^1.11.0",
"email-templates": "^8.0.7",
"express": "^4.17.1",
"express-openapi-validator": "^4.12.11",
"express-openapi-validator": "^4.12.14",
"express-rate-limit": "^5.2.6",
"express-session": "^1.17.2",
"formik": "^2.2.9",
"gravatar-url": "3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.21",
"next": "10.1.3",
"next": "11.0.1",
"node-cache": "^5.1.2",
"node-schedule": "^2.0.0",
"nodemailer": "^6.6.1",
"openpgp": "^5.0.0-2",
"nodemailer": "^6.6.2",
"openpgp": "^5.0.0-3",
"plex-api": "^5.3.1",
"pug": "^3.0.2",
"react": "17.0.2",
@ -52,7 +52,7 @@
"react-animate-height": "^2.0.23",
"react-dom": "17.0.2",
"react-intersection-observer": "^8.32.0",
"react-intl": "5.19.0",
"react-intl": "5.20.3",
"react-markdown": "^6.0.2",
"react-select": "^4.3.1",
"react-spring": "^9.2.3",
@ -61,12 +61,11 @@
"react-truncate-markup": "^5.1.0",
"react-use-clipboard": "1.0.7",
"reflect-metadata": "^0.1.13",
"secure-random-password": "^0.2.2",
"secure-random-password": "^0.2.3",
"sqlite3": "^5.0.2",
"swagger-ui-express": "^4.1.6",
"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.5",
@ -75,7 +74,7 @@
"yup": "^0.32.9"
},
"devDependencies": {
"@babel/cli": "^7.14.3",
"@babel/cli": "^7.14.5",
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"@semantic-release/changelog": "^5.0.1",
@ -91,35 +90,35 @@
"@types/csurf": "^1.11.1",
"@types/email-templates": "^8.0.3",
"@types/express": "^4.17.12",
"@types/express-rate-limit": "^5.1.1",
"@types/express-rate-limit": "^5.1.2",
"@types/express-session": "^1.17.3",
"@types/lodash": "^4.14.170",
"@types/node": "^15.6.1",
"@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.8",
"@types/react-select": "^4.0.15",
"@types/react-toast-notifications": "^2.4.1",
"@types/react-transition-group": "^4.4.1",
"@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/web-push": "^3.3.1",
"@types/xml2js": "^0.4.8",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"autoprefixer": "^10.2.6",
"babel-plugin-react-intl": "^8.2.25",
"babel-plugin-react-intl-auto": "^3.3.0",
"commitizen": "^4.2.4",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.27.0",
"eslint": "^7.29.0",
"eslint-config-next": "^11.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-formatjs": "^2.15.5",
"eslint-plugin-formatjs": "^2.16.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
@ -128,13 +127,13 @@
"husky": "4.3.8",
"lint-staged": "^11.0.0",
"nodemon": "^2.0.7",
"postcss": "^8.3.0",
"postcss": "^8.3.5",
"prettier": "^2.3.1",
"semantic-release": "^17.4.3",
"semantic-release": "^17.4.4",
"semantic-release-docker-buildx": "^1.0.1",
"tailwindcss": "^2.1.4",
"ts-node": "^9.1.1",
"typescript": "^4.3.2"
"tailwindcss": "^2.2.2",
"ts-node": "^10.0.0",
"typescript": "^4.3.4"
},
"resolutions": {
"sqlite3/node-gyp": "^5.1.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -31,7 +31,7 @@
<body>
<h1>You are offline</h1>
<button type="button"> Reload</button>
<button type="button"> Reload</button>
<!-- Inline the page's JavaScript file. -->
<script>

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -142,7 +142,7 @@ class PlexAPI {
`/library/sections/${id}/all`
);
return response.MediaContainer.Metadata;
return response.MediaContainer.Metadata ?? [];
}
public async getMetadata(

@ -1,39 +1,28 @@
import cacheManager from '../lib/cache';
import ExternalAPI from './externalapi';
interface RTMovieOldSearchResult {
id: number;
title: string;
year: number;
ratings: {
critics_rating: 'Certified Fresh' | 'Fresh' | 'Rotten';
critics_score: number;
audience_rating: 'Upright' | 'Spilled';
audience_score: number;
};
links: {
self: string;
alternate: string;
};
interface RTSearchResult {
meterClass: 'certified_fresh' | 'fresh' | 'rotten';
meterScore: number;
url: string;
}
interface RTTvSearchResult {
interface RTTvSearchResult extends RTSearchResult {
title: string;
meterClass: 'fresh' | 'rotten';
meterScore: number;
url: string;
startYear: number;
endYear: number;
}
interface RTMovieSearchResponse {
total: number;
movies: RTMovieOldSearchResult[];
interface RTMovieSearchResult extends RTSearchResult {
name: string;
url: string;
year: number;
}
interface RTMultiSearchResponse {
tvCount: number;
tvSeries: RTTvSearchResult[];
movieCount: number;
movies: RTMovieSearchResult[];
}
export interface RTRating {
@ -88,19 +77,19 @@ class RottenTomatoes extends ExternalAPI {
year: number
): Promise<RTRating | null> {
try {
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
params: { q: name },
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
params: { q: name, limit: 10 },
});
// First, attempt to match exact name and year
let movie = data.movies.find(
(movie) => movie.year === year && movie.title === name
(movie) => movie.year === year && movie.name === name
);
// If we don't find a movie, try to match partial name and year
if (!movie) {
movie = data.movies.find(
(movie) => movie.year === year && movie.title.includes(name)
(movie) => movie.year === year && movie.name.includes(name)
);
}
@ -111,7 +100,7 @@ class RottenTomatoes extends ExternalAPI {
// One last try, try exact name match only
if (!movie) {
movie = data.movies.find((movie) => movie.title === name);
movie = data.movies.find((movie) => movie.name === name);
}
if (!movie) {
@ -119,12 +108,15 @@ class RottenTomatoes extends ExternalAPI {
}
return {
title: movie.title,
url: movie.links.alternate,
criticsRating: movie.ratings.critics_rating,
criticsScore: movie.ratings.critics_score,
audienceRating: movie.ratings.audience_rating,
audienceScore: movie.ratings.audience_score,
title: movie.name,
url: movie.url,
criticsRating:
movie.meterClass === 'certified_fresh'
? 'Certified Fresh'
: movie.meterClass === 'fresh'
? 'Fresh'
: 'Rotten',
criticsScore: movie.meterScore,
year: movie.year,
};
} catch (e) {

@ -170,7 +170,8 @@ class TheMovieDb extends ExternalAPI {
{
params: {
language,
append_to_response: 'credits,external_ids,videos,release_dates',
append_to_response:
'credits,external_ids,videos,release_dates,watch/providers',
},
},
43200
@ -196,7 +197,7 @@ class TheMovieDb extends ExternalAPI {
params: {
language,
append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings',
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
},
},
43200

@ -166,6 +166,10 @@ export interface TmdbMovieDetails {
};
external_ids: TmdbExternalIds;
videos: TmdbVideoResult;
'watch/providers'?: {
id: number;
results?: { [iso_3166_1: string]: TmdbWatchProviders };
};
}
export interface TmdbVideo {
@ -269,6 +273,10 @@ export interface TmdbTvDetails {
results: TmdbKeyword[];
};
videos: TmdbVideoResult;
'watch/providers'?: {
id: number;
results?: { [iso_3166_1: string]: TmdbWatchProviders };
};
}
export interface TmdbVideoResult {
@ -401,3 +409,16 @@ export interface TmdbNetwork {
logo_path?: string;
origin_country?: string;
}
export interface TmdbWatchProviders {
link?: string;
buy?: TmdbWatchProviderDetails[];
flatrate?: TmdbWatchProviderDetails[];
}
export interface TmdbWatchProviderDetails {
display_priority?: number;
logo_path?: string;
provider_id: number;
provider_name: string;
}

@ -1,4 +1,4 @@
import { isEqual } from 'lodash';
import { isEqual, truncate } from 'lodash';
import {
AfterInsert,
AfterRemove,
@ -145,7 +145,11 @@ export class MediaRequest {
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
media,
request: this,
@ -158,7 +162,11 @@ export class MediaRequest {
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: tv.overview,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media,
extra: [
@ -217,7 +225,11 @@ export class MediaRequest {
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
@ -236,7 +248,11 @@ export class MediaRequest {
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: tv.overview,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
@ -495,7 +511,11 @@ export class MediaRequest {
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: this,
@ -707,7 +727,11 @@ export class MediaRequest {
? ` (${series.first_air_date.slice(0, 4)})`
: ''
}`,
message: series.overview,
message: truncate(series.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media,
extra: [

@ -1,4 +1,5 @@
import bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
import path from 'path';
import { default as generatePassword } from 'secure-random-password';
import {
@ -15,7 +16,6 @@ import {
RelationCount,
UpdateDateColumn,
} from 'typeorm';
import { v4 as uuid } from 'uuid';
import { MediaRequestStatus, MediaType } from '../constants/media';
import { UserType } from '../constants/user';
import { QuotaResponse } from '../interfaces/api/userInterfaces';
@ -189,7 +189,7 @@ export class User {
}
public async resetPassword(): Promise<void> {
const guid = uuid();
const guid = randomUUID();
this.resetPasswordGuid = guid;
// 24 hours into the future
@ -212,7 +212,7 @@ export class User {
},
locals: {
resetPasswordLink,
applicationUrl: resetPasswordLink,
applicationUrl,
applicationTitle,
},
});
@ -307,7 +307,7 @@ export class User {
limit: movieQuotaLimit,
used: movieQuotaUsed,
remaining: movieQuotaLimit
? movieQuotaLimit - movieQuotaUsed
? Math.max(0, movieQuotaLimit - movieQuotaUsed)
: undefined,
restricted:
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
@ -318,7 +318,9 @@ export class User {
days: tvQuotaDays,
limit: tvQuotaLimit,
used: tvQuotaUsed,
remaining: tvQuotaLimit ? tvQuotaLimit - tvQuotaUsed : undefined,
remaining: tvQuotaLimit
? Math.max(0, tvQuotaLimit - tvQuotaUsed)
: undefined,
restricted:
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
},

@ -1,4 +1,4 @@
import crypto from 'crypto';
import { randomBytes } from 'crypto';
import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream';
@ -41,7 +41,7 @@ class PGPEncryptor extends Transform {
const validPublicKeys = await Promise.all(
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey }))
);
let privateKey: openpgp.Key | undefined;
let privateKey: openpgp.PrivateKey | undefined;
// Just return the message if there is no one to encrypt for
if (!validPublicKeys.length) {
@ -51,7 +51,7 @@ class PGPEncryptor extends Transform {
// Only sign the message if private key and password exist
if (this._signingKey && this._password) {
privateKey = await openpgp.readKey({
privateKey = await openpgp.readPrivateKey({
armoredKey: this._signingKey,
});
@ -107,7 +107,7 @@ class PGPEncryptor extends Transform {
}
// Generate a new boundary for the email content
const boundary = 'nm_' + crypto.randomBytes(14).toString('hex');
const boundary = 'nm_' + randomBytes(14).toString('hex');
/**
* Concatenate everything into single strings
* and add pgp headers to the email headers
@ -135,8 +135,8 @@ class PGPEncryptor extends Transform {
emailPartDelimiter +
messageParts.join(emailPartDelimiter),
}),
publicKeys: validPublicKeys,
privateKeys: privateKey,
encryptionKeys: validPublicKeys,
signingKeys: privateKey,
});
const body =

@ -190,6 +190,7 @@ class WebPushAgent
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.leftJoinAndSelect('pushSub.user', 'user')
.where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id),
})

@ -1,5 +1,5 @@
import { randomUUID } from 'crypto';
import { getRepository } from 'typeorm';
import { v4 as uuid } from 'uuid';
import TheMovieDb from '../../api/themoviedb';
import { MediaStatus, MediaType } from '../../constants/media';
import Media from '../../entity/Media';
@ -512,7 +512,7 @@ class BaseScanner<T> {
*/
protected startRun(): string {
const settings = getSettings();
const sessionId = uuid();
const sessionId = randomUUID();
this.sessionId = sessionId;
this.log('Scan starting', 'info', { sessionId });

@ -7,9 +7,9 @@ import { User } from '../../../entity/User';
import { getSettings, Library } from '../../settings';
import BaseScanner, {
MediaIds,
ProcessableSeason,
RunnableScanner,
StatusBase,
ProcessableSeason,
} from '../baseScanner';
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);

@ -1,7 +1,7 @@
import { randomUUID } from 'crypto';
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';
@ -234,7 +234,7 @@ class Settings {
constructor(initialSettings?: AllSettings) {
this.data = {
clientId: uuidv4(),
clientId: randomUUID(),
vapidPrivate: '',
vapidPublic: '',
main: {
@ -428,7 +428,7 @@ class Settings {
get clientId(): string {
if (!this.data.clientId) {
this.data.clientId = uuidv4();
this.data.clientId = randomUUID();
this.save();
}
@ -454,7 +454,7 @@ class Settings {
}
private generateApiKey(): string {
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
return Buffer.from(`${Date.now()}${randomUUID()})`).toString('base64');
}
private generateVapidKeys(force = false): void {

@ -3,18 +3,20 @@ import type {
TmdbMovieReleaseResult,
TmdbProductionCompany,
} from '../api/themoviedb/interfaces';
import Media from '../entity/Media';
import {
ProductionCompany,
Genre,
Cast,
Crew,
ExternalIds,
Genre,
mapCast,
mapCrew,
ExternalIds,
mapExternalIds,
mapVideos,
mapWatchProviders,
ProductionCompany,
WatchProviders,
} from './common';
import Media from '../entity/Media';
export interface Video {
url?: string;
@ -78,6 +80,7 @@ export interface MovieDetails {
mediaInfo?: Media;
externalIds: ExternalIds;
plexUrl?: string;
watchProviders?: WatchProviders[];
}
export const mapProductionCompany = (
@ -136,4 +139,5 @@ export const mapMovieDetails = (
: undefined,
externalIds: mapExternalIds(movie.external_ids),
mediaInfo: media,
watchProviders: mapWatchProviders(movie['watch/providers']?.results ?? {}),
});

@ -1,25 +1,27 @@
import type {
TmdbNetwork,
TmdbSeasonWithEpisodes,
TmdbTvDetails,
TmdbTvEpisodeResult,
TmdbTvRatingResult,
TmdbTvSeasonResult,
} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import {
Genre,
ProductionCompany,
Cast,
Crew,
ExternalIds,
Genre,
Keyword,
mapAggregateCast,
mapCrew,
ExternalIds,
mapExternalIds,
Keyword,
mapVideos,
mapWatchProviders,
ProductionCompany,
TvNetwork,
WatchProviders,
} from './common';
import type {
TmdbTvEpisodeResult,
TmdbTvSeasonResult,
TmdbTvDetails,
TmdbSeasonWithEpisodes,
TmdbTvRatingResult,
TmdbNetwork,
} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import { Video } from './Movie';
interface Episode {
@ -102,6 +104,7 @@ export interface TvDetails {
externalIds: ExternalIds;
keywords: Keyword[];
mediaInfo?: Media;
watchProviders?: WatchProviders[];
}
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
@ -213,4 +216,5 @@ export const mapTvDetails = (
name: keyword.name,
})),
mediaInfo: media,
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
});

@ -1,12 +1,13 @@
import type {
TmdbCreditCast,
TmdbAggregateCreditCast,
TmdbCreditCast,
TmdbCreditCrew,
TmdbExternalIds,
TmdbVideo,
TmdbVideoResult,
TmdbWatchProviderDetails,
TmdbWatchProviders,
} from '../api/themoviedb/interfaces';
import { Video } from '../models/Movie';
export interface ProductionCompany {
@ -70,6 +71,20 @@ export interface ExternalIds {
twitterId?: string;
}
export interface WatchProviders {
iso_3166_1: string;
link?: string;
buy?: WatchProviderDetails[];
flatrate?: WatchProviderDetails[];
}
export interface WatchProviderDetails {
displayPriority?: number;
logoPath?: string;
id: number;
name: string;
}
export const mapCast = (person: TmdbCreditCast): Cast => ({
castId: person.cast_id,
character: person.character,
@ -124,7 +139,33 @@ export const mapVideos = (videoResult: TmdbVideoResult): Video[] =>
url: siteUrlCreator(site, key),
}));
export const mapWatchProviders = (watchProvidersResult: {
[iso_3166_1: string]: TmdbWatchProviders;
}): WatchProviders[] =>
Object.entries(watchProvidersResult).map(
([iso_3166_1, provider]) =>
({
iso_3166_1,
link: provider.link,
buy: mapWatchProviderDetails(provider.buy ?? []),
flatrate: mapWatchProviderDetails(provider.flatrate ?? []),
} as WatchProviders)
);
export const mapWatchProviderDetails = (
watchProviderDetails: TmdbWatchProviderDetails[]
): WatchProviderDetails[] =>
watchProviderDetails.map(
(provider) =>
({
displayPriority: provider.display_priority,
logoPath: provider.logo_path,
id: provider.provider_id,
name: provider.provider_name,
} as WatchProviderDetails)
);
const siteUrlCreator = (site: Video['site'], key: string): string =>
({
YouTube: `https://www.youtube.com/watch?v=${key}/`,
YouTube: `https://www.youtube.com/watch?v=${key}`,
}[site]);

@ -350,6 +350,14 @@ requestRoutes.post('/', async (req, res, next) => {
status: 202,
message: 'No seasons available to request',
});
} else if (
quotas.tv.limit &&
finalSeasons.length > (quotas.tv.remaining ?? 0)
) {
return next({
status: 403,
message: 'Series Quota Exceeded',
});
}
await mediaRepository.save(media);

@ -1,3 +1,4 @@
import { truncate } from 'lodash';
import {
EntitySubscriberInterface,
EventSubscriber,
@ -34,7 +35,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: movie.overview,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: entity,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: request,
@ -89,7 +94,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: tv.overview,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
notifyUser: request.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media: entity,

@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap' rel='stylesheet' media='screen')
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
//if mso
xml
o:officedocumentsettings
@ -26,72 +26,37 @@ head
mso-line-height-rule: exactly;
}
style.
@media (max-width: 600px) {
.sm-w-full {
.title:hover * {
text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important;
}
}
div(role='article' aria-roledescription='email' aria-label='' lang='en')
table(style="\
background-color: #f2f4f6;\
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
width: 100%;\
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
div(style='display: block; background-color: #111827;')
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
tr
td(style="text-align: center;")
a(href=applicationUrl)
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
tr
td(style='text-align: center;')
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
span
| An account has been created for you at #{applicationTitle}.
tr
td(align='center')
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
tr
td(align='center' style='\
padding-top: 25px;\
padding-bottom: 25px;\
text-align: center;\
')
a(href=applicationUrl style='\
text-shadow: 0 1px 0 #ffffff;\
font-weight: 700;\
font-size: 24px;\
color: #a8aaaf;\
text-decoration: none;\
')
| #{applicationTitle}
tr
td(style='width: 100%' width='100%')
table.sm-w-full(align='center' style='\
background-color: #ffffff;\
margin-left: auto;\
margin-right: auto;\
width: 570px;\
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
tr
td(style='padding: 45px')
div(style='font-size: 16px; text-align: center; padding-bottom: 14px;')
| Your new password is:
div(style='font-size: 16px; text-align: center')
| #{password}
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
color: #51545e;\
')
a(href=applicationUrl style='color: #3869d4') Open #{applicationTitle}
tr
td
table.sm-w-full(align='center' style='\
margin-left: auto;\
margin-right: auto;\
text-align: center;\
width: 570px;\
' width='570' cellpadding='0' cellspacing='0' role='presentation')
td(style='text-align: center;')
div(style='margin: 1rem 1rem 1rem; font-size: 1.25em;')
span
| Your new password is:
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
span
| #{password}
if applicationUrl
tr
td(align='center' style='font-size: 16px; padding: 45px')
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}
td
a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
| Open #{applicationTitle}

@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap' rel='stylesheet' media='screen')
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
//if mso
xml
o:officedocumentsettings
@ -26,92 +26,56 @@ head
mso-line-height-rule: exactly;
}
style.
@media (max-width: 600px) {
.sm-w-full {
.title:hover * {
text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important;
}
}
div(role='article' aria-roledescription='email' aria-label='' lang='en')
table(style="\
background-color: #f2f4f6;\
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
width: 100%;\
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
div(style='display: block; background-color: #111827;')
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
tr
td(style="text-align: center;")
a(href=applicationUrl)
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
tr
td(style='text-align: center;')
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
span
| #{body}
tr
td(align='center')
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
tr
td(align='center' style='\
padding-top: 25px;\
padding-bottom: 25px;\
text-align: center;\
')
a(href=applicationUrl style='\
text-shadow: 0 1px 0 #ffffff;\
font-weight: 700;\
font-size: 24px;\
color: #a8aaaf;\
text-decoration: none;\
')
| #{applicationTitle}
tr
td(style='width: 100%' width='100%')
table.sm-w-full(align='center' style='\
background-color: #ffffff;\
margin-left: auto;\
margin-right: auto;\
width: 570px;\
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
tr
td(style='padding: 45px')
div(style='font-size: 16px')
| #{body}
br
br
p(style='margin-top: 4px; text-align: center')
b
| #{mediaName}
each extra in mediaExtra
br
| #{extra.name}:&nbsp;
| #{extra.value}
table(align='center' cellpadding='0' cellspacing='0' role='presentation')
tr
td
a(href=actionUrl style='color: #3869d4')
img(src=imageUrl alt='')
p(style='\
font-size: 16px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
color: #51545e;\
')
| Requested by #{requestedBy} at #{timestamp}
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
color: #51545e;\
')
a(href=actionUrl style='color: #3869d4') Open in #{applicationTitle}
tr
td
table.sm-w-full(align='center' style='\
margin-left: auto;\
margin-right: auto;\
text-align: center;\
width: 570px;\
' width='570' cellpadding='0' cellspacing='0' role='presentation')
td
div(style='box-sizing: border-box; margin: 0; width: 100%; color: #fff; border-radius: .75rem; padding: 1rem; border: 1px solid rgba(100, 100, 100, 1); background: linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgb(17, 24, 39) 75%), url(' + imageUrl + ') center 25%/cover')
table(style='color: #fff; width: 100%;')
tr
td(style='vertical-align: top;')
a(href=actionUrl style='display: block; max-width: 20rem; color: #fff; font-weight: 700; text-decoration: none; margin: 0 1rem 0.25rem 0; font-size: 1.3em; line-height: 1.25em; margin-bottom: 5px;' class='title')
span
| #{mediaName}
div(style='overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #d1d5db; font-size: .975em; line-height: 1.45em; padding-top: .25rem; padding-bottom: .25rem;')
span(style='display: block;')
b(style='color: #9ca3af; font-weight: 700;')
| Requested By&nbsp;
| #{requestedBy}
each extra in mediaExtra
span(style='display: block;')
b(style='color: #9ca3af; font-weight: 700;')
| #{extra.name}&nbsp;
| #{extra.value}
td(rowspan='2' style='width: 7rem;')
a(style='display: block; width: 7rem; overflow: hidden; border-radius: .375rem;' href=actionUrl)
div(style='overflow: hidden; box-sizing: border-box; margin: 0px;')
img(alt='' src=imageUrl style='box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;')
tr
td(style='font-size: .85em; color: #9ca3af; line-height: 1em; vertical-align: bottom; margin-right: 1rem')
span
| #{timestamp}
if actionUrl
tr
td(align='center' style='font-size: 16px; padding: 45px')
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}
td
a(href=actionUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
| Open in #{applicationTitle}

@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap' rel='stylesheet' media='screen')
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
//if mso
xml
o:officedocumentsettings
@ -26,74 +26,34 @@ head
mso-line-height-rule: exactly;
}
style.
@media (max-width: 600px) {
.sm-w-full {
.title:hover * {
text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important;
}
}
div(role='article' aria-roledescription='email' aria-label='' lang='en')
table(style="\
background-color: #f2f4f6;\
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
width: 100%;\
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
div(style='display: block; background-color: #111827;')
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
tr
td(style="text-align: center;")
a(href=applicationUrl)
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
tr
td(align='center')
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
tr
td(align='center' style='\
padding-top: 25px;\
padding-bottom: 25px;\
text-align: center;\
')
a(href=applicationUrl style='\
text-shadow: 0 1px 0 #ffffff;\
font-weight: 700;\
font-size: 24px;\
color: #a8aaaf;\
text-decoration: none;\
')
| #{applicationTitle}
tr
td(style='width: 100%' width='100%')
table.sm-w-full(align='center' style='\
background-color: #ffffff;\
margin-left: auto;\
margin-right: auto;\
width: 570px;\
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
tr
td(style='padding: 45px')
div(style='font-size: 16px; text-align: center; padding-bottom: 14px;')
| A request to reset the password was made. Click
a(href=applicationUrl style='color: #3869d4; padding: 0px 5px;') here
| to set a new password.
div(style='font-size: 16px; text-align: center; padding-bottom: 14px;')
| If you did not request this recovery link you can safely ignore this email.
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
color: #51545e;\
')
a(href=applicationUrl style='color: #3869d4') Open #{applicationTitle}
tr
td
table.sm-w-full(align='center' style='\
margin-left: auto;\
margin-right: auto;\
text-align: center;\
width: 570px;\
' width='570' cellpadding='0' cellspacing='0' role='presentation')
td(style='text-align: center;')
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
span
| Your #{applicationTitle} account password was requested to be reset. Click below to reset your password.
if resetPasswordLink
tr
td(align='center' style='font-size: 16px; padding: 45px')
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}.
td
a(href=resetPasswordLink style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
| Reset Password
tr
td(style='text-align: center;')
div(style='margin: 1rem; font-size: .85em;')
span
| If you did not request that your password be reset, you can safely ignore this email.

@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap' rel='stylesheet' media='screen')
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
//if mso
xml
o:officedocumentsettings
@ -26,70 +26,29 @@ head
mso-line-height-rule: exactly;
}
style.
@media (max-width: 600px) {
.sm-w-full {
.title:hover * {
text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important;
}
}
div(role='article' aria-roledescription='email' aria-label='' lang='en')
table(style="\
background-color: #f2f4f6;\
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
width: 100%;\
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
div(style='display: block; background-color: #111827;')
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
tr
td(style="text-align: center;")
a(href=applicationUrl)
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
tr
td(align='center')
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
tr
td(align='center' style='\
padding-top: 25px;\
padding-bottom: 25px;\
text-align: center;\
')
a(href=applicationUrl style='\
text-shadow: 0 1px 0 #ffffff;\
font-weight: 700;\
font-size: 24px;\
color: #a8aaaf;\
text-decoration: none;\
')
| #{applicationTitle}
tr
td(style='width: 100%' width='100%')
table.sm-w-full(align='center' style='\
background-color: #ffffff;\
margin-left: auto;\
margin-right: auto;\
width: 570px;\
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
tr
td(style='padding: 45px')
div(style='font-size: 16px')
| #{body}
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
color: #51545e;\
')
a(href=applicationUrl style='color: #3869d4') Open #{applicationTitle}
tr
td
table.sm-w-full(align='center' style='\
margin-left: auto;\
margin-right: auto;\
text-align: center;\
width: 570px;\
' width='570' cellpadding='0' cellspacing='0' role='presentation')
td(style='text-align: center;')
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
span
| #{body}
if applicationUrl
tr
td(align='center' style='font-size: 16px; padding: 45px')
p(style='\
font-size: 13px;\
line-height: 24px;\
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}
td
a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
| Open #{applicationTitle}

@ -11,7 +11,7 @@ confinement: strict
parts:
overseerr:
plugin: nodejs
nodejs-version: '14.16.1'
nodejs-version: '14.17.0'
nodejs-package-manager: 'yarn'
nodejs-yarn-version: v1.22.10
build-packages:

@ -1 +1 @@
<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>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="57 57 593.71 593.71"><g transform="translate(55.201 55.147)"><circle cx="216.31" cy="152.08" r="296.86" fill="currentColor" transform="rotate(132.42 225.16 243.54)"/><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: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -1 +1 @@
<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>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320.03 103.61"><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 offset=".17" stop-color="#f9be03"/><stop offset=".51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><polygon points="320.03 -.09 289.96 -.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79" class="cls-1"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" 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" class="cls-3"/><path d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z" class="cls-1"/><path d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z" class="cls-1"/><path 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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -13,7 +13,7 @@ interface AlertProps {
const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
let design = {
bgColor: 'bg-yellow-600',
titleColor: 'text-yellow-200',
titleColor: 'text-yellow-100',
textColor: 'text-yellow-300',
svg: <ExclamationIcon className="w-5 h-5" />,
};
@ -22,7 +22,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
case 'info':
design = {
bgColor: 'bg-indigo-600',
titleColor: 'text-indigo-200',
titleColor: 'text-indigo-100',
textColor: 'text-indigo-300',
svg: <InformationCircleIcon className="w-5 h-5" />,
};
@ -30,7 +30,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
case 'error':
design = {
bgColor: 'bg-red-600',
titleColor: 'text-red-200',
titleColor: 'text-red-100',
textColor: 'text-red-300',
svg: <XCircleIcon className="w-5 h-5" />,
};

@ -45,7 +45,7 @@ function Button<P extends ElementTypes = 'button'>(
ref?: React.Ref<Element<P>>
): JSX.Element {
const buttonStyle = [
'inline-flex items-center justify-center border border-transparent leading-5 font-medium rounded-md focus:outline-none transition ease-in-out duration-150 cursor-pointer disabled:opacity-50',
'inline-flex items-center justify-center border border-transparent leading-5 font-medium rounded-md focus:outline-none transition ease-in-out duration-150 cursor-pointer disabled:opacity-50 whitespace-nowrap',
];
switch (buttonType) {
case 'primary':

@ -10,7 +10,7 @@ const ListItem: React.FC<ListItemProps> = ({ title, className, children }) => {
return (
<div>
<div className="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt className="block text-sm font-medium text-gray-400">{title}</dt>
<dt className="block text-sm font-bold text-gray-400">{title}</dt>
<dd className="flex text-sm text-white sm:mt-0 sm:col-span-2">
<span className={`flex-grow ${className}`}>{children}</span>
</dd>

@ -137,7 +137,7 @@ const Modal: React.FC<ModalProps> = ({
>
{title && (
<span
className="text-lg font-medium leading-6 text-white"
className="text-lg font-bold leading-6 text-white"
id="modal-headline"
>
{title}

@ -46,7 +46,7 @@ const SettingsLink: React.FC<{
if (tabType === 'button') {
linkClasses =
'px-3 py-2 ml-8 text-sm font-medium transition duration-300 rounded-md whitespace-nowrap first:ml-0';
'px-3 py-2 text-sm font-medium transition duration-300 rounded-md whitespace-nowrap mx-2 my-1';
activeLinkColor = 'bg-indigo-700';
inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700';
}
@ -119,8 +119,8 @@ const SettingsTabs: React.FC<{
</select>
</div>
{tabType === 'button' ? (
<div className="hidden overflow-x-scroll overflow-y-hidden sm:block hide-scrollbar">
<nav className="flex space-x-4" aria-label="Tabs">
<div className="hidden sm:block">
<nav className="flex flex-wrap -mx-2 -my-1" aria-label="Tabs">
{settingsRoutes.map((route, index) => (
<SettingsLink
tabType={tabType}

@ -73,7 +73,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
<div className="flex flex-col h-full overflow-y-scroll bg-gray-700 shadow-xl">
<header className="px-4 space-y-1 bg-indigo-600 slideover">
<div className="flex items-center justify-between space-x-3">
<h2 className="text-lg font-medium leading-7 text-white">
<h2 className="text-lg font-bold leading-7 text-white">
{title}
</h2>
<div className="flex items-center h-7">

@ -13,7 +13,7 @@ const TH: React.FC<AllHTMLAttributes<HTMLTableHeaderCellElement>> = ({
...props
}) => {
const style = [
'px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider',
'px-4 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider truncate',
];
if (className) {
@ -39,7 +39,7 @@ const TD: React.FC<TDProps> = ({
className,
...props
}) => {
const style = ['whitespace-nowrap text-sm leading-5 text-white'];
const style = ['text-sm leading-5 text-white'];
switch (alignText) {
case 'left':
@ -54,7 +54,7 @@ const TD: React.FC<TDProps> = ({
}
if (!noPadding) {
style.push('px-6 py-4');
style.push('px-4 py-4');
}
if (className) {
@ -73,7 +73,7 @@ const Table: React.FC = ({ children }) => {
<div className="flex flex-col">
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
<div className="inline-block min-w-full py-2 align-middle">
<div className="overflow-hidden shadow sm:rounded-lg">
<div className="overflow-hidden rounded-lg shadow md:mx-0 lg:mx-0">
<table className="min-w-full">{children}</table>
</div>
</div>

@ -10,7 +10,7 @@ import useLocale from '../../../hooks/useLocale';
import Transition from '../../Transition';
const messages = defineMessages({
changelanguage: 'Change Language',
displaylanguage: 'Display Language',
});
const LanguagePicker: React.FC = () => {
@ -50,9 +50,9 @@ const LanguagePicker: React.FC = () => {
<div>
<label
htmlFor="language"
className="block pb-2 text-sm font-medium leading-5 text-gray-300"
className="block pb-2 text-sm font-bold leading-5 text-gray-300"
>
{intl.formatMessage(messages.changelanguage)}
{intl.formatMessage(messages.displaylanguage)}
</label>
<select
id="language"

@ -13,7 +13,7 @@ const SearchInput: React.FC = () => {
const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput();
return (
<div className="flex flex-1">
<div className="flex w-full md:ml-0">
<div className="flex w-full">
<label htmlFor="search_field" className="sr-only">
Search
</label>
@ -24,7 +24,7 @@ const SearchInput: React.FC = () => {
<input
id="search_field"
style={{ paddingRight: searchValue.length > 0 ? '1.75rem' : '' }}
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"
className="block w-full py-2 pl-10 text-white placeholder-gray-300 bg-gray-900 border border-gray-600 rounded-full bg-opacity-80 focus:bg-opacity-100 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"

@ -2,9 +2,9 @@ import {
ClockIcon,
CogIcon,
SparklesIcon,
UsersIcon,
XIcon,
} from '@heroicons/react/outline';
import { UsersIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { ReactNode, useRef } from 'react';
@ -39,34 +39,26 @@ const SidebarLinks: SidebarLinkProps[] = [
{
href: '/',
messagesKey: 'dashboard',
svgIcon: (
<SparklesIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
),
svgIcon: <SparklesIcon className="w-6 h-6 mr-3" />,
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
},
{
href: '/requests',
messagesKey: 'requests',
svgIcon: (
<ClockIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
),
svgIcon: <ClockIcon className="w-6 h-6 mr-3" />,
activeRegExp: /^\/requests/,
},
{
href: '/users',
messagesKey: 'users',
svgIcon: (
<UsersIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
),
svgIcon: <UsersIcon className="w-6 h-6 mr-3" />,
activeRegExp: /^\/users/,
requiredPermission: Permission.MANAGE_USERS,
},
{
href: '/settings',
messagesKey: 'settings',
svgIcon: (
<CogIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
),
svgIcon: <CogIcon className="w-6 h-6 mr-3" />,
activeRegExp: /^\/settings/,
requiredPermission: Permission.MANAGE_SETTINGS,
},
@ -81,7 +73,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
return (
<>
<div className="md:hidden">
<div className="lg:hidden">
<Transition show={open}>
<div className="fixed inset-0 z-40 flex">
<Transition
@ -93,7 +85,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
leaveTo="opacity-0"
>
<div className="fixed inset-0">
<div className="absolute inset-0 bg-gray-600 opacity-75"></div>
<div className="absolute inset-0 bg-gray-900 opacity-90"></div>
</div>
</Transition>
<Transition
@ -117,16 +109,16 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
</div>
<div
ref={navRef}
className="flex flex-col flex-1 h-0 pt-5 pb-8 overflow-y-auto sm:pb-4"
className="flex flex-col flex-1 h-0 pt-8 pb-8 overflow-y-auto sm:pb-4"
>
<div className="flex items-center flex-shrink-0 px-4">
<span className="text-xl text-gray-50">
<div className="flex items-center flex-shrink-0 px-2">
<span className="px-4 text-xl text-gray-50">
<a href="/">
<img src="/logo.png" alt="Logo" />
<img src="/logo_full.svg" alt="Logo" />
</a>
</span>
</div>
<nav className="flex-1 px-2 mt-5 space-y-1">
<nav className="flex-1 px-4 mt-16 space-y-4">
{SidebarLinks.filter((link) =>
link.requiredPermission
? hasPermission(link.requiredPermission)
@ -147,13 +139,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
}}
role="button"
tabIndex={0}
className={`flex items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white focus:outline-none focus:bg-gray-700 transition ease-in-out duration-150
className={`flex items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white focus:outline-none transition ease-in-out duration-150
${
router.pathname.match(
sidebarLink.activeRegExp
)
? 'bg-gray-900'
: ''
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
>
@ -167,7 +159,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
})}
</nav>
{hasPermission(Permission.ADMIN) && (
<VersionStatus onClick={() => setClosed()} />
<div className="px-2">
<VersionStatus onClick={() => setClosed()} />
</div>
)}
</div>
</div>
@ -180,18 +174,18 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
</Transition>
</div>
<div className="fixed top-0 bottom-0 left-0 hidden md:flex md:flex-shrink-0">
<div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0">
<div className="flex flex-col w-64 sidebar">
<div className="flex flex-col flex-1 h-0 bg-gray-800">
<div className="flex flex-col flex-1 pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<span className="text-2xl text-gray-50">
<div className="flex flex-col flex-1 h-0">
<div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0">
<span className="px-4 text-2xl text-gray-50">
<a href="/">
<img src="/logo.png" alt="Logo" />
<img src="/logo_full.svg" alt="Logo" />
</a>
</span>
</div>
<nav className="flex-1 px-2 mt-5 space-y-1 bg-gray-800">
<nav className="flex-1 px-4 mt-16 space-y-4">
{SidebarLinks.filter((link) =>
link.requiredPermission
? hasPermission(link.requiredPermission)
@ -204,13 +198,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
as={sidebarLink.as}
>
<a
className={`flex group items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white hover:text-gray-100 hover:bg-gray-700 focus:outline-none focus:bg-gray-700 transition ease-in-out duration-150
className={`flex group items-center px-2 py-2 text-lg leading-6 font-medium rounded-md text-white focus:outline-none transition ease-in-out duration-150
${
router.pathname.match(
sidebarLink.activeRegExp
)
? 'bg-gray-900'
: ''
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
>
@ -221,7 +215,11 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
);
})}
</nav>
{hasPermission(Permission.ADMIN) && <VersionStatus />}
{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus />
</div>
)}
</div>
</div>
</div>

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

Loading…
Cancel
Save