Compare commits
12 Commits
fix-librar
...
prisma-(de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9428664f2e | ||
|
|
7e7efc06ba | ||
|
|
8a8953d52e | ||
|
|
6385c9bcb2 | ||
|
|
d202f9f618 | ||
|
|
77d3747267 | ||
|
|
31392856dc | ||
|
|
5e000abd56 | ||
|
|
250cdb969c | ||
|
|
71bc90ef89 | ||
|
|
7beea396a4 | ||
|
|
cdfa938471 |
7
.env
Normal file
7
.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Environment variables declared in this file are automatically made available to Prisma.
|
||||||
|
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||||
|
|
||||||
|
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (Preview) and CockroachDB (Preview).
|
||||||
|
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||||
|
|
||||||
|
DATABASE_URL="postgresql://jumail:1DontGive1@localhost:5432/jellyseerr?schema=public"
|
||||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
time: '20:00'
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
time: '20:00'
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
155
README.md
155
README.md
@@ -2,157 +2,48 @@
|
|||||||
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
|
<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" />
|
<a href="https://discord.gg/BHak4GCk"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||||
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20CI/badge.svg" alt="Overseerr CI">
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://discord.gg/overseerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
|
||||||
<a href="https://hub.docker.com/r/sctx/overseerr"><img src="https://img.shields.io/docker/pulls/sctx/overseerr" alt="Docker pulls"></a>
|
|
||||||
<a href="https://hosted.weblate.org/engage/overseerr/"><img src="https://hosted.weblate.org/widgets/overseerr/-/overseerr-frontend/svg-badge.svg" alt="Translation status" /></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>
|
|
||||||
<!-- 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-58-orange.svg"/></a>
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Overseerr** is a free and open source software application for managing requests for your media library. It integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**, and **[Plex](https://www.plex.tv/)**!
|
**Jellyseerr** is a free and open source fork of Overseerr for managing requests for your media library. It integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**, and **[Jellyfin](https://jellyfin.org/)**!
|
||||||
|
|
||||||
## Current Features
|
## Current Features
|
||||||
|
|
||||||
- Full Plex integration. Authenticate and manage user access with Plex!
|
- Jellyfin support
|
||||||
- Easy integration with your existing services. Currently, Overseerr supports Sonarr and Radarr. More to come!
|
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr.
|
||||||
- Plex library scan, to keep track of the titles which are already available.
|
- Jellyfin library scan, to keep track of the titles which are already available.
|
||||||
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
|
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
|
||||||
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
|
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
|
||||||
- Granular permission system.
|
- Granular permission system.
|
||||||
- Support for various notification agents.
|
- Support for various notification agents.
|
||||||
- Mobile-friendly design, for when you need to approve requests on the go!
|
- Mobile-friendly design, for when you need to approve requests on the go!
|
||||||
|
|
||||||
With more features on the way! Check out our [issue tracker](https://github.com/sct/overseerr/issues) to see the features which have already been requested.
|
Check out our [issue tracker](https://github.com/Fallenbagel/jellyseerr/issues).
|
||||||
|
|
||||||
|
## Supported Architectures
|
||||||
|
|
||||||
|
Jellyseerr image support multiple architectures such as x86-64, arm64 and armv7.
|
||||||
|
|
||||||
|
| **Architecture** | **Tag** |
|
||||||
|
|------------------|---------|
|
||||||
|
| x86-64 | latest |
|
||||||
|
| ARM64 | arm |
|
||||||
|
| ARMv7 | arm |
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Check out our documentation for instructions on how to install and run Overseerr:
|
Check out our dockerhub for instructions on how to install and run Jellyseerr:
|
||||||
|
https://hub.docker.com/r/fallenbagel/jellyseerr
|
||||||
https://docs.overseerr.dev/getting-started/installation
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
<img src="./public/preview.jpg">
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- Check out the [Overseerr Documentation](https://docs.overseerr.dev/) before asking for help. Your question might already be in the [FAQ](https://docs.overseerr.dev/support/faq).
|
- You can get support on [Discord](https://discord.gg/VpVnZ92yQK).
|
||||||
- You can get support on [Discord](https://discord.gg/overseerr).
|
|
||||||
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions).
|
|
||||||
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
|
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
|
||||||
|
|
||||||
## API Documentation
|
|
||||||
|
|
||||||
Our documentation is built on every commit and hosted at https://api-docs.overseerr.dev
|
|
||||||
|
|
||||||
You can also access the API documentation from your local Overseerr install at http://localhost:5055/api-docs
|
|
||||||
|
|
||||||
## Community
|
|
||||||
|
|
||||||
You can ask questions, share ideas, and more in [GitHub Discussions](https://github.com/sct/overseerr/discussions).
|
|
||||||
|
|
||||||
If you would like to chat with other members of our growing community, [join the Overseerr Discord server](https://discord.gg/overseerr)!
|
|
||||||
|
|
||||||
Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Overseerr community channels.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
You can help improve Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
|
|
||||||
|
|
||||||
## Contributors ✨
|
|
||||||
|
|
||||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
<!-- markdownlint-disable -->
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://sct.dev"><img src="https://avatars1.githubusercontent.com/u/234213?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sct</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sct" title="Code">💻</a> <a href="#design-sct" title="Design">🎨</a> <a href="#ideas-sct" title="Ideas, Planning, & Feedback">🤔</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/azoitos"><img src="https://avatars2.githubusercontent.com/u/26529049?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Zoitos</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=azoitos" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/OwsleyJr"><img src="https://avatars3.githubusercontent.com/u/8635678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brandon Cohen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Ahreluth"><img src="https://avatars2.githubusercontent.com/u/75682440?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ahreluth</b></sub></a><br /><a href="#translation-Ahreluth" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/KovalevArtem"><img src="https://avatars0.githubusercontent.com/u/36500228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>KovalevArtem</b></sub></a><br /><a href="#translation-KovalevArtem" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/GiyomuWeb"><img src="https://avatars0.githubusercontent.com/u/62489209?v=4?s=100" width="100px;" alt=""/><br /><sub><b>GiyomuWeb</b></sub></a><br /><a href="#translation-GiyomuWeb" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/angrycuban13"><img src="https://avatars3.githubusercontent.com/u/39564898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Angry Cuban</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=angrycuban13" title="Documentation">📖</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4?s=100" width="100px;" alt=""/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/saltydk"><img src="https://avatars1.githubusercontent.com/u/6587950?v=4?s=100" width="100px;" alt=""/><br /><sub><b>salty</b></sub></a><br /><a href="#infra-saltydk" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Shutruk"><img src="https://avatars2.githubusercontent.com/u/9198633?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shutruk</b></sub></a><br /><a href="#translation-Shutruk" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/krystiancharubin"><img src="https://avatars2.githubusercontent.com/u/17775600?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krystian Charubin</b></sub></a><br /><a href="#design-krystiancharubin" title="Design">🎨</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/kieron"><img src="https://avatars2.githubusercontent.com/u/8655212?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kieron Boswell</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=kieron" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/samwiseg0"><img src="https://avatars1.githubusercontent.com/u/2241731?v=4?s=100" width="100px;" alt=""/><br /><sub><b>samwiseg0</b></sub></a><br /><a href="#question-samwiseg0" title="Answering Questions">💬</a> <a href="#infra-samwiseg0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/ecelebi29"><img src="https://avatars2.githubusercontent.com/u/8337120?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ecelebi29</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/mmozeiko"><img src="https://avatars3.githubusercontent.com/u/1665010?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mārtiņš Možeiko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=mmozeiko" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/mazzetta86"><img src="https://avatars2.githubusercontent.com/u/45591560?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mazzetta86</b></sub></a><br /><a href="#translation-mazzetta86" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Panzer1119"><img src="https://avatars1.githubusercontent.com/u/23016343?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Paul Hagedorn</b></sub></a><br /><a href="#translation-Panzer1119" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Shagon94"><img src="https://avatars3.githubusercontent.com/u/9140783?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shagon94</b></sub></a><br /><a href="#translation-Shagon94" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/sebstrgg"><img src="https://avatars3.githubusercontent.com/u/27026694?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sebstrgg</b></sub></a><br /><a href="#translation-sebstrgg" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/danshilm"><img src="https://avatars2.githubusercontent.com/u/20923978?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Danshil Mungur</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Documentation">📖</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/doob187"><img src="https://avatars1.githubusercontent.com/u/60312740?v=4?s=100" width="100px;" alt=""/><br /><sub><b>doob187</b></sub></a><br /><a href="#infra-doob187" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/johnpyp"><img src="https://avatars2.githubusercontent.com/u/20625636?v=4?s=100" width="100px;" alt=""/><br /><sub><b>johnpyp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=johnpyp" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/ankarhem"><img src="https://avatars1.githubusercontent.com/u/14110063?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakob Ankarhem</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Code">💻</a> <a href="#translation-ankarhem" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/jayesh100"><img src="https://avatars1.githubusercontent.com/u/8022175?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jayesh</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jayesh100" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/flying-sausages"><img src="https://avatars1.githubusercontent.com/u/23618693?v=4?s=100" width="100px;" alt=""/><br /><sub><b>flying-sausages</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=flying-sausages" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/hirenshah"><img src="https://avatars2.githubusercontent.com/u/418112?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hirenshah</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=hirenshah" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/TheCatLady"><img src="https://avatars0.githubusercontent.com/u/52870424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TheCatLady</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=TheCatLady" title="Code">💻</a> <a href="#translation-TheCatLady" title="Translation">🌍</a> <a href="https://github.com/sct/overseerr/commits?author=TheCatLady" title="Documentation">📖</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/chriscpritchard"><img src="https://avatars1.githubusercontent.com/u/1839074?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chris Pritchard</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=chriscpritchard" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=chriscpritchard" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Tamberlox"><img src="https://avatars3.githubusercontent.com/u/56069014?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tamberlox</b></sub></a><br /><a href="#translation-Tamberlox" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://hmnd.io"><img src="https://avatars.githubusercontent.com/u/12853597?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=hmnd" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://www.douglas-parker.com"><img src="https://avatars.githubusercontent.com/u/18235822?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Douglas Parker</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=douglasparker" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/dancarter"><img src="https://avatars.githubusercontent.com/u/4387516?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Carter</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=dancarter" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://nuro.dev"><img src="https://avatars.githubusercontent.com/u/4991309?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nuro</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=NuroDev" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/onedr0p"><img src="https://avatars.githubusercontent.com/u/213795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ᗪєνιη ᗷυнʟ</b></sub></a><br /><a href="#infra-onedr0p" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/JonnyWong16"><img src="https://avatars.githubusercontent.com/u/9099342?v=4?s=100" width="100px;" alt=""/><br /><sub><b>JonnyWong16</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JonnyWong16" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Roxedus"><img src="https://avatars.githubusercontent.com/u/7110194?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Roxedus</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Roxedus" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/WoisWoi"><img src="https://avatars.githubusercontent.com/u/75491231?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WoisWoi</b></sub></a><br /><a href="#translation-WoisWoi" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/HubDuck"><img src="https://avatars.githubusercontent.com/u/77843475?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HubDuck</b></sub></a><br /><a href="#translation-HubDuck" title="Translation">🌍</a> <a href="https://github.com/sct/overseerr/commits?author=HubDuck" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/costaht"><img src="https://avatars.githubusercontent.com/u/50637431?v=4?s=100" width="100px;" alt=""/><br /><sub><b>costaht</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=costaht" title="Documentation">📖</a> <a href="#translation-costaht" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Shjosan"><img src="https://avatars.githubusercontent.com/u/20847626?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shjosan</b></sub></a><br /><a href="#translation-Shjosan" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/kobaubarr"><img src="https://avatars.githubusercontent.com/u/28481522?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kobaubarr</b></sub></a><br /><a href="#translation-kobaubarr" title="Translation">🌍</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/notorius28"><img src="https://avatars.githubusercontent.com/u/1621513?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ricardo González</b></sub></a><br /><a href="#translation-notorius28" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="http://torkili.uz"><img src="https://avatars.githubusercontent.com/u/460764?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Torkil</b></sub></a><br /><a href="#translation-Torkiliuz" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://www.jagandeepbrar.io"><img src="https://avatars.githubusercontent.com/u/3048295?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jagandeep Brar</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JagandeepBrar" title="Documentation">📖</a></td>
|
|
||||||
<td align="center"><a href="http://dtalens.com"><img src="https://avatars.githubusercontent.com/u/6631832?v=4?s=100" width="100px;" alt=""/><br /><sub><b>dtalens</b></sub></a><br /><a href="#translation-dtalens" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/acortelyou"><img src="https://avatars.githubusercontent.com/u/1689668?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Cortelyou</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=acortelyou" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://nz.linkedin.com/in/jonocairns"><img src="https://avatars.githubusercontent.com/u/182836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jono Cairns</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jonocairns" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://scias.net/"><img src="https://avatars.githubusercontent.com/u/439655?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DJScias</b></sub></a><br /><a href="#translation-DJScias" title="Translation">🌍</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/Dabu-dot"><img src="https://avatars.githubusercontent.com/u/52525576?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dabu-dot</b></sub></a><br /><a href="#translation-Dabu-dot" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/Jabster28"><img src="https://avatars.githubusercontent.com/u/29015942?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jabster28</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Jabster28" title="Code">💻</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/Generator"><img src="https://avatars.githubusercontent.com/u/44146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bruno Guerreiro</b></sub></a><br /><a href="#translation-Generator" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/iceHtwoO"><img src="https://avatars.githubusercontent.com/u/27020492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Neuhäuser</b></sub></a><br /><a href="#translation-iceHtwoO" title="Translation">🌍</a></td>
|
|
||||||
<td align="center"><a href="http://www.unext.co.jp"><img src="https://avatars.githubusercontent.com/u/37431541?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Livio</b></sub></a><br /><a href="#design-liviokanone" title="Design">🎨</a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/tangentThought"><img src="https://avatars.githubusercontent.com/u/25516090?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tangentThought</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=tangentThought" title="Code">💻</a></td>
|
|
||||||
<td align="center"><a href="https://github.com/nicospz"><img src="https://avatars.githubusercontent.com/u/31373060?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolás Espinoza</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=nicospz" title="Code">💻</a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
<!-- markdownlint-restore -->
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
## Buy me a Coffee!
|
||||||
|
If you like jellyseerr and want to help maintain it, please buy me a coffee as it would help me out a lot!
|
||||||
|
|
||||||
|
[](https://www.buymeacoffee.com/fallen.bagel)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.4.1",
|
"@headlessui/react": "^1.4.1",
|
||||||
"@heroicons/react": "^1.0.4",
|
"@heroicons/react": "^1.0.4",
|
||||||
|
"@prisma/client": "^3.11.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.79",
|
"@tanem/react-nprogress": "^3.0.79",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"nodemailer": "^6.6.3",
|
"nodemailer": "^6.6.3",
|
||||||
"openpgp": "^5.0.0-3",
|
"openpgp": "^5.0.0-3",
|
||||||
"plex-api": "^5.3.1",
|
"plex-api": "^5.3.1",
|
||||||
|
"prisma": "^3.11.1",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-ace": "^9.3.0",
|
"react-ace": "^9.3.0",
|
||||||
|
|||||||
163
prisma/migrations/20220326212051_init/migration.sql
Normal file
163
prisma/migrations/20220326212051_init/migration.sql
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "media" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"mediaType" TEXT NOT NULL,
|
||||||
|
"tmdbId" INTEGER NOT NULL,
|
||||||
|
"tvdbId" INTEGER,
|
||||||
|
"imdbId" TEXT,
|
||||||
|
"status" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"status4k" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"lastSeasonChange" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"mediaAddedAt" TIMESTAMP(3),
|
||||||
|
"serviceId" INTEGER,
|
||||||
|
"serviceId4k" INTEGER,
|
||||||
|
"externalServiceId" INTEGER,
|
||||||
|
"externalServiceId4k" INTEGER,
|
||||||
|
"externalServiceSlug" TEXT,
|
||||||
|
"externalServiceSlug4k" TEXT,
|
||||||
|
"ratingKey" TEXT,
|
||||||
|
"ratingKey4k" TEXT,
|
||||||
|
"jellyfinMediaId" TEXT,
|
||||||
|
"jellyfinMediaId4k" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "media_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "media_request" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"status" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"mediaId" INTEGER,
|
||||||
|
"requestedById" INTEGER,
|
||||||
|
"modifiedById" INTEGER,
|
||||||
|
"is4k" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"serverId" INTEGER,
|
||||||
|
"profileId" INTEGER,
|
||||||
|
"rootFolder" TEXT,
|
||||||
|
"languageProfileId" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "media_request_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "migrations" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"timestamp" BIGINT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "migrations_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "season" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"seasonNumber" INTEGER NOT NULL,
|
||||||
|
"status" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"mediaId" INTEGER,
|
||||||
|
"status4k" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
|
CONSTRAINT "season_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "season_request" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"seasonNumber" INTEGER NOT NULL,
|
||||||
|
"status" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"requestId" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "season_request_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session" (
|
||||||
|
"expiredAt" BIGINT NOT NULL,
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"json" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"username" TEXT,
|
||||||
|
"plexId" INTEGER,
|
||||||
|
"plexToken" TEXT,
|
||||||
|
"permissions" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"avatar" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"password" TEXT,
|
||||||
|
"userType" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"plexUsername" TEXT,
|
||||||
|
"resetPasswordGuid" TEXT,
|
||||||
|
"recoveryLinkExpirationDate" TIMESTAMP(3),
|
||||||
|
"jellyfinUsername" TEXT,
|
||||||
|
"jellyfinAuthToken" TEXT,
|
||||||
|
"jellyfinUserId" TEXT,
|
||||||
|
"jellyfinDeviceId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user_settings" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"enableNotifications" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"discordId" TEXT,
|
||||||
|
"userId" INTEGER,
|
||||||
|
"region" TEXT,
|
||||||
|
"originalLanguage" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "user_settings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "sqlite_autoindex_media_1" ON "media"("tvdbId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media"("imdbId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media"("tvdbId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media"("tmdbId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session"("expiredAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "sqlite_autoindex_user_1" ON "user"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "sqlite_autoindex_user_settings_1" ON "user_settings"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_modifiedById_fkey" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "season" ADD CONSTRAINT "season_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "season_request" ADD CONSTRAINT "season_request_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
127
prisma/schema.prisma
Normal file
127
prisma/schema.prisma
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgres"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model media {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
mediaType String
|
||||||
|
tmdbId Int
|
||||||
|
tvdbId Int? @unique(map: "sqlite_autoindex_media_1")
|
||||||
|
imdbId String?
|
||||||
|
status Int @default(1)
|
||||||
|
status4k Int @default(1)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now())
|
||||||
|
lastSeasonChange DateTime @default(now())
|
||||||
|
mediaAddedAt DateTime?
|
||||||
|
serviceId Int?
|
||||||
|
serviceId4k Int?
|
||||||
|
externalServiceId Int?
|
||||||
|
externalServiceId4k Int?
|
||||||
|
externalServiceSlug String?
|
||||||
|
externalServiceSlug4k String?
|
||||||
|
ratingKey String?
|
||||||
|
ratingKey4k String?
|
||||||
|
jellyfinMediaId String?
|
||||||
|
jellyfinMediaId4k String?
|
||||||
|
media_request media_request[]
|
||||||
|
season season[]
|
||||||
|
|
||||||
|
@@index([imdbId], map: "IDX_7ff2d11f6a83cb52386eaebe74")
|
||||||
|
@@index([tvdbId], map: "IDX_41a289eb1fa489c1bc6f38d9c3")
|
||||||
|
@@index([tmdbId], map: "IDX_7157aad07c73f6a6ae3bbd5ef5")
|
||||||
|
}
|
||||||
|
|
||||||
|
model media_request {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
status Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now())
|
||||||
|
type String
|
||||||
|
mediaId Int?
|
||||||
|
requestedById Int?
|
||||||
|
modifiedById Int?
|
||||||
|
is4k Boolean @default(false)
|
||||||
|
serverId Int?
|
||||||
|
profileId Int?
|
||||||
|
rootFolder String?
|
||||||
|
languageProfileId Int?
|
||||||
|
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
user_media_request_modifiedByIdTouser user? @relation("media_request_modifiedByIdTouser", fields: [modifiedById], references: [id], onUpdate: NoAction)
|
||||||
|
user_media_request_requestedByIdTouser user? @relation("media_request_requestedByIdTouser", fields: [requestedById], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
season_request season_request[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model migrations {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
timestamp BigInt
|
||||||
|
name String
|
||||||
|
}
|
||||||
|
|
||||||
|
model season {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
seasonNumber Int
|
||||||
|
status Int @default(1)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now())
|
||||||
|
mediaId Int?
|
||||||
|
status4k Int @default(1)
|
||||||
|
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
model season_request {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
seasonNumber Int
|
||||||
|
status Int @default(1)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now())
|
||||||
|
requestId Int?
|
||||||
|
media_request media_request? @relation(fields: [requestId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
model session {
|
||||||
|
expiredAt BigInt
|
||||||
|
id String @id
|
||||||
|
json String
|
||||||
|
|
||||||
|
@@index([expiredAt], map: "IDX_28c5d1d16da7908c97c9bc2f74")
|
||||||
|
}
|
||||||
|
|
||||||
|
model user {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String @unique(map: "sqlite_autoindex_user_1")
|
||||||
|
username String?
|
||||||
|
plexId Int?
|
||||||
|
plexToken String?
|
||||||
|
permissions Int @default(0)
|
||||||
|
avatar String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now())
|
||||||
|
password String?
|
||||||
|
userType Int @default(1)
|
||||||
|
plexUsername String?
|
||||||
|
resetPasswordGuid String?
|
||||||
|
recoveryLinkExpirationDate DateTime?
|
||||||
|
jellyfinUsername String?
|
||||||
|
jellyfinAuthToken String?
|
||||||
|
jellyfinUserId String?
|
||||||
|
jellyfinDeviceId String?
|
||||||
|
media_request_media_request_modifiedByIdTouser media_request[] @relation("media_request_modifiedByIdTouser")
|
||||||
|
media_request_media_request_requestedByIdTouser media_request[] @relation("media_request_requestedByIdTouser")
|
||||||
|
user_settings user_settings?
|
||||||
|
}
|
||||||
|
|
||||||
|
model user_settings {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
enableNotifications Boolean @default(true)
|
||||||
|
discordId String?
|
||||||
|
userId Int? @unique(map: "sqlite_autoindex_user_settings_1")
|
||||||
|
region String?
|
||||||
|
originalLanguage String?
|
||||||
|
user user? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 48 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 76 KiB |
0
public/logo_full.svg:Zone.Identifier
Normal file
0
public/logo_full.svg:Zone.Identifier
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 504 KiB |
@@ -1,18 +1,14 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import csurf from 'csurf';
|
import csurf from 'csurf';
|
||||||
import express, { NextFunction, Request, Response } from 'express';
|
import express, { NextFunction, Request, Response } from 'express';
|
||||||
import * as OpenApiValidator from 'express-openapi-validator';
|
import * as OpenApiValidator from 'express-openapi-validator';
|
||||||
import session, { Store } from 'express-session';
|
|
||||||
import next from 'next';
|
import next from 'next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
import { createConnection, getRepository } from 'typeorm';
|
|
||||||
import YAML from 'yamljs';
|
import YAML from 'yamljs';
|
||||||
import PlexAPI from './api/plexapi';
|
import PlexAPI from './api/plexapi';
|
||||||
import { Session } from './entity/Session';
|
|
||||||
import { User } from './entity/User';
|
|
||||||
import { startJobs } from './job/schedule';
|
import { startJobs } from './job/schedule';
|
||||||
import notificationManager from './lib/notifications';
|
import notificationManager from './lib/notifications';
|
||||||
import DiscordAgent from './lib/notifications/agents/discord';
|
import DiscordAgent from './lib/notifications/agents/discord';
|
||||||
@@ -35,18 +31,17 @@ logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
|||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
const app = next({ dev });
|
const app = next({ dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
app
|
app
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const dbConnection = await createConnection();
|
// // Run migrations in production
|
||||||
|
// if (process.env.NODE_ENV === 'production') {
|
||||||
// Run migrations in production
|
// await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||||
if (process.env.NODE_ENV === 'production') {
|
// await dbConnection.runMigrations();
|
||||||
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
// await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||||
await dbConnection.runMigrations();
|
// }
|
||||||
await dbConnection.query('PRAGMA foreign_keys=ON');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Settings
|
// Load Settings
|
||||||
const settings = getSettings().load();
|
const settings = getSettings().load();
|
||||||
@@ -56,14 +51,23 @@ app
|
|||||||
settings.plex.libraries.length > 1 &&
|
settings.plex.libraries.length > 1 &&
|
||||||
!settings.plex.libraries[0].type
|
!settings.plex.libraries[0].type
|
||||||
) {
|
) {
|
||||||
const userRepository = getRepository(User);
|
const admin = await prisma.user.findFirst({
|
||||||
const admin = await userRepository.findOne({
|
select: {
|
||||||
select: ['id', 'plexToken'],
|
id: true,
|
||||||
order: { id: 'ASC' },
|
plexToken: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
// const userRepository = getRepository(User);
|
||||||
|
// const admin = await userRepository.findOne({
|
||||||
|
// select: ['id', 'plexToken'],
|
||||||
|
// order: { id: 'ASC' },
|
||||||
|
// });
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
const plexapi = new PlexAPI({ plexToken: admin.plexToken! });
|
||||||
await plexapi.syncLibraries();
|
await plexapi.syncLibraries();
|
||||||
logger.info('Migrating libraries to include media type', {
|
logger.info('Migrating libraries to include media type', {
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
@@ -129,22 +133,24 @@ app
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up sessions
|
// Set up sessions
|
||||||
const sessionRespository = getRepository(Session);
|
// const sessionRespository = getRepository(Session);
|
||||||
server.use(
|
// const sessionRespository = await prisma.session.findMany();
|
||||||
'/api',
|
|
||||||
session({
|
// server.use(
|
||||||
secret: settings.clientId,
|
// '/api',
|
||||||
resave: false,
|
// session({
|
||||||
saveUninitialized: false,
|
// secret: settings.clientId,
|
||||||
cookie: {
|
// resave: false,
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
// saveUninitialized: false,
|
||||||
},
|
// cookie: {
|
||||||
store: new TypeormStore({
|
// maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||||
cleanupLimit: 2,
|
// },
|
||||||
ttl: 1000 * 60 * 60 * 24 * 30,
|
// store: new TypeormStore({
|
||||||
}).connect(sessionRespository) as Store,
|
// cleanupLimit: 2,
|
||||||
})
|
// ttl: 1000 * 60 * 60 * 24 * 30,
|
||||||
);
|
// }).connect(sessionRespository) as Store,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
const apiDocs = YAML.load(API_SPEC_PATH);
|
const apiDocs = YAML.load(API_SPEC_PATH);
|
||||||
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
|
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
|
||||||
server.use(
|
server.use(
|
||||||
|
|||||||
@@ -60,9 +60,8 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: genres } = useSWR<{ id: number; name: string }[]>(
|
const { data: genres } =
|
||||||
`/api/v1/genres/movie`
|
useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
|
||||||
);
|
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
|
|||||||
@@ -35,13 +35,11 @@ const Discover: React.FC = () => {
|
|||||||
{ revalidateOnMount: true }
|
{ revalidateOnMount: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { data: requests, error: requestError } =
|
||||||
data: requests,
|
useSWR<RequestResultsResponse>(
|
||||||
error: requestError,
|
'/api/v1/request?filter=all&take=10&sort=modified&skip=0',
|
||||||
} = useSWR<RequestResultsResponse>(
|
{ revalidateOnMount: true }
|
||||||
'/api/v1/request?filter=all&take=10&sort=modified&skip=0',
|
);
|
||||||
{ revalidateOnMount: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ const LanguagePicker: React.FC = () => {
|
|||||||
}
|
}
|
||||||
defaultValue={locale}
|
defaultValue={locale}
|
||||||
>
|
>
|
||||||
{(Object.keys(
|
{(
|
||||||
availableLanguages
|
Object.keys(
|
||||||
) as (keyof typeof availableLanguages)[]).map((key) => (
|
availableLanguages
|
||||||
|
) as (keyof typeof availableLanguages)[]
|
||||||
|
).map((key) => (
|
||||||
<option key={key} value={availableLanguages[key].code}>
|
<option key={key} value={availableLanguages[key].code}>
|
||||||
{availableLanguages[key].display}
|
{availableLanguages[key].display}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -106,9 +106,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
`/api/v1/movie/${router.query.movieId}/ratings`
|
`/api/v1/movie/${router.query.movieId}/ratings`
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedCrew = useMemo(() => sortCrewPriority(data?.credits.crew ?? []), [
|
const sortedCrew = useMemo(
|
||||||
data,
|
() => sortCrewPriority(data?.credits.crew ?? []),
|
||||||
]);
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
|
|||||||
@@ -32,12 +32,10 @@ const PersonDetails: React.FC = () => {
|
|||||||
);
|
);
|
||||||
const [showBio, setShowBio] = useState(false);
|
const [showBio, setShowBio] = useState(false);
|
||||||
|
|
||||||
const {
|
const { data: combinedCredits, error: errorCombinedCredits } =
|
||||||
data: combinedCredits,
|
useSWR<PersonCombinedCreditsResponse>(
|
||||||
error: errorCombinedCredits,
|
`/api/v1/person/${router.query.personId}/combined_credits`
|
||||||
} = useSWR<PersonCombinedCreditsResponse>(
|
);
|
||||||
`/api/v1/person/${router.query.personId}/combined_credits`
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortedCast = useMemo(() => {
|
const sortedCast = useMemo(() => {
|
||||||
const grouped = groupBy(combinedCredits?.cast ?? [], 'id');
|
const grouped = groupBy(combinedCredits?.cast ?? [], 'id');
|
||||||
|
|||||||
@@ -1,38 +1,24 @@
|
|||||||
import {
|
import React, { useContext, useEffect } from 'react';
|
||||||
CheckIcon,
|
|
||||||
PencilIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
TrashIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@heroicons/react/solid';
|
|
||||||
import axios from 'axios';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
import { useToasts } from 'react-toast-notifications';
|
|
||||||
import useSWR, { mutate } from 'swr';
|
|
||||||
import {
|
|
||||||
MediaRequestStatus,
|
|
||||||
MediaStatus,
|
|
||||||
} from '../../../server/constants/media';
|
|
||||||
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
import type { MediaRequest } from '../../../server/entity/MediaRequest';
|
||||||
import type { MovieDetails } from '../../../server/models/Movie';
|
|
||||||
import type { TvDetails } from '../../../server/models/Tv';
|
import type { TvDetails } from '../../../server/models/Tv';
|
||||||
import { Permission, useUser } from '../../hooks/useUser';
|
import type { MovieDetails } from '../../../server/models/Movie';
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import useSWR from 'swr';
|
||||||
import { withProperties } from '../../utils/typeHelpers';
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
|
import { MediaRequestStatus } from '../../../server/constants/media';
|
||||||
import Badge from '../Common/Badge';
|
import Badge from '../Common/Badge';
|
||||||
|
import { useUser, Permission } from '../../hooks/useUser';
|
||||||
|
import axios from 'axios';
|
||||||
import Button from '../Common/Button';
|
import Button from '../Common/Button';
|
||||||
import CachedImage from '../Common/CachedImage';
|
import { withProperties } from '../../utils/typeHelpers';
|
||||||
import RequestModal from '../RequestModal';
|
import Link from 'next/link';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
import StatusBadge from '../StatusBadge';
|
import StatusBadge from '../StatusBadge';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
seasons: 'Seasons',
|
||||||
failedretry: 'Something went wrong while retrying the request.',
|
all: 'All',
|
||||||
mediaerror: 'The associated title for this request is no longer available.',
|
|
||||||
deleterequest: 'Delete Request',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||||
@@ -41,7 +27,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
|||||||
|
|
||||||
const RequestCardPlaceholder: React.FC = () => {
|
const RequestCardPlaceholder: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="relative p-4 bg-gray-700 rounded-xl w-72 sm:w-96 animate-pulse">
|
<div className="relative p-4 bg-gray-700 rounded-lg w-72 sm:w-96 animate-pulse">
|
||||||
<div className="w-20 sm:w-28">
|
<div className="w-20 sm:w-28">
|
||||||
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,45 +35,6 @@ const RequestCardPlaceholder: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RequestCardErrorProps {
|
|
||||||
mediaId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
|
|
||||||
const { hasPermission } = useUser();
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
|
||||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative p-4 bg-gray-800 ring-1 ring-red-500 rounded-xl w-72 sm:w-96">
|
|
||||||
<div className="w-20 sm:w-28">
|
|
||||||
<div className="w-full" style={{ paddingBottom: '150%' }}>
|
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center w-full h-full px-10">
|
|
||||||
<div className="w-full text-xs text-center text-gray-300 whitespace-normal sm:text-sm">
|
|
||||||
{intl.formatMessage(messages.mediaerror)}
|
|
||||||
</div>
|
|
||||||
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
|
|
||||||
<Button
|
|
||||||
buttonType="danger"
|
|
||||||
buttonSize="sm"
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => deleteRequest()}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RequestCardProps {
|
interface RequestCardProps {
|
||||||
request: MediaRequest;
|
request: MediaRequest;
|
||||||
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
|
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
|
||||||
@@ -98,16 +45,14 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
|||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
});
|
});
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { user, hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
const { addToast } = useToasts();
|
const { locale } = useContext(LanguageContext);
|
||||||
const [isRetrying, setRetrying] = useState(false);
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
|
||||||
const url =
|
const url =
|
||||||
request.type === 'movie'
|
request.type === 'movie'
|
||||||
? `/api/v1/movie/${request.media.tmdbId}`
|
? `/api/v1/movie/${request.media.tmdbId}`
|
||||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||||
inView ? `${url}` : null
|
inView ? `${url}?language=${locale}` : null
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
data: requestData,
|
data: requestData,
|
||||||
@@ -125,30 +70,6 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
|
||||||
await axios.delete(`/api/v1/request/${request.id}`);
|
|
||||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
|
||||||
};
|
|
||||||
|
|
||||||
const retryRequest = async () => {
|
|
||||||
setRetrying(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
revalidate();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
addToast(intl.formatMessage(messages.failedretry), {
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setRetrying(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (title && onTitleData) {
|
if (title && onTitleData) {
|
||||||
onTitleData(request.id, title);
|
onTitleData(request.id, title);
|
||||||
@@ -164,242 +85,157 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!requestData && !requestError) {
|
if (!requestData && !requestError) {
|
||||||
return <RequestCardError />;
|
return <RequestCardPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title || !requestData) {
|
if (!title || !requestData) {
|
||||||
return <RequestCardError mediaId={requestData?.media.id} />;
|
return <RequestCardPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<RequestModal
|
className="relative flex p-4 text-gray-400 bg-gray-800 bg-center bg-cover rounded-md w-72 sm:w-96"
|
||||||
show={showEditModal}
|
style={{
|
||||||
tmdbId={request.media.tmdbId}
|
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`,
|
||||||
type={request.type}
|
}}
|
||||||
is4k={request.is4k}
|
>
|
||||||
editRequest={request}
|
<div className="flex flex-col flex-1 min-w-0 pr-4">
|
||||||
onCancel={() => setShowEditModal(false)}
|
<h2 className="overflow-hidden text-base text-white cursor-pointer sm:text-lg overflow-ellipsis whitespace-nowrap hover:underline">
|
||||||
onComplete={() => {
|
<Link
|
||||||
revalidate();
|
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
|
||||||
setShowEditModal(false);
|
as={
|
||||||
}}
|
request.type === 'movie'
|
||||||
/>
|
? `/movie/${request.media.tmdbId}`
|
||||||
<div className="relative flex p-4 overflow-hidden text-gray-400 bg-gray-800 bg-center bg-cover shadow rounded-xl w-72 sm:w-96 ring-1 ring-gray-700">
|
: `/tv/${request.media.tmdbId}`
|
||||||
{title.backdropPath && (
|
}
|
||||||
<div className="absolute inset-0 z-0">
|
>
|
||||||
<CachedImage
|
{isMovie(title) ? title.title : title.name}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||||
|
<a className="flex items-center group">
|
||||||
|
<img
|
||||||
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
className="w-4 mr-1 rounded-full sm:mr-2 sm:w-5"
|
||||||
layout="fill"
|
|
||||||
objectFit="cover"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<span className="text-xs truncate sm:text-sm group-hover:underline">
|
||||||
className="absolute inset-0"
|
{requestData.requestedBy.displayName}
|
||||||
style={{
|
</span>
|
||||||
backgroundImage:
|
</a>
|
||||||
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
|
</Link>
|
||||||
}}
|
{requestData.media.status && (
|
||||||
|
<div className="mt-1 sm:mt-2">
|
||||||
|
<StatusBadge
|
||||||
|
status={
|
||||||
|
requestData.is4k
|
||||||
|
? requestData.media.status4k
|
||||||
|
: requestData.media.status
|
||||||
|
}
|
||||||
|
is4k={requestData.is4k}
|
||||||
|
inProgress={
|
||||||
|
(
|
||||||
|
requestData.media[
|
||||||
|
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||||
|
] ?? []
|
||||||
|
).length > 0
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative z-10 flex flex-col flex-1 min-w-0 pr-4">
|
{request.seasons.length > 0 && (
|
||||||
<div className="hidden text-xs font-medium text-white sm:flex">
|
<div className="items-center hidden mt-2 text-sm sm:flex">
|
||||||
{(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice(
|
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
|
||||||
0,
|
{!isMovie(title) &&
|
||||||
4
|
title.seasons.filter((season) => season.seasonNumber !== 0)
|
||||||
)}
|
.length === request.seasons.length ? (
|
||||||
</div>
|
<span className="mr-2 uppercase">
|
||||||
<Link
|
<Badge>{intl.formatMessage(messages.all)}</Badge>
|
||||||
href={
|
|
||||||
request.type === 'movie'
|
|
||||||
? `/movie/${requestData.media.tmdbId}`
|
|
||||||
: `/tv/${requestData.media.tmdbId}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a className="overflow-hidden text-base font-bold text-white sm:text-lg overflow-ellipsis whitespace-nowrap hover:underline">
|
|
||||||
{isMovie(title) ? title.title : title.name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
{hasPermission(
|
|
||||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
|
||||||
{ type: 'or' }
|
|
||||||
) && (
|
|
||||||
<div className="card-field">
|
|
||||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
|
||||||
<a className="flex items-center group">
|
|
||||||
<img
|
|
||||||
src={requestData.requestedBy.avatar}
|
|
||||||
alt=""
|
|
||||||
className="avatar-sm"
|
|
||||||
/>
|
|
||||||
<span className="truncate group-hover:underline">
|
|
||||||
{requestData.requestedBy.displayName}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isMovie(title) && request.seasons.length > 0 && (
|
|
||||||
<div className="items-center my-0.5 sm:my-1 text-sm hidden sm:flex">
|
|
||||||
<span className="mr-2 font-bold ">
|
|
||||||
{intl.formatMessage(messages.seasons, {
|
|
||||||
seasonCount:
|
|
||||||
title.seasons.filter((season) => season.seasonNumber !== 0)
|
|
||||||
.length === request.seasons.length
|
|
||||||
? 0
|
|
||||||
: request.seasons.length,
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
{title.seasons.filter((season) => season.seasonNumber !== 0)
|
|
||||||
.length === request.seasons.length ? (
|
|
||||||
<span className="mr-2 uppercase">
|
|
||||||
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-scroll hide-scrollbar">
|
|
||||||
{request.seasons.map((season) => (
|
|
||||||
<span key={`season-${season.id}`} className="mr-2">
|
|
||||||
<Badge>{season.seasonNumber}</Badge>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center mt-2 text-sm sm:mt-1">
|
|
||||||
<span className="hidden mr-2 font-bold sm:block">
|
|
||||||
{intl.formatMessage(globalMessages.status)}
|
|
||||||
</span>
|
|
||||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.UNKNOWN ||
|
|
||||||
requestData.status === MediaRequestStatus.DECLINED ? (
|
|
||||||
<Badge badgeType="danger">
|
|
||||||
{requestData.status === MediaRequestStatus.DECLINED
|
|
||||||
? intl.formatMessage(globalMessages.declined)
|
|
||||||
: intl.formatMessage(globalMessages.failed)}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
) : (
|
||||||
<StatusBadge
|
<div className="overflow-x-scroll hide-scrollbar">
|
||||||
status={
|
{request.seasons.map((season) => (
|
||||||
requestData.media[requestData.is4k ? 'status4k' : 'status']
|
<span key={`season-${season.id}`} className="mr-2">
|
||||||
}
|
<Badge>{season.seasonNumber}</Badge>
|
||||||
inProgress={
|
</span>
|
||||||
(
|
))}
|
||||||
requestData.media[
|
</div>
|
||||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
|
||||||
] ?? []
|
|
||||||
).length > 0
|
|
||||||
}
|
|
||||||
is4k={requestData.is4k}
|
|
||||||
plexUrl={requestData.media.plexUrl}
|
|
||||||
plexUrl4k={requestData.media.plexUrl4k}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end flex-1 space-x-2">
|
)}
|
||||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
{requestData.status === MediaRequestStatus.PENDING &&
|
||||||
MediaStatus.UNKNOWN &&
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
<div className="flex items-end flex-1">
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
<span className="mr-2">
|
||||||
<Button
|
<Button
|
||||||
buttonType="primary"
|
buttonType="success"
|
||||||
buttonSize="sm"
|
buttonSize="sm"
|
||||||
disabled={isRetrying}
|
onClick={() => modifyRequest('approve')}
|
||||||
onClick={() => retryRequest()}
|
|
||||||
>
|
>
|
||||||
<RefreshIcon
|
<svg
|
||||||
className={isRetrying ? 'animate-spin' : ''}
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
style={{ marginRight: '0', animationDirection: 'reverse' }}
|
fill="currentColor"
|
||||||
/>
|
viewBox="0 0 20 20"
|
||||||
<span className="hidden ml-1.5 sm:block">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{intl.formatMessage(globalMessages.retry)}
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.approve)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</span>
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
<span>
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
buttonType="success"
|
|
||||||
buttonSize="sm"
|
|
||||||
onClick={() => modifyRequest('approve')}
|
|
||||||
>
|
|
||||||
<CheckIcon style={{ marginRight: '0' }} />
|
|
||||||
<span className="hidden ml-1.5 sm:block">
|
|
||||||
{intl.formatMessage(globalMessages.approve)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
buttonType="danger"
|
|
||||||
buttonSize="sm"
|
|
||||||
onClick={() => modifyRequest('decline')}
|
|
||||||
>
|
|
||||||
<XIcon style={{ marginRight: '0' }} />
|
|
||||||
<span className="hidden ml-1.5 sm:block">
|
|
||||||
{intl.formatMessage(globalMessages.decline)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
|
||||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
|
||||||
requestData.requestedBy.id === user?.id &&
|
|
||||||
(requestData.type === 'tv' ||
|
|
||||||
hasPermission(Permission.REQUEST_ADVANCED)) && (
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
buttonSize="sm"
|
|
||||||
onClick={() => setShowEditModal(true)}
|
|
||||||
className={`${
|
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) ? 'sm:hidden' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<PencilIcon style={{ marginRight: '0' }} />
|
|
||||||
<span className="hidden ml-1.5 sm:block">
|
|
||||||
{intl.formatMessage(globalMessages.edit)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
|
||||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
|
||||||
requestData.requestedBy.id === user?.id && (
|
|
||||||
<Button
|
<Button
|
||||||
buttonType="danger"
|
buttonType="danger"
|
||||||
buttonSize="sm"
|
buttonSize="sm"
|
||||||
onClick={() => deleteRequest()}
|
onClick={() => modifyRequest('decline')}
|
||||||
>
|
>
|
||||||
<XIcon style={{ marginRight: '0' }} />
|
<svg
|
||||||
<span className="hidden ml-1.5 sm:block">
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
{intl.formatMessage(globalMessages.cancel)}
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.decline)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 w-20 sm:w-28">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
|
||||||
|
as={
|
||||||
request.type === 'movie'
|
request.type === 'movie'
|
||||||
? `/movie/${requestData.media.tmdbId}`
|
? `/movie/${request.media.tmdbId}`
|
||||||
: `/tv/${requestData.media.tmdbId}`
|
: `/tv/${request.media.tmdbId}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<a className="flex-shrink-0 w-20 overflow-hidden transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md">
|
<img
|
||||||
<CachedImage
|
src={
|
||||||
src={
|
title.posterPath
|
||||||
title.posterPath
|
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
: '/images/overseerr_poster_not_found.png'
|
||||||
: '/images/overseerr_poster_not_found.png'
|
}
|
||||||
}
|
alt=""
|
||||||
alt=""
|
className="w-20 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md"
|
||||||
layout="responsive"
|
/>
|
||||||
width={600}
|
|
||||||
height={900}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
PencilIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
TrashIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@heroicons/react/solid';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React, { useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
import {
|
||||||
|
defineMessages,
|
||||||
|
FormattedDate,
|
||||||
|
FormattedRelativeTime,
|
||||||
|
useIntl,
|
||||||
|
} from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
@@ -19,70 +17,25 @@ import {
|
|||||||
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
|
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
|
||||||
import type { MovieDetails } from '../../../../server/models/Movie';
|
import type { MovieDetails } from '../../../../server/models/Movie';
|
||||||
import type { TvDetails } from '../../../../server/models/Tv';
|
import type { TvDetails } from '../../../../server/models/Tv';
|
||||||
|
import { LanguageContext } from '../../../context/LanguageContext';
|
||||||
import { Permission, useUser } from '../../../hooks/useUser';
|
import { Permission, useUser } from '../../../hooks/useUser';
|
||||||
import globalMessages from '../../../i18n/globalMessages';
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
import Badge from '../../Common/Badge';
|
import Badge from '../../Common/Badge';
|
||||||
import Button from '../../Common/Button';
|
import Button from '../../Common/Button';
|
||||||
import CachedImage from '../../Common/CachedImage';
|
import Table from '../../Common/Table';
|
||||||
import ConfirmButton from '../../Common/ConfirmButton';
|
|
||||||
import RequestModal from '../../RequestModal';
|
import RequestModal from '../../RequestModal';
|
||||||
import StatusBadge from '../../StatusBadge';
|
import StatusBadge from '../../StatusBadge';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
seasons: 'Seasons',
|
||||||
|
notavailable: 'N/A',
|
||||||
failedretry: 'Something went wrong while retrying the request.',
|
failedretry: 'Something went wrong while retrying the request.',
|
||||||
requested: 'Requested',
|
|
||||||
requesteddate: 'Requested',
|
|
||||||
modified: 'Modified',
|
|
||||||
modifieduserdate: '{date} by {user}',
|
|
||||||
mediaerror: 'The associated title for this request is no longer available.',
|
|
||||||
editrequest: 'Edit Request',
|
|
||||||
deleterequest: 'Delete Request',
|
|
||||||
cancelRequest: 'Cancel Request',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||||
return (movie as MovieDetails).title !== undefined;
|
return (movie as MovieDetails).title !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RequestItemErroProps {
|
|
||||||
mediaId?: number;
|
|
||||||
revalidateList: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RequestItemError: React.FC<RequestItemErroProps> = ({
|
|
||||||
mediaId,
|
|
||||||
revalidateList,
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { hasPermission } = useUser();
|
|
||||||
|
|
||||||
const deleteRequest = async () => {
|
|
||||||
await axios.delete(`/api/v1/media/${mediaId}`);
|
|
||||||
revalidateList();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-64 px-10 bg-gray-800 lg:flex-row ring-1 ring-red-500 rounded-xl xl:h-32">
|
|
||||||
<span className="text-sm text-center text-gray-300 lg:text-left">
|
|
||||||
{intl.formatMessage(messages.mediaerror)}
|
|
||||||
</span>
|
|
||||||
{hasPermission(Permission.MANAGE_REQUESTS) && mediaId && (
|
|
||||||
<div className="mt-4 lg:ml-4 lg:mt-0">
|
|
||||||
<Button
|
|
||||||
buttonType="danger"
|
|
||||||
buttonSize="sm"
|
|
||||||
onClick={() => deleteRequest()}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RequestItemProps {
|
interface RequestItemProps {
|
||||||
request: MediaRequest;
|
request: MediaRequest;
|
||||||
revalidateList: () => void;
|
revalidateList: () => void;
|
||||||
@@ -97,21 +50,23 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
|||||||
});
|
});
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { user, hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
const url =
|
const url =
|
||||||
request.type === 'movie'
|
request.type === 'movie'
|
||||||
? `/api/v1/movie/${request.media.tmdbId}`
|
? `/api/v1/movie/${request.media.tmdbId}`
|
||||||
: `/api/v1/tv/${request.media.tmdbId}`;
|
: `/api/v1/tv/${request.media.tmdbId}`;
|
||||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||||
inView ? `${url}` : null
|
inView ? `${url}?language=${locale}` : null
|
||||||
);
|
|
||||||
const { data: requestData, revalidate, mutate } = useSWR<MediaRequest>(
|
|
||||||
`/api/v1/request/${request.id}`,
|
|
||||||
{
|
|
||||||
initialData: request,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
data: requestData,
|
||||||
|
revalidate,
|
||||||
|
mutate,
|
||||||
|
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
|
||||||
|
initialData: request,
|
||||||
|
});
|
||||||
|
|
||||||
const [isRetrying, setRetrying] = useState(false);
|
const [isRetrying, setRetrying] = useState(false);
|
||||||
|
|
||||||
@@ -147,24 +102,22 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
|||||||
|
|
||||||
if (!title && !error) {
|
if (!title && !error) {
|
||||||
return (
|
return (
|
||||||
<div
|
<tr className="w-full h-24 animate-pulse" ref={ref}>
|
||||||
className="w-full h-64 bg-gray-800 rounded-xl xl:h-32 animate-pulse"
|
<td colSpan={6}></td>
|
||||||
ref={ref}
|
</tr>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title || !requestData) {
|
if (!title || !requestData) {
|
||||||
return (
|
return (
|
||||||
<RequestItemError
|
<tr className="w-full h-24 animate-pulse">
|
||||||
mediaId={requestData?.media.id}
|
<td colSpan={6}></td>
|
||||||
revalidateList={revalidateList}
|
</tr>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<tr className="relative w-full h-24 p-2">
|
||||||
<RequestModal
|
<RequestModal
|
||||||
show={showEditModal}
|
show={showEditModal}
|
||||||
tmdbId={request.media.tmdbId}
|
tmdbId={request.media.tmdbId}
|
||||||
@@ -177,26 +130,28 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
|||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex flex-col justify-between w-full py-4 overflow-hidden text-gray-400 bg-gray-800 shadow-md ring-1 ring-gray-700 rounded-xl xl:h-32 xl:flex-row">
|
<Table.TD>
|
||||||
{title.backdropPath && (
|
<div className="flex items-center">
|
||||||
<div className="absolute inset-0 z-0 w-full bg-center bg-cover xl:w-2/3">
|
<Link
|
||||||
<CachedImage
|
href={
|
||||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
request.type === 'movie'
|
||||||
alt=""
|
? `/movie/${request.media.tmdbId}`
|
||||||
layout="fill"
|
: `/tv/${request.media.tmdbId}`
|
||||||
objectFit="cover"
|
}
|
||||||
/>
|
>
|
||||||
<div
|
<a className="flex-shrink-0 hidden mr-4 sm:block">
|
||||||
className="absolute inset-0"
|
<img
|
||||||
style={{
|
src={
|
||||||
backgroundImage:
|
title.posterPath
|
||||||
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
|
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
}}
|
: '/images/overseerr_poster_not_found.png'
|
||||||
/>
|
}
|
||||||
</div>
|
alt=""
|
||||||
)}
|
className="w-12 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer transform-gpu hover:scale-105 hover:shadow-md"
|
||||||
<div className="relative flex flex-col justify-between w-full overflow-hidden sm:flex-row">
|
/>
|
||||||
<div className="relative z-10 flex items-center w-full pl-4 pr-4 overflow-hidden xl:w-7/12 2xl:w-2/3 sm:pr-0">
|
</a>
|
||||||
|
</Link>
|
||||||
|
<div className="flex-shrink overflow-hidden">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
requestData.type === 'movie'
|
requestData.type === 'movie'
|
||||||
@@ -204,285 +159,219 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
|||||||
: `/tv/${requestData.media.tmdbId}`
|
: `/tv/${requestData.media.tmdbId}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<a className="relative flex-shrink-0 w-12 h-auto overflow-hidden transition duration-300 scale-100 rounded-md sm:w-14 transform-gpu hover:scale-105">
|
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
|
||||||
<CachedImage
|
{isMovie(title) ? title.title : title.name}
|
||||||
src={
|
|
||||||
title.posterPath
|
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
|
||||||
: '/images/overseerr_poster_not_found.png'
|
|
||||||
}
|
|
||||||
alt=""
|
|
||||||
layout="responsive"
|
|
||||||
width={600}
|
|
||||||
height={900}
|
|
||||||
objectFit="cover"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-col justify-center pl-2 overflow-hidden xl:pl-4">
|
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||||
<div className="font-medium pt-0.5 sm:pt-1 text-xs text-white">
|
<a className="flex items-center mt-1">
|
||||||
{(isMovie(title)
|
<img
|
||||||
? title.releaseDate
|
src={requestData.requestedBy.avatar}
|
||||||
: title.firstAirDate
|
alt=""
|
||||||
)?.slice(0, 4)}
|
className="w-5 mr-2 rounded-full"
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
requestData.type === 'movie'
|
|
||||||
? `/movie/${requestData.media.tmdbId}`
|
|
||||||
: `/tv/${requestData.media.tmdbId}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a className="min-w-0 mr-2 text-lg font-bold text-white truncate xl:text-xl hover:underline">
|
|
||||||
{isMovie(title) ? title.title : title.name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
{!isMovie(title) && request.seasons.length > 0 && (
|
|
||||||
<div className="card-field">
|
|
||||||
<span className="card-field-name">
|
|
||||||
{intl.formatMessage(messages.seasons, {
|
|
||||||
seasonCount:
|
|
||||||
title.seasons.filter(
|
|
||||||
(season) => season.seasonNumber !== 0
|
|
||||||
).length === request.seasons.length
|
|
||||||
? 0
|
|
||||||
: request.seasons.length,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
{title.seasons.filter((season) => season.seasonNumber !== 0)
|
|
||||||
.length === request.seasons.length ? (
|
|
||||||
<span className="mr-2 uppercase">
|
|
||||||
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className="flex overflow-x-scroll hide-scrollbar flex-nowrap">
|
|
||||||
{request.seasons.map((season) => (
|
|
||||||
<span key={`season-${season.id}`} className="mr-2">
|
|
||||||
<Badge>{season.seasonNumber}</Badge>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="z-10 flex flex-col justify-center w-full pr-4 mt-4 ml-4 overflow-hidden text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
|
|
||||||
<div className="card-field">
|
|
||||||
<span className="card-field-name">
|
|
||||||
{intl.formatMessage(globalMessages.status)}
|
|
||||||
</span>
|
|
||||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.UNKNOWN ||
|
|
||||||
requestData.status === MediaRequestStatus.DECLINED ? (
|
|
||||||
<Badge badgeType="danger">
|
|
||||||
{requestData.status === MediaRequestStatus.DECLINED
|
|
||||||
? intl.formatMessage(globalMessages.declined)
|
|
||||||
: intl.formatMessage(globalMessages.failed)}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<StatusBadge
|
|
||||||
status={
|
|
||||||
requestData.media[requestData.is4k ? 'status4k' : 'status']
|
|
||||||
}
|
|
||||||
inProgress={
|
|
||||||
(
|
|
||||||
requestData.media[
|
|
||||||
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
|
||||||
] ?? []
|
|
||||||
).length > 0
|
|
||||||
}
|
|
||||||
is4k={requestData.is4k}
|
|
||||||
plexUrl={requestData.media.plexUrl}
|
|
||||||
plexUrl4k={requestData.media.plexUrl4k}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<span className="text-sm hover:underline">
|
||||||
</div>
|
{requestData.requestedBy.displayName}
|
||||||
<div className="card-field">
|
|
||||||
{hasPermission(
|
|
||||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
|
||||||
{ type: 'or' }
|
|
||||||
) ? (
|
|
||||||
<>
|
|
||||||
<span className="card-field-name">
|
|
||||||
{intl.formatMessage(messages.requested)}
|
|
||||||
</span>
|
|
||||||
<span className="flex text-sm text-gray-300 truncate">
|
|
||||||
{intl.formatMessage(messages.modifieduserdate, {
|
|
||||||
date: (
|
|
||||||
<FormattedRelativeTime
|
|
||||||
value={Math.floor(
|
|
||||||
(new Date(requestData.createdAt).getTime() -
|
|
||||||
Date.now()) /
|
|
||||||
1000
|
|
||||||
)}
|
|
||||||
updateIntervalInSeconds={1}
|
|
||||||
numeric="auto"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
user: (
|
|
||||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
|
||||||
<a className="flex items-center truncate group">
|
|
||||||
<img
|
|
||||||
src={requestData.requestedBy.avatar}
|
|
||||||
alt=""
|
|
||||||
className="ml-1.5 avatar-sm"
|
|
||||||
/>
|
|
||||||
<span className="text-sm truncate group-hover:underline">
|
|
||||||
{requestData.requestedBy.displayName}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="card-field-name">
|
|
||||||
{intl.formatMessage(messages.requesteddate)}
|
|
||||||
</span>
|
|
||||||
<span className="flex text-sm text-gray-300 truncate">
|
|
||||||
<FormattedRelativeTime
|
|
||||||
value={Math.floor(
|
|
||||||
(new Date(requestData.createdAt).getTime() -
|
|
||||||
Date.now()) /
|
|
||||||
1000
|
|
||||||
)}
|
|
||||||
updateIntervalInSeconds={1}
|
|
||||||
numeric="auto"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{requestData.modifiedBy && (
|
|
||||||
<div className="card-field">
|
|
||||||
<span className="card-field-name">
|
|
||||||
{intl.formatMessage(messages.modified)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="flex text-sm text-gray-300 truncate">
|
</a>
|
||||||
{intl.formatMessage(messages.modifieduserdate, {
|
</Link>
|
||||||
date: (
|
{requestData.seasons.length > 0 && (
|
||||||
<FormattedRelativeTime
|
<div className="items-center hidden mt-2 text-sm sm:flex">
|
||||||
value={Math.floor(
|
<span className="mr-2">
|
||||||
(new Date(requestData.updatedAt).getTime() -
|
{intl.formatMessage(messages.seasons)}
|
||||||
Date.now()) /
|
|
||||||
1000
|
|
||||||
)}
|
|
||||||
updateIntervalInSeconds={1}
|
|
||||||
numeric="auto"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
user: (
|
|
||||||
<Link href={`/users/${requestData.modifiedBy.id}`}>
|
|
||||||
<a className="flex items-center truncate group">
|
|
||||||
<img
|
|
||||||
src={requestData.modifiedBy.avatar}
|
|
||||||
alt=""
|
|
||||||
className="ml-1.5 avatar-sm"
|
|
||||||
/>
|
|
||||||
<span className="text-sm truncate group-hover:underline">
|
|
||||||
{requestData.modifiedBy.displayName}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
|
{requestData.seasons.map((season) => (
|
||||||
|
<span key={`season-${season.id}`} className="mr-2">
|
||||||
|
<Badge>{season.seasonNumber}</Badge>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="z-10 flex flex-col justify-center w-full pl-4 pr-4 mt-4 space-y-2 xl:mt-0 xl:items-end xl:w-96 xl:pl-0">
|
</Table.TD>
|
||||||
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
<Table.TD>
|
||||||
MediaStatus.UNKNOWN &&
|
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||||
requestData.status !== MediaRequestStatus.DECLINED &&
|
MediaStatus.UNKNOWN ||
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
requestData.status === MediaRequestStatus.DECLINED ? (
|
||||||
<Button
|
<Badge badgeType="danger">
|
||||||
className="w-full"
|
{requestData.status === MediaRequestStatus.DECLINED
|
||||||
buttonType="primary"
|
? intl.formatMessage(globalMessages.declined)
|
||||||
disabled={isRetrying}
|
: intl.formatMessage(globalMessages.failed)}
|
||||||
onClick={() => retryRequest()}
|
</Badge>
|
||||||
>
|
) : (
|
||||||
<RefreshIcon
|
<StatusBadge
|
||||||
className={isRetrying ? 'animate-spin' : ''}
|
status={requestData.media[requestData.is4k ? 'status4k' : 'status']}
|
||||||
style={{ animationDirection: 'reverse' }}
|
inProgress={
|
||||||
|
(
|
||||||
|
requestData.media[
|
||||||
|
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
|
||||||
|
] ?? []
|
||||||
|
).length > 0
|
||||||
|
}
|
||||||
|
is4k={requestData.is4k}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Table.TD>
|
||||||
|
<Table.TD>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm text-gray-300">
|
||||||
|
<FormattedDate value={requestData.createdAt} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Table.TD>
|
||||||
|
<Table.TD>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{requestData.modifiedBy ? (
|
||||||
|
<span className="text-sm text-gray-300">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={requestData.modifiedBy.avatar}
|
||||||
|
alt=""
|
||||||
|
className="w-5 mr-2 rounded-full"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span className="text-sm">
|
||||||
{intl.formatMessage(
|
{requestData.modifiedBy.displayName} (
|
||||||
isRetrying ? globalMessages.retrying : globalMessages.retry
|
<FormattedRelativeTime
|
||||||
)}
|
value={Math.floor(
|
||||||
</span>
|
(new Date(requestData.updatedAt).getTime() - Date.now()) /
|
||||||
</Button>
|
1000
|
||||||
)}
|
)}
|
||||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
updateIntervalInSeconds={1}
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
/>
|
||||||
<ConfirmButton
|
)
|
||||||
onClick={() => deleteRequest()}
|
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
|
||||||
</ConfirmButton>
|
|
||||||
)}
|
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
|
||||||
<div className="flex flex-row w-full space-x-2">
|
|
||||||
<span className="w-full">
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
buttonType="success"
|
|
||||||
onClick={() => modifyRequest('approve')}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
<span>{intl.formatMessage(globalMessages.approve)}</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
<span className="w-full">
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
buttonType="danger"
|
|
||||||
onClick={() => modifyRequest('decline')}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span>{intl.formatMessage(globalMessages.decline)}</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</span>
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
) : (
|
||||||
(hasPermission(Permission.MANAGE_REQUESTS) ||
|
<span className="text-sm text-gray-300">N/A</span>
|
||||||
(requestData.requestedBy.id === user?.id &&
|
)}
|
||||||
(requestData.type === 'tv' ||
|
</div>
|
||||||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
|
</Table.TD>
|
||||||
<span className="w-full">
|
<Table.TD alignText="right">
|
||||||
|
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.UNKNOWN &&
|
||||||
|
requestData.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
buttonType="primary"
|
||||||
|
buttonSize="sm"
|
||||||
|
disabled={isRetrying}
|
||||||
|
onClick={() => retryRequest()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18px"
|
||||||
|
height="18px"
|
||||||
|
>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.retry)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||||
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
<Button
|
||||||
|
buttonType="danger"
|
||||||
|
buttonSize="sm"
|
||||||
|
onClick={() => deleteRequest()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.delete)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{requestData.status === MediaRequestStatus.PENDING &&
|
||||||
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
<>
|
||||||
|
<span className="mr-2">
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
buttonType="success"
|
||||||
buttonType="primary"
|
buttonSize="sm"
|
||||||
onClick={() => setShowEditModal(true)}
|
onClick={() => modifyRequest('approve')}
|
||||||
>
|
>
|
||||||
<PencilIcon />
|
<svg
|
||||||
<span>{intl.formatMessage(messages.editrequest)}</span>
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.approve)}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="mr-2">
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
<Button
|
||||||
!hasPermission(Permission.MANAGE_REQUESTS) &&
|
buttonType="danger"
|
||||||
requestData.requestedBy.id === user?.id && (
|
buttonSize="sm"
|
||||||
<ConfirmButton
|
onClick={() => modifyRequest('decline')}
|
||||||
onClick={() => deleteRequest()}
|
>
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
<svg
|
||||||
className="w-full"
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
>
|
fill="currentColor"
|
||||||
<XIcon />
|
viewBox="0 0 20 20"
|
||||||
<span>{intl.formatMessage(messages.cancelRequest)}</span>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</ConfirmButton>
|
>
|
||||||
)}
|
<path
|
||||||
</div>
|
fillRule="evenodd"
|
||||||
</div>
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
</>
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.decline)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
buttonSize="sm"
|
||||||
|
onClick={() => setShowEditModal(true)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.edit)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table.TD>
|
||||||
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,94 +1,51 @@
|
|||||||
import {
|
import React, { useState } from 'react';
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
FilterIcon,
|
|
||||||
SortDescendingIcon,
|
|
||||||
} from '@heroicons/react/solid';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
|
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
|
||||||
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
|
|
||||||
import { useUser } from '../../hooks/useUser';
|
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
|
||||||
import Button from '../Common/Button';
|
|
||||||
import Header from '../Common/Header';
|
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||||
import PageTitle from '../Common/PageTitle';
|
|
||||||
import RequestItem from './RequestItem';
|
import RequestItem from './RequestItem';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
import Table from '../Common/Table';
|
||||||
|
import Button from '../Common/Button';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import PageTitle from '../Common/PageTitle';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
requests: 'Requests',
|
requests: 'Requests',
|
||||||
|
mediaInfo: 'Media Info',
|
||||||
|
status: 'Status',
|
||||||
|
requestedAt: 'Requested At',
|
||||||
|
modifiedBy: 'Last Modified By',
|
||||||
|
showingresults:
|
||||||
|
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
|
||||||
|
resultsperpage: 'Display {pageSize} results per page',
|
||||||
|
next: 'Next',
|
||||||
|
previous: 'Previous',
|
||||||
|
filterAll: 'All',
|
||||||
|
filterPending: 'Pending',
|
||||||
|
filterApproved: 'Approved',
|
||||||
|
filterAvailable: 'Available',
|
||||||
|
filterProcessing: 'Processing',
|
||||||
|
noresults: 'No results.',
|
||||||
showallrequests: 'Show All Requests',
|
showallrequests: 'Show All Requests',
|
||||||
sortAdded: 'Request Date',
|
sortAdded: 'Request Date',
|
||||||
sortModified: 'Last Modified',
|
sortModified: 'Last Modified',
|
||||||
});
|
});
|
||||||
|
|
||||||
enum Filter {
|
type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available';
|
||||||
ALL = 'all',
|
|
||||||
PENDING = 'pending',
|
|
||||||
APPROVED = 'approved',
|
|
||||||
PROCESSING = 'processing',
|
|
||||||
AVAILABLE = 'available',
|
|
||||||
UNAVAILABLE = 'unavailable',
|
|
||||||
}
|
|
||||||
|
|
||||||
type Sort = 'added' | 'modified';
|
type Sort = 'added' | 'modified';
|
||||||
|
|
||||||
const RequestList: React.FC = () => {
|
const RequestList: React.FC = () => {
|
||||||
const router = useRouter();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { user } = useUser({
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
id: Number(router.query.userId),
|
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
|
||||||
});
|
|
||||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||||
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
|
||||||
|
|
||||||
const page = router.query.page ? Number(router.query.page) : 1;
|
|
||||||
const pageIndex = page - 1;
|
|
||||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
|
||||||
|
|
||||||
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
|
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
|
||||||
`/api/v1/request?take=${currentPageSize}&skip=${
|
`/api/v1/request?take=${currentPageSize}&skip=${
|
||||||
pageIndex * currentPageSize
|
pageIndex * currentPageSize
|
||||||
}&filter=${currentFilter}&sort=${currentSort}${
|
}&filter=${currentFilter}&sort=${currentSort}`
|
||||||
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore last set filter values on component mount
|
|
||||||
useEffect(() => {
|
|
||||||
const filterString = window.localStorage.getItem('rl-filter-settings');
|
|
||||||
|
|
||||||
if (filterString) {
|
|
||||||
const filterSettings = JSON.parse(filterString);
|
|
||||||
|
|
||||||
setCurrentFilter(filterSettings.currentFilter);
|
|
||||||
setCurrentSort(filterSettings.currentSort);
|
|
||||||
setCurrentPageSize(filterSettings.currentPageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If filter value is provided in query, use that instead
|
|
||||||
if (Object.values(Filter).includes(router.query.filter as Filter)) {
|
|
||||||
setCurrentFilter(router.query.filter as Filter);
|
|
||||||
}
|
|
||||||
}, [router.query.filter]);
|
|
||||||
|
|
||||||
// Set filter values to local storage any time they are changed
|
|
||||||
useEffect(() => {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
'rl-filter-settings',
|
|
||||||
JSON.stringify({
|
|
||||||
currentFilter,
|
|
||||||
currentSort,
|
|
||||||
currentPageSize,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [currentFilter, currentSort, currentPageSize]);
|
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
@@ -102,81 +59,73 @@ const RequestList: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle title={intl.formatMessage(messages.requests)} />
|
||||||
title={[
|
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
|
||||||
intl.formatMessage(messages.requests),
|
<Header>{intl.formatMessage(messages.requests)}</Header>
|
||||||
router.query.userId ? user?.displayName : '',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col justify-between mb-4 lg:items-end lg:flex-row">
|
|
||||||
<Header
|
|
||||||
subtext={
|
|
||||||
router.query.userId ? (
|
|
||||||
<Link href={`/users/${user?.id}`}>
|
|
||||||
<a className="hover:underline">{user?.displayName}</a>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.requests)}
|
|
||||||
</Header>
|
|
||||||
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
|
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
|
||||||
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
|
||||||
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
||||||
<FilterIcon className="w-6 h-6" />
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
id="filter"
|
id="filter"
|
||||||
name="filter"
|
name="filter"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
setPageIndex(0);
|
||||||
setCurrentFilter(e.target.value as Filter);
|
setCurrentFilter(e.target.value as Filter);
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: router.query.userId
|
|
||||||
? { userId: router.query.userId }
|
|
||||||
: {},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
value={currentFilter}
|
value={currentFilter}
|
||||||
className="rounded-r-only"
|
className="rounded-r-only"
|
||||||
>
|
>
|
||||||
<option value="all">
|
<option value="all">
|
||||||
{intl.formatMessage(globalMessages.all)}
|
{intl.formatMessage(messages.filterAll)}
|
||||||
</option>
|
</option>
|
||||||
<option value="pending">
|
<option value="pending">
|
||||||
{intl.formatMessage(globalMessages.pending)}
|
{intl.formatMessage(messages.filterPending)}
|
||||||
</option>
|
</option>
|
||||||
<option value="approved">
|
<option value="approved">
|
||||||
{intl.formatMessage(globalMessages.approved)}
|
{intl.formatMessage(messages.filterApproved)}
|
||||||
</option>
|
</option>
|
||||||
<option value="processing">
|
<option value="processing">
|
||||||
{intl.formatMessage(globalMessages.processing)}
|
{intl.formatMessage(messages.filterProcessing)}
|
||||||
</option>
|
</option>
|
||||||
<option value="available">
|
<option value="available">
|
||||||
{intl.formatMessage(globalMessages.available)}
|
{intl.formatMessage(messages.filterAvailable)}
|
||||||
</option>
|
|
||||||
<option value="unavailable">
|
|
||||||
{intl.formatMessage(globalMessages.unavailable)}
|
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
|
<div className="flex flex-grow mb-2 sm:mb-0 lg:flex-grow-0">
|
||||||
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
|
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default sm:text-sm rounded-l-md">
|
||||||
<SortDescendingIcon className="w-6 h-6" />
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
id="sort"
|
id="sort"
|
||||||
name="sort"
|
name="sort"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
setPageIndex(0);
|
||||||
|
setCurrentSort(e.target.value as Sort);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setPageIndex(0);
|
||||||
setCurrentSort(e.target.value as Sort);
|
setCurrentSort(e.target.value as Sort);
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: router.query.userId
|
|
||||||
? { userId: router.query.userId }
|
|
||||||
: {},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
value={currentSort}
|
value={currentSort}
|
||||||
className="rounded-r-only"
|
className="rounded-r-only"
|
||||||
@@ -191,104 +140,114 @@ const RequestList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{data.results.map((request) => {
|
<Table>
|
||||||
return (
|
<thead>
|
||||||
<div className="py-2" key={`request-list-${request.id}`}>
|
<tr>
|
||||||
<RequestItem
|
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
|
||||||
request={request}
|
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
|
||||||
revalidateList={() => revalidate()}
|
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
|
||||||
/>
|
<Table.TH>{intl.formatMessage(messages.modifiedBy)}</Table.TH>
|
||||||
</div>
|
<Table.TH></Table.TH>
|
||||||
);
|
</tr>
|
||||||
})}
|
</thead>
|
||||||
|
<Table.TBody>
|
||||||
|
{data.results.map((request) => {
|
||||||
|
return (
|
||||||
|
<RequestItem
|
||||||
|
request={request}
|
||||||
|
key={`request-list-${request.id}`}
|
||||||
|
revalidateList={() => revalidate()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{data.results.length === 0 && (
|
{data.results.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center w-full py-24 text-white">
|
<tr className="relative h-24 p-2 text-white">
|
||||||
<span className="text-2xl text-gray-400">
|
<Table.TD colSpan={6} noPadding>
|
||||||
{intl.formatMessage(globalMessages.noresults)}
|
<div className="flex flex-col items-center justify-center w-screen p-6 lg:w-full">
|
||||||
</span>
|
<span className="text-base">
|
||||||
{currentFilter !== Filter.ALL && (
|
{intl.formatMessage(messages.noresults)}
|
||||||
<div className="mt-4">
|
</span>
|
||||||
<Button
|
{currentFilter !== 'all' && (
|
||||||
buttonType="primary"
|
<div className="mt-4">
|
||||||
onClick={() => setCurrentFilter(Filter.ALL)}
|
<Button
|
||||||
>
|
buttonSize="sm"
|
||||||
{intl.formatMessage(messages.showallrequests)}
|
buttonType="primary"
|
||||||
</Button>
|
onClick={() => setCurrentFilter('all')}
|
||||||
</div>
|
>
|
||||||
|
{intl.formatMessage(messages.showallrequests)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Table.TD>
|
||||||
|
</tr>
|
||||||
)}
|
)}
|
||||||
</div>
|
<tr className="bg-gray-700">
|
||||||
)}
|
<Table.TD colSpan={6} noPadding>
|
||||||
<div className="actions">
|
<nav
|
||||||
<nav
|
className="flex flex-col items-center w-screen px-6 py-3 space-x-4 space-y-3 sm:space-y-0 sm:flex-row lg:w-full"
|
||||||
className="flex flex-col items-center mb-3 space-y-3 sm:space-y-0 sm:flex-row"
|
aria-label="Pagination"
|
||||||
aria-label="Pagination"
|
>
|
||||||
>
|
<div className="hidden lg:flex lg:flex-1">
|
||||||
<div className="hidden lg:flex lg:flex-1">
|
<p className="text-sm">
|
||||||
<p className="text-sm">
|
{data.results.length > 0 &&
|
||||||
{data.results.length > 0 &&
|
intl.formatMessage(messages.showingresults, {
|
||||||
intl.formatMessage(globalMessages.showingresults, {
|
from: pageIndex * currentPageSize + 1,
|
||||||
from: pageIndex * currentPageSize + 1,
|
to:
|
||||||
to:
|
data.results.length < currentPageSize
|
||||||
data.results.length < currentPageSize
|
? pageIndex * currentPageSize + data.results.length
|
||||||
? pageIndex * currentPageSize + data.results.length
|
: (pageIndex + 1) * currentPageSize,
|
||||||
: (pageIndex + 1) * currentPageSize,
|
total: data.pageInfo.results,
|
||||||
total: data.pageInfo.results,
|
strong: function strong(msg) {
|
||||||
strong: function strong(msg) {
|
return <span className="font-medium">{msg}</span>;
|
||||||
return <span className="font-medium">{msg}</span>;
|
},
|
||||||
},
|
})}
|
||||||
})}
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
||||||
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
<span className="items-center -mt-3 text-sm sm:-ml-4 lg:ml-0 sm:mt-0">
|
||||||
<span className="items-center -mt-3 text-sm truncate sm:mt-0">
|
{intl.formatMessage(messages.resultsperpage, {
|
||||||
{intl.formatMessage(globalMessages.resultsperpage, {
|
pageSize: (
|
||||||
pageSize: (
|
<select
|
||||||
<select
|
id="pageSize"
|
||||||
id="pageSize"
|
name="pageSize"
|
||||||
name="pageSize"
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
setPageIndex(0);
|
||||||
setCurrentPageSize(Number(e.target.value));
|
setCurrentPageSize(Number(e.target.value));
|
||||||
router
|
}}
|
||||||
.push({
|
value={currentPageSize}
|
||||||
pathname: router.pathname,
|
className="inline short"
|
||||||
query: router.query.userId
|
>
|
||||||
? { userId: router.query.userId }
|
<option value="5">5</option>
|
||||||
: {},
|
<option value="10">10</option>
|
||||||
})
|
<option value="25">25</option>
|
||||||
.then(() => window.scrollTo(0, 0));
|
<option value="50">50</option>
|
||||||
}}
|
<option value="100">100</option>
|
||||||
value={currentPageSize}
|
</select>
|
||||||
className="inline short"
|
),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
|
||||||
|
<Button
|
||||||
|
disabled={!hasPrevPage}
|
||||||
|
onClick={() => setPageIndex((current) => current - 1)}
|
||||||
>
|
>
|
||||||
<option value="5">5</option>
|
{intl.formatMessage(messages.previous)}
|
||||||
<option value="10">10</option>
|
</Button>
|
||||||
<option value="25">25</option>
|
<Button
|
||||||
<option value="50">50</option>
|
disabled={!hasNextPage}
|
||||||
<option value="100">100</option>
|
onClick={() => setPageIndex((current) => current + 1)}
|
||||||
</select>
|
>
|
||||||
),
|
{intl.formatMessage(messages.next)}
|
||||||
})}
|
</Button>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
|
</Table.TD>
|
||||||
<Button
|
</tr>
|
||||||
disabled={!hasPrevPage}
|
</Table.TBody>
|
||||||
onClick={() => updateQueryParams('page', (page - 1).toString())}
|
</Table>
|
||||||
>
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
<span>{intl.formatMessage(globalMessages.previous)}</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={!hasNextPage}
|
|
||||||
onClick={() => updateQueryParams('page', (page + 1).toString())}
|
|
||||||
>
|
|
||||||
<span>{intl.formatMessage(globalMessages.next)}</span>
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,21 +97,19 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
|
|||||||
defaultOverrides?.tags ?? []
|
defaultOverrides?.tags ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { data: serverData, isValidating } =
|
||||||
data: serverData,
|
useSWR<ServiceCommonServerWithDetails>(
|
||||||
isValidating,
|
selectedServer !== null
|
||||||
} = useSWR<ServiceCommonServerWithDetails>(
|
? `/api/v1/service/${
|
||||||
selectedServer !== null
|
type === 'movie' ? 'radarr' : 'sonarr'
|
||||||
? `/api/v1/service/${
|
}/${selectedServer}`
|
||||||
type === 'movie' ? 'radarr' : 'sonarr'
|
: null,
|
||||||
}/${selectedServer}`
|
{
|
||||||
: null,
|
refreshInterval: 0,
|
||||||
{
|
refreshWhenHidden: false,
|
||||||
refreshInterval: 0,
|
revalidateOnFocus: false,
|
||||||
refreshWhenHidden: false,
|
}
|
||||||
revalidateOnFocus: false,
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(
|
const [selectedUser, setSelectedUser] = useState<User | null>(
|
||||||
requestUser ?? null
|
requestUser ?? null
|
||||||
|
|||||||
@@ -51,10 +51,8 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
|
|||||||
is4k = false,
|
is4k = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [
|
const [requestOverrides, setRequestOverrides] =
|
||||||
requestOverrides,
|
useState<RequestOverrides | null>(null);
|
||||||
setRequestOverrides,
|
|
||||||
] = useState<RequestOverrides | null>(null);
|
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`, {
|
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`, {
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
|
|||||||
@@ -74,10 +74,8 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
|
|||||||
(season) => season.seasonNumber
|
(season) => season.seasonNumber
|
||||||
);
|
);
|
||||||
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
|
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
|
||||||
const [
|
const [requestOverrides, setRequestOverrides] =
|
||||||
requestOverrides,
|
useState<RequestOverrides | null>(null);
|
||||||
setRequestOverrides,
|
|
||||||
] = useState<RequestOverrides | null>(null);
|
|
||||||
const [selectedSeasons, setSelectedSeasons] = useState<number[]>(
|
const [selectedSeasons, setSelectedSeasons] = useState<number[]>(
|
||||||
editRequest ? editingSeasons : []
|
editRequest ? editingSeasons : []
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -177,20 +177,19 @@ const NotificationsPushover: React.FC = () => {
|
|||||||
<span className="label-required">*</span>
|
<span className="label-required">*</span>
|
||||||
<span className="label-tip">
|
<span className="label-tip">
|
||||||
{intl.formatMessage(messages.accessTokenTip, {
|
{intl.formatMessage(messages.accessTokenTip, {
|
||||||
ApplicationRegistrationLink: function ApplicationRegistrationLink(
|
ApplicationRegistrationLink:
|
||||||
msg
|
function ApplicationRegistrationLink(msg) {
|
||||||
) {
|
return (
|
||||||
return (
|
<a
|
||||||
<a
|
href="https://pushover.net/api#registration"
|
||||||
href="https://pushover.net/api#registration"
|
className="text-white transition duration-300 hover:underline"
|
||||||
className="text-white transition duration-300 hover:underline"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noreferrer"
|
||||||
rel="noreferrer"
|
>
|
||||||
>
|
{msg}
|
||||||
{msg}
|
</a>
|
||||||
</a>
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -306,9 +306,11 @@ const SettingsMain: React.FC = () => {
|
|||||||
<div className="form-input">
|
<div className="form-input">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
<Field as="select" id="locale" name="locale">
|
<Field as="select" id="locale" name="locale">
|
||||||
{(Object.keys(
|
{(
|
||||||
availableLanguages
|
Object.keys(
|
||||||
) as (keyof typeof availableLanguages)[]).map((key) => (
|
availableLanguages
|
||||||
|
) as (keyof typeof availableLanguages)[]
|
||||||
|
).map((key) => (
|
||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={availableLanguages[key].code}
|
value={availableLanguages[key].code}
|
||||||
|
|||||||
@@ -33,10 +33,8 @@ const Setup: React.FC = () => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [
|
const [mediaServerSettingsComplete, setMediaServerSettingsComplete] =
|
||||||
mediaServerSettingsComplete,
|
useState(false);
|
||||||
setMediaServerSettingsComplete,
|
|
||||||
] = useState(false);
|
|
||||||
const [mediaServerType, setMediaServerType] = useState('');
|
const [mediaServerType, setMediaServerType] = useState('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { SparklesIcon } from '@heroicons/react/outline';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
|
import { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
|
||||||
import Modal from '../Common/Modal';
|
|
||||||
import Transition from '../Transition';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
newversionavailable: 'Application Update',
|
|
||||||
newversionDescription:
|
|
||||||
'Overseerr has been updated! Please click the button below to reload the page.',
|
|
||||||
reloadOverseerr: 'Reload',
|
|
||||||
});
|
|
||||||
|
|
||||||
const StatusChecker: React.FC = () => {
|
const StatusChecker: React.FC = () => {
|
||||||
const intl = useIntl();
|
|
||||||
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
|
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
|
||||||
refreshInterval: 60 * 1000,
|
refreshInterval: 60 * 1000,
|
||||||
});
|
});
|
||||||
@@ -27,28 +15,7 @@ const StatusChecker: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<Transition
|
|
||||||
enter="opacity-0 transition duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="opacity-100 transition duration-300"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
appear
|
|
||||||
show={data.commitTag !== process.env.commitTag}
|
|
||||||
>
|
|
||||||
<Modal
|
|
||||||
iconSvg={<SparklesIcon />}
|
|
||||||
title={intl.formatMessage(messages.newversionavailable)}
|
|
||||||
onOk={() => location.reload()}
|
|
||||||
okText={intl.formatMessage(messages.reloadOverseerr)}
|
|
||||||
backgroundClickable={false}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.newversionDescription)}
|
|
||||||
</Modal>
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatusChecker;
|
export default StatusChecker;
|
||||||
|
|||||||
@@ -108,9 +108,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
`/api/v1/tv/${router.query.tvId}/ratings`
|
`/api/v1/tv/${router.query.tvId}/ratings`
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedCrew = useMemo(() => sortCrewPriority(data?.credits.crew ?? []), [
|
const sortedCrew = useMemo(
|
||||||
data,
|
() => sortCrewPriority(data?.credits.crew ?? []),
|
||||||
]);
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -197,8 +198,9 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const seasonCount = data.seasons.filter((season) => season.seasonNumber !== 0)
|
const seasonCount = data.seasons.filter(
|
||||||
.length;
|
(season) => season.seasonNumber !== 0
|
||||||
|
).length;
|
||||||
|
|
||||||
if (seasonCount) {
|
if (seasonCount) {
|
||||||
seriesAttributes.push(
|
seriesAttributes.push(
|
||||||
|
|||||||
@@ -223,9 +223,11 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
availableLanguages[currentSettings.locale].display,
|
availableLanguages[currentSettings.locale].display,
|
||||||
})}
|
})}
|
||||||
</option>
|
</option>
|
||||||
{(Object.keys(
|
{(
|
||||||
availableLanguages
|
Object.keys(
|
||||||
) as (keyof typeof availableLanguages)[]).map((key) => (
|
availableLanguages
|
||||||
|
) as (keyof typeof availableLanguages)[]
|
||||||
|
).map((key) => (
|
||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={availableLanguages[key].code}
|
value={availableLanguages[key].code}
|
||||||
|
|||||||
@@ -12,25 +12,24 @@ const CollectionPage: NextPage<CollectionPageProps> = ({ collection }) => {
|
|||||||
return <CollectionDetails collection={collection} />;
|
return <CollectionDetails collection={collection} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<CollectionPageProps> = async (
|
export const getServerSideProps: GetServerSideProps<CollectionPageProps> =
|
||||||
ctx
|
async (ctx) => {
|
||||||
) => {
|
const response = await axios.get<Collection>(
|
||||||
const response = await axios.get<Collection>(
|
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
|
ctx.query.collectionId
|
||||||
ctx.query.collectionId
|
}`,
|
||||||
}`,
|
{
|
||||||
{
|
headers: ctx.req?.headers?.cookie
|
||||||
headers: ctx.req?.headers?.cookie
|
? { cookie: ctx.req.headers.cookie }
|
||||||
? { cookie: ctx.req.headers.cookie }
|
: undefined,
|
||||||
: undefined,
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
collection: response.data,
|
collection: response.data,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionPage;
|
export default CollectionPage;
|
||||||
|
|||||||
24
yarn.lock
24
yarn.lock
@@ -2018,6 +2018,23 @@
|
|||||||
"@octokit/openapi-types" "^1.2.0"
|
"@octokit/openapi-types" "^1.2.0"
|
||||||
"@types/node" ">= 8"
|
"@types/node" ">= 8"
|
||||||
|
|
||||||
|
"@prisma/client@^3.11.1":
|
||||||
|
version "3.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7"
|
||||||
|
integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng==
|
||||||
|
dependencies:
|
||||||
|
"@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
|
|
||||||
|
"@prisma/engines-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9":
|
||||||
|
version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9"
|
||||||
|
integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ==
|
||||||
|
|
||||||
|
"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9":
|
||||||
|
version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c"
|
||||||
|
integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw==
|
||||||
|
|
||||||
"@react-spring/animated@~9.2.0":
|
"@react-spring/animated@~9.2.0":
|
||||||
version "9.2.0"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.0.tgz#02ea2a75c3b1557c9878f248227451119a9eb874"
|
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.0.tgz#02ea2a75c3b1557c9878f248227451119a9eb874"
|
||||||
@@ -11091,6 +11108,13 @@ preview-email@^3.0.5:
|
|||||||
pug "^3.0.2"
|
pug "^3.0.2"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
|
prisma@^3.11.1:
|
||||||
|
version "3.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028"
|
||||||
|
integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg==
|
||||||
|
dependencies:
|
||||||
|
"@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||||
|
|
||||||
proc-log@^1.0.0:
|
proc-log@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77"
|
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77"
|
||||||
|
|||||||
Reference in New Issue
Block a user