Merge branch 'develop'
@@ -503,6 +503,42 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"translation"
|
"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>",
|
"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
|
docs
|
||||||
LICENSE
|
LICENSE
|
||||||
node_modules
|
node_modules
|
||||||
public/os_logo_square.png
|
public/os_logo_filled.png
|
||||||
public/preview.jpg
|
public/preview.jpg
|
||||||
snap
|
snap
|
||||||
stylelint.config.js
|
stylelint.config.js
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Lint & Test Build
|
name: Lint & Test Build
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
container: node:14.16-alpine
|
container: node:14.17-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2.3.4
|
uses: actions/checkout@v2.3.4
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Lint & Test Build
|
name: Lint & Test Build
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
container: node:14.16-alpine
|
container: node:14.17-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2.3.4
|
uses: actions/checkout@v2.3.4
|
||||||
|
|||||||
2
.github/workflows/snap.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
name: Lint & Test Build
|
name: Lint & Test Build
|
||||||
needs: jobs
|
needs: jobs
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
container: node:14.16-alpine
|
container: node:14.17-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2.3.4
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ RUN touch config/DOCKER
|
|||||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||||
|
|
||||||
|
|
||||||
FROM node:14.16-alpine
|
FROM node:14.17-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:14.16-alpine
|
FROM node:14.17-alpine
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
10
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<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>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" />
|
<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://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>
|
<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 -->
|
<!-- 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 -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
</p>
|
</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/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/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/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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,8 @@ location ^~ /overseerr {
|
|||||||
sub_filter '/android-' '/$app/android-';
|
sub_filter '/android-' '/$app/android-';
|
||||||
sub_filter '/apple-' '/$app/apple-';
|
sub_filter '/apple-' '/$app/apple-';
|
||||||
sub_filter '/favicon' '/$app/favicon';
|
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';
|
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
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name overseerr \
|
--name overseerr \
|
||||||
-e LOG_LEVEL=info \
|
-e LOG_LEVEL=debug \
|
||||||
-e TZ=Asia/Tokyo \
|
-e TZ=Asia/Tokyo \
|
||||||
-p 5055:5055 \
|
-p 5055:5055 \
|
||||||
-v /path/to/appdata/config:/app/config \
|
-v /path/to/appdata/config:/app/config \
|
||||||
@@ -39,7 +39,7 @@ services:
|
|||||||
image: sctx/overseerr:latest
|
image: sctx/overseerr:latest
|
||||||
container_name: overseerr
|
container_name: overseerr
|
||||||
environment:
|
environment:
|
||||||
- LOG_LEVEL=info
|
- LOG_LEVEL=debug
|
||||||
- TZ=Asia/Tokyo
|
- TZ=Asia/Tokyo
|
||||||
ports:
|
ports:
|
||||||
- 5055:5055
|
- 5055:5055
|
||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name overseerr \
|
--name overseerr \
|
||||||
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
|
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
|
||||||
-e LOG_LEVEL=info \
|
-e LOG_LEVEL=debug \
|
||||||
-e TZ=Asia/Tokyo \
|
-e TZ=Asia/Tokyo \
|
||||||
-p 5055:5055 \
|
-p 5055:5055 \
|
||||||
-v /path/to/appdata/config:/app/config \
|
-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
|
## 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" %}
|
{% 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 %}
|
{% endhint %}
|
||||||
|
|
||||||
|
First, create a volume to store the configuration data for Overseerr using using either the Docker CLI:
|
||||||
|
|
||||||
```bash
|
```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 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=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" %}
|
{% 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.
|
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.)
|
**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 %}
|
{% endhint %}
|
||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ Try to answer the following questions:
|
|||||||
|
|
||||||
## How can I share my logs?
|
## 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.
|
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).
|
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.
|
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
|
### SMTP Username & Password
|
||||||
|
|
||||||
|
|||||||
1
next-env.d.ts
vendored
@@ -1,2 +1,3 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/types/global" />
|
/// <reference types="next/types/global" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ module.exports = {
|
|||||||
images: {
|
images: {
|
||||||
domains: ['image.tmdb.org'],
|
domains: ['image.tmdb.org'],
|
||||||
},
|
},
|
||||||
future: {
|
|
||||||
webpack5: true,
|
|
||||||
},
|
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
|
|||||||
@@ -52,9 +52,15 @@ components:
|
|||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
example: 'hey@itsme.com'
|
example: 'hey@itsme.com'
|
||||||
|
readOnly: true
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
plexToken:
|
plexToken:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
plexUsername:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
userType:
|
userType:
|
||||||
type: integer
|
type: integer
|
||||||
example: 1
|
example: 1
|
||||||
@@ -77,13 +83,6 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
example: 5
|
example: 5
|
||||||
readOnly: true
|
readOnly: true
|
||||||
requests:
|
|
||||||
type: array
|
|
||||||
readOnly: true
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/MediaRequest'
|
|
||||||
settings:
|
|
||||||
$ref: '#/components/schemas/UserSettings'
|
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- email
|
- email
|
||||||
@@ -92,11 +91,11 @@ components:
|
|||||||
UserSettings:
|
UserSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
discordId:
|
locale:
|
||||||
type: string
|
type: string
|
||||||
region:
|
region:
|
||||||
type: string
|
type: string
|
||||||
language:
|
originalLanguage:
|
||||||
type: string
|
type: string
|
||||||
MainSettings:
|
MainSettings:
|
||||||
type: object
|
type: object
|
||||||
@@ -398,7 +397,6 @@ components:
|
|||||||
activeLanguageProfileId:
|
activeLanguageProfileId:
|
||||||
type: number
|
type: number
|
||||||
example: 1
|
example: 1
|
||||||
nullable: true
|
|
||||||
activeAnimeProfileId:
|
activeAnimeProfileId:
|
||||||
type: number
|
type: number
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -408,6 +406,7 @@ components:
|
|||||||
activeAnimeProfileName:
|
activeAnimeProfileName:
|
||||||
type: string
|
type: string
|
||||||
example: 720p/1080p
|
example: 720p/1080p
|
||||||
|
nullable: true
|
||||||
activeAnimeDirectory:
|
activeAnimeDirectory:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -769,6 +768,10 @@ components:
|
|||||||
$ref: '#/components/schemas/ExternalIds'
|
$ref: '#/components/schemas/ExternalIds'
|
||||||
mediaInfo:
|
mediaInfo:
|
||||||
$ref: '#/components/schemas/MediaInfo'
|
$ref: '#/components/schemas/MediaInfo'
|
||||||
|
watchProviders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/WatchProviders'
|
||||||
Episode:
|
Episode:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -943,6 +946,10 @@ components:
|
|||||||
$ref: '#/components/schemas/Keyword'
|
$ref: '#/components/schemas/Keyword'
|
||||||
mediaInfo:
|
mediaInfo:
|
||||||
$ref: '#/components/schemas/MediaInfo'
|
$ref: '#/components/schemas/MediaInfo'
|
||||||
|
watchProviders:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/WatchProviders'
|
||||||
MediaRequest:
|
MediaRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -953,7 +960,7 @@ components:
|
|||||||
status:
|
status:
|
||||||
type: number
|
type: number
|
||||||
example: 0
|
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
|
readOnly: true
|
||||||
media:
|
media:
|
||||||
$ref: '#/components/schemas/MediaInfo'
|
$ref: '#/components/schemas/MediaInfo'
|
||||||
@@ -1587,9 +1594,8 @@ components:
|
|||||||
UserSettingsNotifications:
|
UserSettingsNotifications:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
notificationAgents:
|
notificationTypes:
|
||||||
type: number
|
$ref: '#/components/schemas/NotificationAgentTypes'
|
||||||
example: 0
|
|
||||||
emailEnabled:
|
emailEnabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
pgpKey:
|
pgpKey:
|
||||||
@@ -1614,6 +1620,52 @@ components:
|
|||||||
telegramSendSilently:
|
telegramSendSilently:
|
||||||
type: boolean
|
type: boolean
|
||||||
nullable: true
|
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:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
@@ -1842,8 +1894,8 @@ paths:
|
|||||||
$ref: '#/components/schemas/PlexLibrary'
|
$ref: '#/components/schemas/PlexLibrary'
|
||||||
/settings/plex/devices/servers:
|
/settings/plex/devices/servers:
|
||||||
get:
|
get:
|
||||||
summary: Gets the user's available plex servers
|
summary: Gets the user's available Plex servers
|
||||||
description: Returns a list of available plex servers and their connectivity state
|
description: Returns a list of available Plex servers and their connectivity state
|
||||||
tags:
|
tags:
|
||||||
- settings
|
- settings
|
||||||
responses:
|
responses:
|
||||||
@@ -2980,7 +3032,15 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/User'
|
type: object
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
example: 'hey@itsme.com'
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
permissions:
|
||||||
|
type: number
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: The created user
|
description: The created user
|
||||||
@@ -2991,7 +3051,7 @@ paths:
|
|||||||
put:
|
put:
|
||||||
summary: Update batch of users
|
summary: Update batch of users
|
||||||
description: |
|
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.
|
Requires the `MANAGE_USERS` permission.
|
||||||
tags:
|
tags:
|
||||||
@@ -3018,7 +3078,6 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/User'
|
$ref: '#/components/schemas/User'
|
||||||
|
|
||||||
/user/import-from-plex:
|
/user/import-from-plex:
|
||||||
post:
|
post:
|
||||||
summary: Import all users from Plex
|
summary: Import all users from Plex
|
||||||
@@ -3067,7 +3126,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: Get user by ID
|
summary: Get user by ID
|
||||||
description: |
|
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:
|
tags:
|
||||||
- users
|
- users
|
||||||
parameters:
|
parameters:
|
||||||
@@ -4180,6 +4239,9 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
languageProfileId:
|
languageProfileId:
|
||||||
type: number
|
type: number
|
||||||
|
userId:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- mediaType
|
- mediaType
|
||||||
- mediaId
|
- mediaId
|
||||||
|
|||||||
47
package.json
@@ -17,11 +17,11 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.2.0",
|
"@headlessui/react": "^1.3.0",
|
||||||
"@heroicons/react": "^1.0.1",
|
"@heroicons/react": "^1.0.1",
|
||||||
"@supercharge/request-ip": "^1.1.2",
|
"@supercharge/request-ip": "^1.1.2",
|
||||||
"@svgr/webpack": "^5.5.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"@tanem/react-nprogress": "^3.0.67",
|
"@tanem/react-nprogress": "^3.0.70",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
@@ -33,18 +33,18 @@
|
|||||||
"csurf": "^1.11.0",
|
"csurf": "^1.11.0",
|
||||||
"email-templates": "^8.0.7",
|
"email-templates": "^8.0.7",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-openapi-validator": "^4.12.11",
|
"express-openapi-validator": "^4.12.14",
|
||||||
"express-rate-limit": "^5.2.6",
|
"express-rate-limit": "^5.2.6",
|
||||||
"express-session": "^1.17.2",
|
"express-session": "^1.17.2",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"gravatar-url": "3.1.0",
|
"gravatar-url": "3.1.0",
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"next": "10.1.3",
|
"next": "11.0.1",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"node-schedule": "^2.0.0",
|
"node-schedule": "^2.0.0",
|
||||||
"nodemailer": "^6.6.1",
|
"nodemailer": "^6.6.2",
|
||||||
"openpgp": "^5.0.0-2",
|
"openpgp": "^5.0.0-3",
|
||||||
"plex-api": "^5.3.1",
|
"plex-api": "^5.3.1",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"react-animate-height": "^2.0.23",
|
"react-animate-height": "^2.0.23",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-intersection-observer": "^8.32.0",
|
"react-intersection-observer": "^8.32.0",
|
||||||
"react-intl": "5.19.0",
|
"react-intl": "5.20.3",
|
||||||
"react-markdown": "^6.0.2",
|
"react-markdown": "^6.0.2",
|
||||||
"react-select": "^4.3.1",
|
"react-select": "^4.3.1",
|
||||||
"react-spring": "^9.2.3",
|
"react-spring": "^9.2.3",
|
||||||
@@ -61,12 +61,11 @@
|
|||||||
"react-truncate-markup": "^5.1.0",
|
"react-truncate-markup": "^5.1.0",
|
||||||
"react-use-clipboard": "1.0.7",
|
"react-use-clipboard": "1.0.7",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"secure-random-password": "^0.2.2",
|
"secure-random-password": "^0.2.3",
|
||||||
"sqlite3": "^5.0.2",
|
"sqlite3": "^5.0.2",
|
||||||
"swagger-ui-express": "^4.1.6",
|
"swagger-ui-express": "^4.1.6",
|
||||||
"swr": "^0.5.6",
|
"swr": "^0.5.6",
|
||||||
"typeorm": "0.2.32",
|
"typeorm": "0.2.32",
|
||||||
"uuid": "^8.3.2",
|
|
||||||
"web-push": "^3.4.4",
|
"web-push": "^3.4.4",
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"winston-daily-rotate-file": "^4.5.5",
|
"winston-daily-rotate-file": "^4.5.5",
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
"yup": "^0.32.9"
|
"yup": "^0.32.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.14.3",
|
"@babel/cli": "^7.14.5",
|
||||||
"@commitlint/cli": "^12.1.4",
|
"@commitlint/cli": "^12.1.4",
|
||||||
"@commitlint/config-conventional": "^12.1.4",
|
"@commitlint/config-conventional": "^12.1.4",
|
||||||
"@semantic-release/changelog": "^5.0.1",
|
"@semantic-release/changelog": "^5.0.1",
|
||||||
@@ -91,35 +90,35 @@
|
|||||||
"@types/csurf": "^1.11.1",
|
"@types/csurf": "^1.11.1",
|
||||||
"@types/email-templates": "^8.0.3",
|
"@types/email-templates": "^8.0.3",
|
||||||
"@types/express": "^4.17.12",
|
"@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/express-session": "^1.17.3",
|
||||||
"@types/lodash": "^4.14.170",
|
"@types/lodash": "^4.14.170",
|
||||||
"@types/node": "^15.6.1",
|
"@types/node": "^15.6.1",
|
||||||
"@types/node-schedule": "^1.3.1",
|
"@types/node-schedule": "^1.3.1",
|
||||||
"@types/nodemailer": "^6.4.2",
|
"@types/nodemailer": "^6.4.2",
|
||||||
"@types/react": "^17.0.9",
|
"@types/react": "^17.0.11",
|
||||||
"@types/react-dom": "^17.0.6",
|
"@types/react-dom": "^17.0.8",
|
||||||
"@types/react-select": "^4.0.15",
|
"@types/react-select": "^4.0.15",
|
||||||
"@types/react-toast-notifications": "^2.4.1",
|
"@types/react-toast-notifications": "^2.4.1",
|
||||||
"@types/react-transition-group": "^4.4.1",
|
"@types/react-transition-group": "^4.4.1",
|
||||||
"@types/secure-random-password": "^0.2.0",
|
"@types/secure-random-password": "^0.2.0",
|
||||||
"@types/swagger-ui-express": "^4.1.2",
|
"@types/swagger-ui-express": "^4.1.2",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/web-push": "^3.3.1",
|
||||||
"@types/web-push": "^3.3.0",
|
|
||||||
"@types/xml2js": "^0.4.8",
|
"@types/xml2js": "^0.4.8",
|
||||||
"@types/yamljs": "^0.2.31",
|
"@types/yamljs": "^0.2.31",
|
||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||||
"@typescript-eslint/parser": "^4.26.0",
|
"@typescript-eslint/parser": "^4.28.0",
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
"babel-plugin-react-intl": "^8.2.25",
|
"babel-plugin-react-intl": "^8.2.25",
|
||||||
"babel-plugin-react-intl-auto": "^3.3.0",
|
"babel-plugin-react-intl-auto": "^3.3.0",
|
||||||
"commitizen": "^4.2.4",
|
"commitizen": "^4.2.4",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"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-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-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
@@ -128,13 +127,13 @@
|
|||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"postcss": "^8.3.0",
|
"postcss": "^8.3.5",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"semantic-release": "^17.4.3",
|
"semantic-release": "^17.4.4",
|
||||||
"semantic-release-docker-buildx": "^1.0.1",
|
"semantic-release-docker-buildx": "^1.0.1",
|
||||||
"tailwindcss": "^2.1.4",
|
"tailwindcss": "^2.2.2",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^4.3.2"
|
"typescript": "^4.3.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"sqlite3/node-gyp": "^5.1.0"
|
"sqlite3/node-gyp": "^5.1.0"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 774 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
public/logo.png
|
Before Width: | Height: | Size: 64 KiB |
BIN
public/logo_full.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
1
public/logo_full.svg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
1
public/logo_stacked.svg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
@@ -31,7 +31,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>You are offline</h1>
|
<h1>You are offline</h1>
|
||||||
|
|
||||||
<button type="button">⤾ Reload</button>
|
<button type="button">↻ Reload</button>
|
||||||
|
|
||||||
<!-- Inline the page's JavaScript file. -->
|
<!-- Inline the page's JavaScript file. -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
1
public/os_icon.svg
Normal file
@@ -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 |
BIN
public/os_logo_filled.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -142,7 +142,7 @@ class PlexAPI {
|
|||||||
`/library/sections/${id}/all`
|
`/library/sections/${id}/all`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.MediaContainer.Metadata;
|
return response.MediaContainer.Metadata ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMetadata(
|
public async getMetadata(
|
||||||
|
|||||||
@@ -1,39 +1,28 @@
|
|||||||
import cacheManager from '../lib/cache';
|
import cacheManager from '../lib/cache';
|
||||||
import ExternalAPI from './externalapi';
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface RTMovieOldSearchResult {
|
interface RTSearchResult {
|
||||||
id: number;
|
meterClass: 'certified_fresh' | 'fresh' | 'rotten';
|
||||||
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 RTTvSearchResult {
|
|
||||||
title: string;
|
|
||||||
meterClass: 'fresh' | 'rotten';
|
|
||||||
meterScore: number;
|
meterScore: number;
|
||||||
url: string;
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RTTvSearchResult extends RTSearchResult {
|
||||||
|
title: string;
|
||||||
startYear: number;
|
startYear: number;
|
||||||
endYear: number;
|
endYear: number;
|
||||||
}
|
}
|
||||||
|
interface RTMovieSearchResult extends RTSearchResult {
|
||||||
interface RTMovieSearchResponse {
|
name: string;
|
||||||
total: number;
|
url: string;
|
||||||
movies: RTMovieOldSearchResult[];
|
year: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RTMultiSearchResponse {
|
interface RTMultiSearchResponse {
|
||||||
tvCount: number;
|
tvCount: number;
|
||||||
tvSeries: RTTvSearchResult[];
|
tvSeries: RTTvSearchResult[];
|
||||||
|
movieCount: number;
|
||||||
|
movies: RTMovieSearchResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RTRating {
|
export interface RTRating {
|
||||||
@@ -88,19 +77,19 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
year: number
|
year: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
|
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
||||||
params: { q: name },
|
params: { q: name, limit: 10 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// First, attempt to match exact name and year
|
// First, attempt to match exact name and year
|
||||||
let movie = data.movies.find(
|
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 we don't find a movie, try to match partial name and year
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = data.movies.find(
|
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
|
// One last try, try exact name match only
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = data.movies.find((movie) => movie.title === name);
|
movie = data.movies.find((movie) => movie.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
@@ -119,12 +108,15 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: movie.title,
|
title: movie.name,
|
||||||
url: movie.links.alternate,
|
url: movie.url,
|
||||||
criticsRating: movie.ratings.critics_rating,
|
criticsRating:
|
||||||
criticsScore: movie.ratings.critics_score,
|
movie.meterClass === 'certified_fresh'
|
||||||
audienceRating: movie.ratings.audience_rating,
|
? 'Certified Fresh'
|
||||||
audienceScore: movie.ratings.audience_score,
|
: movie.meterClass === 'fresh'
|
||||||
|
? 'Fresh'
|
||||||
|
: 'Rotten',
|
||||||
|
criticsScore: movie.meterScore,
|
||||||
year: movie.year,
|
year: movie.year,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
language,
|
language,
|
||||||
append_to_response: 'credits,external_ids,videos,release_dates',
|
append_to_response:
|
||||||
|
'credits,external_ids,videos,release_dates,watch/providers',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
@@ -196,7 +197,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
params: {
|
params: {
|
||||||
language,
|
language,
|
||||||
append_to_response:
|
append_to_response:
|
||||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings',
|
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ export interface TmdbMovieDetails {
|
|||||||
};
|
};
|
||||||
external_ids: TmdbExternalIds;
|
external_ids: TmdbExternalIds;
|
||||||
videos: TmdbVideoResult;
|
videos: TmdbVideoResult;
|
||||||
|
'watch/providers'?: {
|
||||||
|
id: number;
|
||||||
|
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbVideo {
|
export interface TmdbVideo {
|
||||||
@@ -269,6 +273,10 @@ export interface TmdbTvDetails {
|
|||||||
results: TmdbKeyword[];
|
results: TmdbKeyword[];
|
||||||
};
|
};
|
||||||
videos: TmdbVideoResult;
|
videos: TmdbVideoResult;
|
||||||
|
'watch/providers'?: {
|
||||||
|
id: number;
|
||||||
|
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbVideoResult {
|
export interface TmdbVideoResult {
|
||||||
@@ -401,3 +409,16 @@ export interface TmdbNetwork {
|
|||||||
logo_path?: string;
|
logo_path?: string;
|
||||||
origin_country?: 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 {
|
import {
|
||||||
AfterInsert,
|
AfterInsert,
|
||||||
AfterRemove,
|
AfterRemove,
|
||||||
@@ -145,7 +145,11 @@ export class MediaRequest {
|
|||||||
subject: `${movie.title}${
|
subject: `${movie.title}${
|
||||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
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}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
media,
|
media,
|
||||||
request: this,
|
request: this,
|
||||||
@@ -158,7 +162,11 @@ export class MediaRequest {
|
|||||||
subject: `${tv.name}${
|
subject: `${tv.name}${
|
||||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
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}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||||
media,
|
media,
|
||||||
extra: [
|
extra: [
|
||||||
@@ -217,7 +225,11 @@ export class MediaRequest {
|
|||||||
subject: `${movie.title}${
|
subject: `${movie.title}${
|
||||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
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}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
notifyUser: autoApproved ? undefined : this.requestedBy,
|
notifyUser: autoApproved ? undefined : this.requestedBy,
|
||||||
media,
|
media,
|
||||||
@@ -236,7 +248,11 @@ export class MediaRequest {
|
|||||||
subject: `${tv.name}${
|
subject: `${tv.name}${
|
||||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
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}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||||
notifyUser: autoApproved ? undefined : this.requestedBy,
|
notifyUser: autoApproved ? undefined : this.requestedBy,
|
||||||
media,
|
media,
|
||||||
@@ -495,7 +511,11 @@ export class MediaRequest {
|
|||||||
subject: `${movie.title}${
|
subject: `${movie.title}${
|
||||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||||
}`,
|
}`,
|
||||||
message: movie.overview,
|
message: truncate(movie.overview, {
|
||||||
|
length: 500,
|
||||||
|
separator: /\s/,
|
||||||
|
omission: '…',
|
||||||
|
}),
|
||||||
media,
|
media,
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
request: this,
|
request: this,
|
||||||
@@ -707,7 +727,11 @@ export class MediaRequest {
|
|||||||
? ` (${series.first_air_date.slice(0, 4)})`
|
? ` (${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}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
|
||||||
media,
|
media,
|
||||||
extra: [
|
extra: [
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { default as generatePassword } from 'secure-random-password';
|
import { default as generatePassword } from 'secure-random-password';
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
RelationCount,
|
RelationCount,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { MediaRequestStatus, MediaType } from '../constants/media';
|
import { MediaRequestStatus, MediaType } from '../constants/media';
|
||||||
import { UserType } from '../constants/user';
|
import { UserType } from '../constants/user';
|
||||||
import { QuotaResponse } from '../interfaces/api/userInterfaces';
|
import { QuotaResponse } from '../interfaces/api/userInterfaces';
|
||||||
@@ -189,7 +189,7 @@ export class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async resetPassword(): Promise<void> {
|
public async resetPassword(): Promise<void> {
|
||||||
const guid = uuid();
|
const guid = randomUUID();
|
||||||
this.resetPasswordGuid = guid;
|
this.resetPasswordGuid = guid;
|
||||||
|
|
||||||
// 24 hours into the future
|
// 24 hours into the future
|
||||||
@@ -212,7 +212,7 @@ export class User {
|
|||||||
},
|
},
|
||||||
locals: {
|
locals: {
|
||||||
resetPasswordLink,
|
resetPasswordLink,
|
||||||
applicationUrl: resetPasswordLink,
|
applicationUrl,
|
||||||
applicationTitle,
|
applicationTitle,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -307,7 +307,7 @@ export class User {
|
|||||||
limit: movieQuotaLimit,
|
limit: movieQuotaLimit,
|
||||||
used: movieQuotaUsed,
|
used: movieQuotaUsed,
|
||||||
remaining: movieQuotaLimit
|
remaining: movieQuotaLimit
|
||||||
? movieQuotaLimit - movieQuotaUsed
|
? Math.max(0, movieQuotaLimit - movieQuotaUsed)
|
||||||
: undefined,
|
: undefined,
|
||||||
restricted:
|
restricted:
|
||||||
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
|
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
|
||||||
@@ -318,7 +318,9 @@ export class User {
|
|||||||
days: tvQuotaDays,
|
days: tvQuotaDays,
|
||||||
limit: tvQuotaLimit,
|
limit: tvQuotaLimit,
|
||||||
used: tvQuotaUsed,
|
used: tvQuotaUsed,
|
||||||
remaining: tvQuotaLimit ? tvQuotaLimit - tvQuotaUsed : undefined,
|
remaining: tvQuotaLimit
|
||||||
|
? Math.max(0, tvQuotaLimit - tvQuotaUsed)
|
||||||
|
: undefined,
|
||||||
restricted:
|
restricted:
|
||||||
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
|
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import crypto from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import * as openpgp from 'openpgp';
|
import * as openpgp from 'openpgp';
|
||||||
import { Transform, TransformCallback } from 'stream';
|
import { Transform, TransformCallback } from 'stream';
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class PGPEncryptor extends Transform {
|
|||||||
const validPublicKeys = await Promise.all(
|
const validPublicKeys = await Promise.all(
|
||||||
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey }))
|
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
|
// Just return the message if there is no one to encrypt for
|
||||||
if (!validPublicKeys.length) {
|
if (!validPublicKeys.length) {
|
||||||
@@ -51,7 +51,7 @@ class PGPEncryptor extends Transform {
|
|||||||
|
|
||||||
// Only sign the message if private key and password exist
|
// Only sign the message if private key and password exist
|
||||||
if (this._signingKey && this._password) {
|
if (this._signingKey && this._password) {
|
||||||
privateKey = await openpgp.readKey({
|
privateKey = await openpgp.readPrivateKey({
|
||||||
armoredKey: this._signingKey,
|
armoredKey: this._signingKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ class PGPEncryptor extends Transform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new boundary for the email content
|
// 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
|
* Concatenate everything into single strings
|
||||||
* and add pgp headers to the email headers
|
* and add pgp headers to the email headers
|
||||||
@@ -135,8 +135,8 @@ class PGPEncryptor extends Transform {
|
|||||||
emailPartDelimiter +
|
emailPartDelimiter +
|
||||||
messageParts.join(emailPartDelimiter),
|
messageParts.join(emailPartDelimiter),
|
||||||
}),
|
}),
|
||||||
publicKeys: validPublicKeys,
|
encryptionKeys: validPublicKeys,
|
||||||
privateKeys: privateKey,
|
signingKeys: privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const body =
|
const body =
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ class WebPushAgent
|
|||||||
|
|
||||||
const allSubs = await userPushSubRepository
|
const allSubs = await userPushSubRepository
|
||||||
.createQueryBuilder('pushSub')
|
.createQueryBuilder('pushSub')
|
||||||
|
.leftJoinAndSelect('pushSub.user', 'user')
|
||||||
.where('pushSub.userId IN (:users)', {
|
.where('pushSub.userId IN (:users)', {
|
||||||
users: manageUsers.map((user) => user.id),
|
users: manageUsers.map((user) => user.id),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import TheMovieDb from '../../api/themoviedb';
|
import TheMovieDb from '../../api/themoviedb';
|
||||||
import { MediaStatus, MediaType } from '../../constants/media';
|
import { MediaStatus, MediaType } from '../../constants/media';
|
||||||
import Media from '../../entity/Media';
|
import Media from '../../entity/Media';
|
||||||
@@ -512,7 +512,7 @@ class BaseScanner<T> {
|
|||||||
*/
|
*/
|
||||||
protected startRun(): string {
|
protected startRun(): string {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const sessionId = uuid();
|
const sessionId = randomUUID();
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
|
|
||||||
this.log('Scan starting', 'info', { sessionId });
|
this.log('Scan starting', 'info', { sessionId });
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { User } from '../../../entity/User';
|
|||||||
import { getSettings, Library } from '../../settings';
|
import { getSettings, Library } from '../../settings';
|
||||||
import BaseScanner, {
|
import BaseScanner, {
|
||||||
MediaIds,
|
MediaIds,
|
||||||
|
ProcessableSeason,
|
||||||
RunnableScanner,
|
RunnableScanner,
|
||||||
StatusBase,
|
StatusBase,
|
||||||
ProcessableSeason,
|
|
||||||
} from '../baseScanner';
|
} from '../baseScanner';
|
||||||
|
|
||||||
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { randomUUID } from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
import { Permission } from './permissions';
|
import { Permission } from './permissions';
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class Settings {
|
|||||||
|
|
||||||
constructor(initialSettings?: AllSettings) {
|
constructor(initialSettings?: AllSettings) {
|
||||||
this.data = {
|
this.data = {
|
||||||
clientId: uuidv4(),
|
clientId: randomUUID(),
|
||||||
vapidPrivate: '',
|
vapidPrivate: '',
|
||||||
vapidPublic: '',
|
vapidPublic: '',
|
||||||
main: {
|
main: {
|
||||||
@@ -428,7 +428,7 @@ class Settings {
|
|||||||
|
|
||||||
get clientId(): string {
|
get clientId(): string {
|
||||||
if (!this.data.clientId) {
|
if (!this.data.clientId) {
|
||||||
this.data.clientId = uuidv4();
|
this.data.clientId = randomUUID();
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateApiKey(): string {
|
private generateApiKey(): string {
|
||||||
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
|
return Buffer.from(`${Date.now()}${randomUUID()})`).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateVapidKeys(force = false): void {
|
private generateVapidKeys(force = false): void {
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ import type {
|
|||||||
TmdbMovieReleaseResult,
|
TmdbMovieReleaseResult,
|
||||||
TmdbProductionCompany,
|
TmdbProductionCompany,
|
||||||
} from '../api/themoviedb/interfaces';
|
} from '../api/themoviedb/interfaces';
|
||||||
|
import Media from '../entity/Media';
|
||||||
import {
|
import {
|
||||||
ProductionCompany,
|
|
||||||
Genre,
|
|
||||||
Cast,
|
Cast,
|
||||||
Crew,
|
Crew,
|
||||||
|
ExternalIds,
|
||||||
|
Genre,
|
||||||
mapCast,
|
mapCast,
|
||||||
mapCrew,
|
mapCrew,
|
||||||
ExternalIds,
|
|
||||||
mapExternalIds,
|
mapExternalIds,
|
||||||
mapVideos,
|
mapVideos,
|
||||||
|
mapWatchProviders,
|
||||||
|
ProductionCompany,
|
||||||
|
WatchProviders,
|
||||||
} from './common';
|
} from './common';
|
||||||
import Media from '../entity/Media';
|
|
||||||
|
|
||||||
export interface Video {
|
export interface Video {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -78,6 +80,7 @@ export interface MovieDetails {
|
|||||||
mediaInfo?: Media;
|
mediaInfo?: Media;
|
||||||
externalIds: ExternalIds;
|
externalIds: ExternalIds;
|
||||||
plexUrl?: string;
|
plexUrl?: string;
|
||||||
|
watchProviders?: WatchProviders[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapProductionCompany = (
|
export const mapProductionCompany = (
|
||||||
@@ -136,4 +139,5 @@ export const mapMovieDetails = (
|
|||||||
: undefined,
|
: undefined,
|
||||||
externalIds: mapExternalIds(movie.external_ids),
|
externalIds: mapExternalIds(movie.external_ids),
|
||||||
mediaInfo: media,
|
mediaInfo: media,
|
||||||
|
watchProviders: mapWatchProviders(movie['watch/providers']?.results ?? {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import {
|
|
||||||
Genre,
|
|
||||||
ProductionCompany,
|
|
||||||
Cast,
|
|
||||||
Crew,
|
|
||||||
mapAggregateCast,
|
|
||||||
mapCrew,
|
|
||||||
ExternalIds,
|
|
||||||
mapExternalIds,
|
|
||||||
Keyword,
|
|
||||||
mapVideos,
|
|
||||||
TvNetwork,
|
|
||||||
} from './common';
|
|
||||||
import type {
|
import type {
|
||||||
TmdbTvEpisodeResult,
|
|
||||||
TmdbTvSeasonResult,
|
|
||||||
TmdbTvDetails,
|
|
||||||
TmdbSeasonWithEpisodes,
|
|
||||||
TmdbTvRatingResult,
|
|
||||||
TmdbNetwork,
|
TmdbNetwork,
|
||||||
|
TmdbSeasonWithEpisodes,
|
||||||
|
TmdbTvDetails,
|
||||||
|
TmdbTvEpisodeResult,
|
||||||
|
TmdbTvRatingResult,
|
||||||
|
TmdbTvSeasonResult,
|
||||||
} from '../api/themoviedb/interfaces';
|
} from '../api/themoviedb/interfaces';
|
||||||
import type Media from '../entity/Media';
|
import type Media from '../entity/Media';
|
||||||
|
import {
|
||||||
|
Cast,
|
||||||
|
Crew,
|
||||||
|
ExternalIds,
|
||||||
|
Genre,
|
||||||
|
Keyword,
|
||||||
|
mapAggregateCast,
|
||||||
|
mapCrew,
|
||||||
|
mapExternalIds,
|
||||||
|
mapVideos,
|
||||||
|
mapWatchProviders,
|
||||||
|
ProductionCompany,
|
||||||
|
TvNetwork,
|
||||||
|
WatchProviders,
|
||||||
|
} from './common';
|
||||||
import { Video } from './Movie';
|
import { Video } from './Movie';
|
||||||
|
|
||||||
interface Episode {
|
interface Episode {
|
||||||
@@ -102,6 +104,7 @@ export interface TvDetails {
|
|||||||
externalIds: ExternalIds;
|
externalIds: ExternalIds;
|
||||||
keywords: Keyword[];
|
keywords: Keyword[];
|
||||||
mediaInfo?: Media;
|
mediaInfo?: Media;
|
||||||
|
watchProviders?: WatchProviders[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
||||||
@@ -213,4 +216,5 @@ export const mapTvDetails = (
|
|||||||
name: keyword.name,
|
name: keyword.name,
|
||||||
})),
|
})),
|
||||||
mediaInfo: media,
|
mediaInfo: media,
|
||||||
|
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type {
|
import type {
|
||||||
TmdbCreditCast,
|
|
||||||
TmdbAggregateCreditCast,
|
TmdbAggregateCreditCast,
|
||||||
|
TmdbCreditCast,
|
||||||
TmdbCreditCrew,
|
TmdbCreditCrew,
|
||||||
TmdbExternalIds,
|
TmdbExternalIds,
|
||||||
TmdbVideo,
|
TmdbVideo,
|
||||||
TmdbVideoResult,
|
TmdbVideoResult,
|
||||||
|
TmdbWatchProviderDetails,
|
||||||
|
TmdbWatchProviders,
|
||||||
} from '../api/themoviedb/interfaces';
|
} from '../api/themoviedb/interfaces';
|
||||||
|
|
||||||
import { Video } from '../models/Movie';
|
import { Video } from '../models/Movie';
|
||||||
|
|
||||||
export interface ProductionCompany {
|
export interface ProductionCompany {
|
||||||
@@ -70,6 +71,20 @@ export interface ExternalIds {
|
|||||||
twitterId?: string;
|
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 => ({
|
export const mapCast = (person: TmdbCreditCast): Cast => ({
|
||||||
castId: person.cast_id,
|
castId: person.cast_id,
|
||||||
character: person.character,
|
character: person.character,
|
||||||
@@ -124,7 +139,33 @@ export const mapVideos = (videoResult: TmdbVideoResult): Video[] =>
|
|||||||
url: siteUrlCreator(site, key),
|
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 =>
|
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]);
|
}[site]);
|
||||||
|
|||||||
@@ -350,6 +350,14 @@ requestRoutes.post('/', async (req, res, next) => {
|
|||||||
status: 202,
|
status: 202,
|
||||||
message: 'No seasons available to request',
|
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);
|
await mediaRepository.save(media);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { truncate } from 'lodash';
|
||||||
import {
|
import {
|
||||||
EntitySubscriberInterface,
|
EntitySubscriberInterface,
|
||||||
EventSubscriber,
|
EventSubscriber,
|
||||||
@@ -34,7 +35,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
|
|||||||
subject: `${movie.title}${
|
subject: `${movie.title}${
|
||||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||||
}`,
|
}`,
|
||||||
message: movie.overview,
|
message: truncate(movie.overview, {
|
||||||
|
length: 500,
|
||||||
|
separator: /\s/,
|
||||||
|
omission: '…',
|
||||||
|
}),
|
||||||
media: entity,
|
media: entity,
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
request: request,
|
request: request,
|
||||||
@@ -89,7 +94,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
|
|||||||
subject: `${tv.name}${
|
subject: `${tv.name}${
|
||||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
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,
|
notifyUser: request.requestedBy,
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||||
media: entity,
|
media: entity,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ head
|
|||||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
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&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
|
//if mso
|
||||||
xml
|
xml
|
||||||
o:officedocumentsettings
|
o:officedocumentsettings
|
||||||
@@ -26,72 +26,37 @@ head
|
|||||||
mso-line-height-rule: exactly;
|
mso-line-height-rule: exactly;
|
||||||
}
|
}
|
||||||
style.
|
style.
|
||||||
@media (max-width: 600px) {
|
.title:hover * {
|
||||||
.sm-w-full {
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width:600px) {
|
||||||
|
table {
|
||||||
|
font-size: 20px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
div(style='display: block; background-color: #111827;')
|
||||||
table(style="\
|
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||||
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')
|
|
||||||
tr
|
tr
|
||||||
td(align='center')
|
td(style="text-align: center;")
|
||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
a(href=applicationUrl)
|
||||||
|
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(style='text-align: center;')
|
||||||
padding-top: 25px;\
|
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||||
padding-bottom: 25px;\
|
span
|
||||||
text-align: center;\
|
| An account has been created for you at #{applicationTitle}.
|
||||||
')
|
|
||||||
a(href=applicationUrl style='\
|
|
||||||
text-shadow: 0 1px 0 #ffffff;\
|
|
||||||
font-weight: 700;\
|
|
||||||
font-size: 24px;\
|
|
||||||
color: #a8aaaf;\
|
|
||||||
text-decoration: none;\
|
|
||||||
')
|
|
||||||
| #{applicationTitle}
|
|
||||||
tr
|
tr
|
||||||
td(style='width: 100%' width='100%')
|
td(style='text-align: center;')
|
||||||
table.sm-w-full(align='center' style='\
|
div(style='margin: 1rem 1rem 1rem; font-size: 1.25em;')
|
||||||
background-color: #ffffff;\
|
span
|
||||||
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:
|
| Your new password is:
|
||||||
div(style='font-size: 16px; text-align: center')
|
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||||
|
span
|
||||||
| #{password}
|
| #{password}
|
||||||
p(style='\
|
if applicationUrl
|
||||||
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')
|
|
||||||
tr
|
tr
|
||||||
td(align='center' style='font-size: 16px; padding: 45px')
|
td
|
||||||
p(style='\
|
a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||||
font-size: 13px;\
|
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);')
|
||||||
line-height: 24px;\
|
| Open #{applicationTitle}
|
||||||
margin-top: 6px;\
|
|
||||||
margin-bottom: 20px;\
|
|
||||||
text-align: center;\
|
|
||||||
color: #a8aaaf;\
|
|
||||||
')
|
|
||||||
| #{applicationTitle}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ head
|
|||||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
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&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
|
//if mso
|
||||||
xml
|
xml
|
||||||
o:officedocumentsettings
|
o:officedocumentsettings
|
||||||
@@ -26,92 +26,56 @@ head
|
|||||||
mso-line-height-rule: exactly;
|
mso-line-height-rule: exactly;
|
||||||
}
|
}
|
||||||
style.
|
style.
|
||||||
@media (max-width: 600px) {
|
.title:hover * {
|
||||||
.sm-w-full {
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width:600px) {
|
||||||
|
table {
|
||||||
|
font-size: 20px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
div(style='display: block; background-color: #111827;')
|
||||||
table(style="\
|
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||||
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')
|
|
||||||
tr
|
tr
|
||||||
td(align='center')
|
td(style="text-align: center;")
|
||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
a(href=applicationUrl)
|
||||||
|
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(style='text-align: center;')
|
||||||
padding-top: 25px;\
|
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||||
padding-bottom: 25px;\
|
span
|
||||||
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}
|
| #{body}
|
||||||
br
|
tr
|
||||||
br
|
td
|
||||||
p(style='margin-top: 4px; text-align: center')
|
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')
|
||||||
b
|
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}
|
| #{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
|
||||||
|
| #{requestedBy}
|
||||||
each extra in mediaExtra
|
each extra in mediaExtra
|
||||||
br
|
span(style='display: block;')
|
||||||
| #{extra.name}:
|
b(style='color: #9ca3af; font-weight: 700;')
|
||||||
|
| #{extra.name}
|
||||||
| #{extra.value}
|
| #{extra.value}
|
||||||
table(align='center' cellpadding='0' cellspacing='0' role='presentation')
|
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
|
tr
|
||||||
td
|
td
|
||||||
a(href=actionUrl style='color: #3869d4')
|
a(href=actionUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||||
img(src=imageUrl alt='')
|
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);')
|
||||||
p(style='\
|
| Open in #{applicationTitle}
|
||||||
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')
|
|
||||||
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}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ head
|
|||||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
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&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
|
//if mso
|
||||||
xml
|
xml
|
||||||
o:officedocumentsettings
|
o:officedocumentsettings
|
||||||
@@ -26,74 +26,34 @@ head
|
|||||||
mso-line-height-rule: exactly;
|
mso-line-height-rule: exactly;
|
||||||
}
|
}
|
||||||
style.
|
style.
|
||||||
@media (max-width: 600px) {
|
.title:hover * {
|
||||||
.sm-w-full {
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width:600px) {
|
||||||
|
table {
|
||||||
|
font-size: 20px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
div(style='display: block; background-color: #111827;')
|
||||||
table(style="\
|
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||||
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')
|
|
||||||
tr
|
tr
|
||||||
td(align='center')
|
td(style="text-align: center;")
|
||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
a(href=applicationUrl)
|
||||||
|
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(style='text-align: center;')
|
||||||
padding-top: 25px;\
|
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||||
padding-bottom: 25px;\
|
span
|
||||||
text-align: center;\
|
| Your #{applicationTitle} account password was requested to be reset. Click below to reset your password.
|
||||||
')
|
if resetPasswordLink
|
||||||
a(href=applicationUrl style='\
|
|
||||||
text-shadow: 0 1px 0 #ffffff;\
|
|
||||||
font-weight: 700;\
|
|
||||||
font-size: 24px;\
|
|
||||||
color: #a8aaaf;\
|
|
||||||
text-decoration: none;\
|
|
||||||
')
|
|
||||||
| #{applicationTitle}
|
|
||||||
tr
|
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
|
td
|
||||||
table.sm-w-full(align='center' style='\
|
a(href=resetPasswordLink style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||||
margin-left: auto;\
|
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);')
|
||||||
margin-right: auto;\
|
| Reset Password
|
||||||
text-align: center;\
|
|
||||||
width: 570px;\
|
|
||||||
' width='570' cellpadding='0' cellspacing='0' role='presentation')
|
|
||||||
tr
|
tr
|
||||||
td(align='center' style='font-size: 16px; padding: 45px')
|
td(style='text-align: center;')
|
||||||
p(style='\
|
div(style='margin: 1rem; font-size: .85em;')
|
||||||
font-size: 13px;\
|
span
|
||||||
line-height: 24px;\
|
| If you did not request that your password be reset, you can safely ignore this email.
|
||||||
margin-top: 6px;\
|
|
||||||
margin-bottom: 20px;\
|
|
||||||
text-align: center;\
|
|
||||||
color: #a8aaaf;\
|
|
||||||
')
|
|
||||||
| #{applicationTitle}.
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ head
|
|||||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
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&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
|
//if mso
|
||||||
xml
|
xml
|
||||||
o:officedocumentsettings
|
o:officedocumentsettings
|
||||||
@@ -26,70 +26,29 @@ head
|
|||||||
mso-line-height-rule: exactly;
|
mso-line-height-rule: exactly;
|
||||||
}
|
}
|
||||||
style.
|
style.
|
||||||
@media (max-width: 600px) {
|
.title:hover * {
|
||||||
.sm-w-full {
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width:600px) {
|
||||||
|
table {
|
||||||
|
font-size: 20px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
div(style='display: block; background-color: #111827;')
|
||||||
table(style="\
|
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||||
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')
|
|
||||||
tr
|
tr
|
||||||
td(align='center')
|
td(style="text-align: center;")
|
||||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
a(href=applicationUrl)
|
||||||
|
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||||
tr
|
tr
|
||||||
td(align='center' style='\
|
td(style='text-align: center;')
|
||||||
padding-top: 25px;\
|
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||||
padding-bottom: 25px;\
|
span
|
||||||
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}
|
| #{body}
|
||||||
p(style='\
|
if applicationUrl
|
||||||
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')
|
|
||||||
tr
|
tr
|
||||||
td(align='center' style='font-size: 16px; padding: 45px')
|
td
|
||||||
p(style='\
|
a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||||
font-size: 13px;\
|
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);')
|
||||||
line-height: 24px;\
|
| Open #{applicationTitle}
|
||||||
margin-top: 6px;\
|
|
||||||
margin-bottom: 20px;\
|
|
||||||
text-align: center;\
|
|
||||||
color: #a8aaaf;\
|
|
||||||
')
|
|
||||||
| #{applicationTitle}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ confinement: strict
|
|||||||
parts:
|
parts:
|
||||||
overseerr:
|
overseerr:
|
||||||
plugin: nodejs
|
plugin: nodejs
|
||||||
nodejs-version: '14.16.1'
|
nodejs-version: '14.17.0'
|
||||||
nodejs-package-manager: 'yarn'
|
nodejs-package-manager: 'yarn'
|
||||||
nodejs-yarn-version: v1.22.10
|
nodejs-yarn-version: v1.22.10
|
||||||
build-packages:
|
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 }) => {
|
const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
||||||
let design = {
|
let design = {
|
||||||
bgColor: 'bg-yellow-600',
|
bgColor: 'bg-yellow-600',
|
||||||
titleColor: 'text-yellow-200',
|
titleColor: 'text-yellow-100',
|
||||||
textColor: 'text-yellow-300',
|
textColor: 'text-yellow-300',
|
||||||
svg: <ExclamationIcon className="w-5 h-5" />,
|
svg: <ExclamationIcon className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
@@ -22,7 +22,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
|||||||
case 'info':
|
case 'info':
|
||||||
design = {
|
design = {
|
||||||
bgColor: 'bg-indigo-600',
|
bgColor: 'bg-indigo-600',
|
||||||
titleColor: 'text-indigo-200',
|
titleColor: 'text-indigo-100',
|
||||||
textColor: 'text-indigo-300',
|
textColor: 'text-indigo-300',
|
||||||
svg: <InformationCircleIcon className="w-5 h-5" />,
|
svg: <InformationCircleIcon className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
@@ -30,7 +30,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
|||||||
case 'error':
|
case 'error':
|
||||||
design = {
|
design = {
|
||||||
bgColor: 'bg-red-600',
|
bgColor: 'bg-red-600',
|
||||||
titleColor: 'text-red-200',
|
titleColor: 'text-red-100',
|
||||||
textColor: 'text-red-300',
|
textColor: 'text-red-300',
|
||||||
svg: <XCircleIcon className="w-5 h-5" />,
|
svg: <XCircleIcon className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function Button<P extends ElementTypes = 'button'>(
|
|||||||
ref?: React.Ref<Element<P>>
|
ref?: React.Ref<Element<P>>
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const buttonStyle = [
|
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) {
|
switch (buttonType) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const ListItem: React.FC<ListItemProps> = ({ title, className, children }) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<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">
|
<dd className="flex text-sm text-white sm:mt-0 sm:col-span-2">
|
||||||
<span className={`flex-grow ${className}`}>{children}</span>
|
<span className={`flex-grow ${className}`}>{children}</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<span
|
<span
|
||||||
className="text-lg font-medium leading-6 text-white"
|
className="text-lg font-bold leading-6 text-white"
|
||||||
id="modal-headline"
|
id="modal-headline"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const SettingsLink: React.FC<{
|
|||||||
|
|
||||||
if (tabType === 'button') {
|
if (tabType === 'button') {
|
||||||
linkClasses =
|
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';
|
activeLinkColor = 'bg-indigo-700';
|
||||||
inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700';
|
inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700';
|
||||||
}
|
}
|
||||||
@@ -119,8 +119,8 @@ const SettingsTabs: React.FC<{
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{tabType === 'button' ? (
|
{tabType === 'button' ? (
|
||||||
<div className="hidden overflow-x-scroll overflow-y-hidden sm:block hide-scrollbar">
|
<div className="hidden sm:block">
|
||||||
<nav className="flex space-x-4" aria-label="Tabs">
|
<nav className="flex flex-wrap -mx-2 -my-1" aria-label="Tabs">
|
||||||
{settingsRoutes.map((route, index) => (
|
{settingsRoutes.map((route, index) => (
|
||||||
<SettingsLink
|
<SettingsLink
|
||||||
tabType={tabType}
|
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">
|
<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">
|
<header className="px-4 space-y-1 bg-indigo-600 slideover">
|
||||||
<div className="flex items-center justify-between space-x-3">
|
<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}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center h-7">
|
<div className="flex items-center h-7">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const TH: React.FC<AllHTMLAttributes<HTMLTableHeaderCellElement>> = ({
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const style = [
|
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) {
|
if (className) {
|
||||||
@@ -39,7 +39,7 @@ const TD: React.FC<TDProps> = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const style = ['whitespace-nowrap text-sm leading-5 text-white'];
|
const style = ['text-sm leading-5 text-white'];
|
||||||
|
|
||||||
switch (alignText) {
|
switch (alignText) {
|
||||||
case 'left':
|
case 'left':
|
||||||
@@ -54,7 +54,7 @@ const TD: React.FC<TDProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!noPadding) {
|
if (!noPadding) {
|
||||||
style.push('px-6 py-4');
|
style.push('px-4 py-4');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (className) {
|
if (className) {
|
||||||
@@ -73,7 +73,7 @@ const Table: React.FC = ({ children }) => {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
|
<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="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>
|
<table className="min-w-full">{children}</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import useLocale from '../../../hooks/useLocale';
|
|||||||
import Transition from '../../Transition';
|
import Transition from '../../Transition';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
changelanguage: 'Change Language',
|
displaylanguage: 'Display Language',
|
||||||
});
|
});
|
||||||
|
|
||||||
const LanguagePicker: React.FC = () => {
|
const LanguagePicker: React.FC = () => {
|
||||||
@@ -50,9 +50,9 @@ const LanguagePicker: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="language"
|
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>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="language"
|
id="language"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const SearchInput: React.FC = () => {
|
|||||||
const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput();
|
const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1">
|
<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">
|
<label htmlFor="search_field" className="sr-only">
|
||||||
Search
|
Search
|
||||||
</label>
|
</label>
|
||||||
@@ -24,7 +24,7 @@ const SearchInput: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
id="search_field"
|
id="search_field"
|
||||||
style={{ paddingRight: searchValue.length > 0 ? '1.75rem' : '' }}
|
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)}
|
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||||
type="search"
|
type="search"
|
||||||
inputMode="search"
|
inputMode="search"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
|
UsersIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
import { UsersIcon } from '@heroicons/react/solid';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { ReactNode, useRef } from 'react';
|
import React, { ReactNode, useRef } from 'react';
|
||||||
@@ -39,34 +39,26 @@ const SidebarLinks: SidebarLinkProps[] = [
|
|||||||
{
|
{
|
||||||
href: '/',
|
href: '/',
|
||||||
messagesKey: 'dashboard',
|
messagesKey: 'dashboard',
|
||||||
svgIcon: (
|
svgIcon: <SparklesIcon className="w-6 h-6 mr-3" />,
|
||||||
<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" />
|
|
||||||
),
|
|
||||||
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
|
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/requests',
|
href: '/requests',
|
||||||
messagesKey: 'requests',
|
messagesKey: 'requests',
|
||||||
svgIcon: (
|
svgIcon: <ClockIcon className="w-6 h-6 mr-3" />,
|
||||||
<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" />
|
|
||||||
),
|
|
||||||
activeRegExp: /^\/requests/,
|
activeRegExp: /^\/requests/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/users',
|
href: '/users',
|
||||||
messagesKey: 'users',
|
messagesKey: 'users',
|
||||||
svgIcon: (
|
svgIcon: <UsersIcon className="w-6 h-6 mr-3" />,
|
||||||
<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" />
|
|
||||||
),
|
|
||||||
activeRegExp: /^\/users/,
|
activeRegExp: /^\/users/,
|
||||||
requiredPermission: Permission.MANAGE_USERS,
|
requiredPermission: Permission.MANAGE_USERS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/settings',
|
href: '/settings',
|
||||||
messagesKey: 'settings',
|
messagesKey: 'settings',
|
||||||
svgIcon: (
|
svgIcon: <CogIcon className="w-6 h-6 mr-3" />,
|
||||||
<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" />
|
|
||||||
),
|
|
||||||
activeRegExp: /^\/settings/,
|
activeRegExp: /^\/settings/,
|
||||||
requiredPermission: Permission.MANAGE_SETTINGS,
|
requiredPermission: Permission.MANAGE_SETTINGS,
|
||||||
},
|
},
|
||||||
@@ -81,7 +73,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="md:hidden">
|
<div className="lg:hidden">
|
||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<div className="fixed inset-0 z-40 flex">
|
<div className="fixed inset-0 z-40 flex">
|
||||||
<Transition
|
<Transition
|
||||||
@@ -93,7 +85,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-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>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition
|
<Transition
|
||||||
@@ -117,16 +109,16 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={navRef}
|
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">
|
<div className="flex items-center flex-shrink-0 px-2">
|
||||||
<span className="text-xl text-gray-50">
|
<span className="px-4 text-xl text-gray-50">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="/logo.png" alt="Logo" />
|
<img src="/logo_full.svg" alt="Logo" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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) =>
|
{SidebarLinks.filter((link) =>
|
||||||
link.requiredPermission
|
link.requiredPermission
|
||||||
? hasPermission(link.requiredPermission)
|
? hasPermission(link.requiredPermission)
|
||||||
@@ -147,13 +139,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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(
|
router.pathname.match(
|
||||||
sidebarLink.activeRegExp
|
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>
|
</nav>
|
||||||
{hasPermission(Permission.ADMIN) && (
|
{hasPermission(Permission.ADMIN) && (
|
||||||
|
<div className="px-2">
|
||||||
<VersionStatus onClick={() => setClosed()} />
|
<VersionStatus onClick={() => setClosed()} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,18 +174,18 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</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 w-64 sidebar">
|
||||||
<div className="flex flex-col flex-1 h-0 bg-gray-800">
|
<div className="flex flex-col flex-1 h-0">
|
||||||
<div className="flex flex-col flex-1 pt-5 pb-4 overflow-y-auto">
|
<div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto">
|
||||||
<div className="flex items-center flex-shrink-0 px-4">
|
<div className="flex items-center flex-shrink-0">
|
||||||
<span className="text-2xl text-gray-50">
|
<span className="px-4 text-2xl text-gray-50">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="/logo.png" alt="Logo" />
|
<img src="/logo_full.svg" alt="Logo" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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) =>
|
{SidebarLinks.filter((link) =>
|
||||||
link.requiredPermission
|
link.requiredPermission
|
||||||
? hasPermission(link.requiredPermission)
|
? hasPermission(link.requiredPermission)
|
||||||
@@ -204,13 +198,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
as={sidebarLink.as}
|
as={sidebarLink.as}
|
||||||
>
|
>
|
||||||
<a
|
<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(
|
router.pathname.match(
|
||||||
sidebarLink.activeRegExp
|
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>
|
</nav>
|
||||||
{hasPermission(Permission.ADMIN) && <VersionStatus />}
|
{hasPermission(Permission.ADMIN) && (
|
||||||
|
<div className="px-2">
|
||||||
|
<VersionStatus />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||