Compare commits

...

12 Commits

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

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

7
.env Normal file
View File

@@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (Preview) and CockroachDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://jumail:1DontGive1@localhost:5432/jellyseerr?schema=public"

View File

@@ -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
View File

@@ -2,157 +2,48 @@
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
</p>
<p align="center">
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" />
<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 -->
<a href="https://discord.gg/BHak4GCk"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
</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
- Full Plex integration. Authenticate and manage user access with Plex!
- Easy integration with your existing services. Currently, Overseerr supports Sonarr and Radarr. More to come!
- Plex library scan, to keep track of the titles which are already available.
- Jellyfin support
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr.
- 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.
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
- Granular permission system.
- Support for various notification agents.
- 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
Check out our documentation for instructions on how to install and run Overseerr:
https://docs.overseerr.dev/getting-started/installation
## Preview
<img src="./public/preview.jpg">
Check out our dockerhub for instructions on how to install and run Jellyseerr:
https://hub.docker.com/r/fallenbagel/jellyseerr
## 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/overseerr).
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions).
- You can get support on [Discord](https://discord.gg/VpVnZ92yQK).
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
## 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 -->
<!-- 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!
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/fallen.bagel)

View File

@@ -19,6 +19,7 @@
"dependencies": {
"@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4",
"@prisma/client": "^3.11.1",
"@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.79",
@@ -46,6 +47,7 @@
"nodemailer": "^6.6.3",
"openpgp": "^5.0.0-3",
"plex-api": "^5.3.1",
"prisma": "^3.11.1",
"pug": "^3.0.2",
"react": "17.0.2",
"react-ace": "^9.3.0",

View File

@@ -0,0 +1,163 @@
-- CreateTable
CREATE TABLE "media" (
"id" SERIAL NOT NULL,
"mediaType" TEXT NOT NULL,
"tmdbId" INTEGER NOT NULL,
"tvdbId" INTEGER,
"imdbId" TEXT,
"status" INTEGER NOT NULL DEFAULT 1,
"status4k" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeasonChange" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"mediaAddedAt" TIMESTAMP(3),
"serviceId" INTEGER,
"serviceId4k" INTEGER,
"externalServiceId" INTEGER,
"externalServiceId4k" INTEGER,
"externalServiceSlug" TEXT,
"externalServiceSlug4k" TEXT,
"ratingKey" TEXT,
"ratingKey4k" TEXT,
"jellyfinMediaId" TEXT,
"jellyfinMediaId4k" TEXT,
CONSTRAINT "media_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_request" (
"id" SERIAL NOT NULL,
"status" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"type" TEXT NOT NULL,
"mediaId" INTEGER,
"requestedById" INTEGER,
"modifiedById" INTEGER,
"is4k" BOOLEAN NOT NULL DEFAULT false,
"serverId" INTEGER,
"profileId" INTEGER,
"rootFolder" TEXT,
"languageProfileId" INTEGER,
CONSTRAINT "media_request_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "migrations" (
"id" SERIAL NOT NULL,
"timestamp" BIGINT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "migrations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "season" (
"id" SERIAL NOT NULL,
"seasonNumber" INTEGER NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"mediaId" INTEGER,
"status4k" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "season_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "season_request" (
"id" SERIAL NOT NULL,
"seasonNumber" INTEGER NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"requestId" INTEGER,
CONSTRAINT "season_request_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "session" (
"expiredAt" BIGINT NOT NULL,
"id" TEXT NOT NULL,
"json" TEXT NOT NULL,
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"username" TEXT,
"plexId" INTEGER,
"plexToken" TEXT,
"permissions" INTEGER NOT NULL DEFAULT 0,
"avatar" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"password" TEXT,
"userType" INTEGER NOT NULL DEFAULT 1,
"plexUsername" TEXT,
"resetPasswordGuid" TEXT,
"recoveryLinkExpirationDate" TIMESTAMP(3),
"jellyfinUsername" TEXT,
"jellyfinAuthToken" TEXT,
"jellyfinUserId" TEXT,
"jellyfinDeviceId" TEXT,
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_settings" (
"id" SERIAL NOT NULL,
"enableNotifications" BOOLEAN NOT NULL DEFAULT true,
"discordId" TEXT,
"userId" INTEGER,
"region" TEXT,
"originalLanguage" TEXT,
CONSTRAINT "user_settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "sqlite_autoindex_media_1" ON "media"("tvdbId");
-- CreateIndex
CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media"("imdbId");
-- CreateIndex
CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media"("tvdbId");
-- CreateIndex
CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media"("tmdbId");
-- CreateIndex
CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session"("expiredAt");
-- CreateIndex
CREATE UNIQUE INDEX "sqlite_autoindex_user_1" ON "user"("email");
-- CreateIndex
CREATE UNIQUE INDEX "sqlite_autoindex_user_settings_1" ON "user_settings"("userId");
-- AddForeignKey
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_modifiedById_fkey" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "media_request" ADD CONSTRAINT "media_request_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "season" ADD CONSTRAINT "season_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "season_request" ADD CONSTRAINT "season_request_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

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

127
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,127 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
model media {
id Int @id @default(autoincrement())
mediaType String
tmdbId Int
tvdbId Int? @unique(map: "sqlite_autoindex_media_1")
imdbId String?
status Int @default(1)
status4k Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
lastSeasonChange DateTime @default(now())
mediaAddedAt DateTime?
serviceId Int?
serviceId4k Int?
externalServiceId Int?
externalServiceId4k Int?
externalServiceSlug String?
externalServiceSlug4k String?
ratingKey String?
ratingKey4k String?
jellyfinMediaId String?
jellyfinMediaId4k String?
media_request media_request[]
season season[]
@@index([imdbId], map: "IDX_7ff2d11f6a83cb52386eaebe74")
@@index([tvdbId], map: "IDX_41a289eb1fa489c1bc6f38d9c3")
@@index([tmdbId], map: "IDX_7157aad07c73f6a6ae3bbd5ef5")
}
model media_request {
id Int @id @default(autoincrement())
status Int
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
type String
mediaId Int?
requestedById Int?
modifiedById Int?
is4k Boolean @default(false)
serverId Int?
profileId Int?
rootFolder String?
languageProfileId Int?
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
user_media_request_modifiedByIdTouser user? @relation("media_request_modifiedByIdTouser", fields: [modifiedById], references: [id], onUpdate: NoAction)
user_media_request_requestedByIdTouser user? @relation("media_request_requestedByIdTouser", fields: [requestedById], references: [id], onDelete: Cascade, onUpdate: NoAction)
season_request season_request[]
}
model migrations {
id Int @id @default(autoincrement())
timestamp BigInt
name String
}
model season {
id Int @id @default(autoincrement())
seasonNumber Int
status Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
mediaId Int?
status4k Int @default(1)
media media? @relation(fields: [mediaId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model season_request {
id Int @id @default(autoincrement())
seasonNumber Int
status Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
requestId Int?
media_request media_request? @relation(fields: [requestId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
model session {
expiredAt BigInt
id String @id
json String
@@index([expiredAt], map: "IDX_28c5d1d16da7908c97c9bc2f74")
}
model user {
id Int @id @default(autoincrement())
email String @unique(map: "sqlite_autoindex_user_1")
username String?
plexId Int?
plexToken String?
permissions Int @default(0)
avatar String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
password String?
userType Int @default(1)
plexUsername String?
resetPasswordGuid String?
recoveryLinkExpirationDate DateTime?
jellyfinUsername String?
jellyfinAuthToken String?
jellyfinUserId String?
jellyfinDeviceId String?
media_request_media_request_modifiedByIdTouser media_request[] @relation("media_request_modifiedByIdTouser")
media_request_media_request_requestedByIdTouser media_request[] @relation("media_request_requestedByIdTouser")
user_settings user_settings?
}
model user_settings {
id Int @id @default(autoincrement())
enableNotifications Boolean @default(true)
discordId String?
userId Int? @unique(map: "sqlite_autoindex_user_settings_1")
region String?
originalLanguage String?
user user? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

After

Width:  |  Height:  |  Size: 504 KiB

View File

@@ -1,18 +1,14 @@
import { PrismaClient } from '@prisma/client';
import { getClientIp } from '@supercharge/request-ip';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import express, { NextFunction, Request, Response } from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import session, { Store } from 'express-session';
import next from 'next';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
import { createConnection, getRepository } from 'typeorm';
import YAML from 'yamljs';
import PlexAPI from './api/plexapi';
import { Session } from './entity/Session';
import { User } from './entity/User';
import { startJobs } from './job/schedule';
import notificationManager from './lib/notifications';
import DiscordAgent from './lib/notifications/agents/discord';
@@ -35,18 +31,17 @@ logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const prisma = new PrismaClient();
app
.prepare()
.then(async () => {
const dbConnection = await createConnection();
// Run migrations in production
if (process.env.NODE_ENV === 'production') {
await dbConnection.query('PRAGMA foreign_keys=OFF');
await dbConnection.runMigrations();
await dbConnection.query('PRAGMA foreign_keys=ON');
}
// // Run migrations in production
// if (process.env.NODE_ENV === 'production') {
// await dbConnection.query('PRAGMA foreign_keys=OFF');
// await dbConnection.runMigrations();
// await dbConnection.query('PRAGMA foreign_keys=ON');
// }
// Load Settings
const settings = getSettings().load();
@@ -56,14 +51,23 @@ app
settings.plex.libraries.length > 1 &&
!settings.plex.libraries[0].type
) {
const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
const admin = await prisma.user.findFirst({
select: {
id: true,
plexToken: true,
},
orderBy: {
id: 'asc',
},
});
// const userRepository = getRepository(User);
// const admin = await userRepository.findOne({
// select: ['id', 'plexToken'],
// order: { id: 'ASC' },
// });
if (admin) {
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
const plexapi = new PlexAPI({ plexToken: admin.plexToken! });
await plexapi.syncLibraries();
logger.info('Migrating libraries to include media type', {
label: 'Settings',
@@ -129,22 +133,24 @@ app
}
// Set up sessions
const sessionRespository = getRepository(Session);
server.use(
'/api',
session({
secret: settings.clientId,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 30,
},
store: new TypeormStore({
cleanupLimit: 2,
ttl: 1000 * 60 * 60 * 24 * 30,
}).connect(sessionRespository) as Store,
})
);
// const sessionRespository = getRepository(Session);
// const sessionRespository = await prisma.session.findMany();
// server.use(
// '/api',
// session({
// secret: settings.clientId,
// resave: false,
// saveUninitialized: false,
// cookie: {
// maxAge: 1000 * 60 * 60 * 24 * 30,
// },
// store: new TypeormStore({
// cleanupLimit: 2,
// ttl: 1000 * 60 * 60 * 24 * 30,
// }).connect(sessionRespository) as Store,
// })
// );
const apiDocs = YAML.load(API_SPEC_PATH);
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
server.use(

View File

@@ -60,9 +60,8 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}
);
const { data: genres } = useSWR<{ id: number; name: string }[]>(
`/api/v1/genres/movie`
);
const { data: genres } =
useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
if (!data && !error) {
return <LoadingSpinner />;

View File

@@ -35,13 +35,11 @@ const Discover: React.FC = () => {
{ revalidateOnMount: true }
);
const {
data: requests,
error: requestError,
} = useSWR<RequestResultsResponse>(
'/api/v1/request?filter=all&take=10&sort=modified&skip=0',
{ revalidateOnMount: true }
);
const { data: requests, error: requestError } =
useSWR<RequestResultsResponse>(
'/api/v1/request?filter=all&take=10&sort=modified&skip=0',
{ revalidateOnMount: true }
);
return (
<>

View File

@@ -65,9 +65,11 @@ const LanguagePicker: React.FC = () => {
}
defaultValue={locale}
>
{(Object.keys(
availableLanguages
) as (keyof typeof availableLanguages)[]).map((key) => (
{(
Object.keys(
availableLanguages
) as (keyof typeof availableLanguages)[]
).map((key) => (
<option key={key} value={availableLanguages[key].code}>
{availableLanguages[key].display}
</option>

View File

@@ -106,9 +106,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
`/api/v1/movie/${router.query.movieId}/ratings`
);
const sortedCrew = useMemo(() => sortCrewPriority(data?.credits.crew ?? []), [
data,
]);
const sortedCrew = useMemo(
() => sortCrewPriority(data?.credits.crew ?? []),
[data]
);
if (!data && !error) {
return <LoadingSpinner />;

View File

@@ -32,12 +32,10 @@ const PersonDetails: React.FC = () => {
);
const [showBio, setShowBio] = useState(false);
const {
data: combinedCredits,
error: errorCombinedCredits,
} = useSWR<PersonCombinedCreditsResponse>(
`/api/v1/person/${router.query.personId}/combined_credits`
);
const { data: combinedCredits, error: errorCombinedCredits } =
useSWR<PersonCombinedCreditsResponse>(
`/api/v1/person/${router.query.personId}/combined_credits`
);
const sortedCast = useMemo(() => {
const grouped = groupBy(combinedCredits?.cast ?? [], 'id');

View File

@@ -1,38 +1,24 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import {
MediaRequestStatus,
MediaStatus,
} from '../../../server/constants/media';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { MovieDetails } from '../../../server/models/Movie';
import type { TvDetails } from '../../../server/models/Tv';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import { withProperties } from '../../utils/typeHelpers';
import type { MovieDetails } from '../../../server/models/Movie';
import useSWR from 'swr';
import { LanguageContext } from '../../context/LanguageContext';
import { MediaRequestStatus } from '../../../server/constants/media';
import Badge from '../Common/Badge';
import { useUser, Permission } from '../../hooks/useUser';
import axios from 'axios';
import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage';
import RequestModal from '../RequestModal';
import { withProperties } from '../../utils/typeHelpers';
import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl';
import globalMessages from '../../i18n/globalMessages';
import StatusBadge from '../StatusBadge';
const messages = defineMessages({
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
failedretry: 'Something went wrong while retrying the request.',
mediaerror: 'The associated title for this request is no longer available.',
deleterequest: 'Delete Request',
seasons: 'Seasons',
all: 'All',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -41,7 +27,7 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
const RequestCardPlaceholder: React.FC = () => {
return (
<div className="relative 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-full" style={{ paddingBottom: '150%' }} />
</div>
@@ -49,45 +35,6 @@ const RequestCardPlaceholder: React.FC = () => {
);
};
interface RequestCardErrorProps {
mediaId?: number;
}
const RequestCardError: React.FC<RequestCardErrorProps> = ({ mediaId }) => {
const { hasPermission } = useUser();
const intl = useIntl();
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${mediaId}`);
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
return (
<div className="relative 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 {
request: MediaRequest;
onTitleData?: (requestId: number, title: MovieDetails | TvDetails) => void;
@@ -98,16 +45,14 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
triggerOnce: true,
});
const intl = useIntl();
const { user, hasPermission } = useUser();
const { addToast } = useToasts();
const [isRetrying, setRetrying] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const { hasPermission } = useUser();
const { locale } = useContext(LanguageContext);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? `${url}` : null
inView ? `${url}?language=${locale}` : null
);
const {
data: requestData,
@@ -125,30 +70,6 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
}
};
const deleteRequest = async () => {
await axios.delete(`/api/v1/request/${request.id}`);
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
};
const retryRequest = async () => {
setRetrying(true);
try {
const response = await axios.post(`/api/v1/request/${request.id}/retry`);
if (response) {
revalidate();
}
} catch (e) {
addToast(intl.formatMessage(messages.failedretry), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setRetrying(false);
}
};
useEffect(() => {
if (title && onTitleData) {
onTitleData(request.id, title);
@@ -164,242 +85,157 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
}
if (!requestData && !requestError) {
return <RequestCardError />;
return <RequestCardPlaceholder />;
}
if (!title || !requestData) {
return <RequestCardError mediaId={requestData?.media.id} />;
return <RequestCardPlaceholder />;
}
return (
<>
<RequestModal
show={showEditModal}
tmdbId={request.media.tmdbId}
type={request.type}
is4k={request.is4k}
editRequest={request}
onCancel={() => setShowEditModal(false)}
onComplete={() => {
revalidate();
setShowEditModal(false);
}}
/>
<div className="relative flex 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">
{title.backdropPath && (
<div className="absolute inset-0 z-0">
<CachedImage
<div
className="relative flex p-4 text-gray-400 bg-gray-800 bg-center bg-cover rounded-md w-72 sm:w-96"
style={{
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`,
}}
>
<div className="flex flex-col flex-1 min-w-0 pr-4">
<h2 className="overflow-hidden text-base text-white cursor-pointer sm:text-lg overflow-ellipsis whitespace-nowrap hover:underline">
<Link
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
as={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
{isMovie(title) ? title.title : title.name}
</Link>
</h2>
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="flex items-center group">
<img
src={requestData.requestedBy.avatar}
alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
layout="fill"
objectFit="cover"
className="w-4 mr-1 rounded-full sm:mr-2 sm:w-5"
/>
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
}}
<span className="text-xs truncate sm:text-sm group-hover:underline">
{requestData.requestedBy.displayName}
</span>
</a>
</Link>
{requestData.media.status && (
<div className="mt-1 sm:mt-2">
<StatusBadge
status={
requestData.is4k
? requestData.media.status4k
: requestData.media.status
}
is4k={requestData.is4k}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
/>
</div>
)}
<div className="relative z-10 flex flex-col flex-1 min-w-0 pr-4">
<div className="hidden text-xs font-medium text-white sm:flex">
{(isMovie(title) ? title.releaseDate : title.firstAirDate)?.slice(
0,
4
)}
</div>
<Link
href={
request.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="overflow-hidden 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,
})}
{request.seasons.length > 0 && (
<div className="items-center hidden mt-2 text-sm sm:flex">
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{!isMovie(title) &&
title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(messages.all)}</Badge>
</span>
{title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(globalMessages.all)}</Badge>
</span>
) : (
<div className="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
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}
/>
<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-end flex-1 space-x-2">
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="flex items-end flex-1">
<span className="mr-2">
<Button
buttonType="primary"
buttonType="success"
buttonSize="sm"
disabled={isRetrying}
onClick={() => retryRequest()}
onClick={() => modifyRequest('approve')}
>
<RefreshIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ marginRight: '0', animationDirection: 'reverse' }}
/>
<span className="hidden ml-1.5 sm:block">
{intl.formatMessage(globalMessages.retry)}
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<Button
buttonType="success"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<CheckIcon style={{ marginRight: '0' }} />
<span className="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 && (
</span>
<span>
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => deleteRequest()}
onClick={() => modifyRequest('decline')}
>
<XIcon style={{ marginRight: '0' }} />
<span className="hidden ml-1.5 sm:block">
{intl.formatMessage(globalMessages.cancel)}
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
)}
</div>
</div>
</span>
</div>
)}
</div>
<div className="flex-shrink-0 w-20 sm:w-28">
<Link
href={
href={request.type === 'movie' ? '/movie/[movieId]' : '/tv/[tvId]'}
as={
request.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<a className="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">
<CachedImage
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
layout="responsive"
width={600}
height={900}
/>
</a>
<img
src={
title.posterPath
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
className="w-20 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md"
/>
</Link>
</div>
</>
</div>
);
};

View File

@@ -1,15 +1,13 @@
import {
CheckIcon,
PencilIcon,
RefreshIcon,
TrashIcon,
XIcon,
} from '@heroicons/react/solid';
import axios from 'axios';
import Link from 'next/link';
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import {
defineMessages,
FormattedDate,
FormattedRelativeTime,
useIntl,
} from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import {
@@ -19,70 +17,25 @@ import {
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
import type { MovieDetails } from '../../../../server/models/Movie';
import type { TvDetails } from '../../../../server/models/Tv';
import { LanguageContext } from '../../../context/LanguageContext';
import { Permission, useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages';
import Badge from '../../Common/Badge';
import Button from '../../Common/Button';
import CachedImage from '../../Common/CachedImage';
import ConfirmButton from '../../Common/ConfirmButton';
import Table from '../../Common/Table';
import RequestModal from '../../RequestModal';
import StatusBadge from '../../StatusBadge';
const messages = defineMessages({
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
seasons: 'Seasons',
notavailable: 'N/A',
failedretry: 'Something went wrong while retrying the request.',
requested: 'Requested',
requesteddate: 'Requested',
modified: 'Modified',
modifieduserdate: '{date} by {user}',
mediaerror: 'The associated title for this request is no longer available.',
editrequest: 'Edit Request',
deleterequest: 'Delete Request',
cancelRequest: 'Cancel Request',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined;
};
interface RequestItemErroProps {
mediaId?: number;
revalidateList: () => void;
}
const RequestItemError: React.FC<RequestItemErroProps> = ({
mediaId,
revalidateList,
}) => {
const intl = useIntl();
const { hasPermission } = useUser();
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${mediaId}`);
revalidateList();
};
return (
<div className="flex 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 {
request: MediaRequest;
revalidateList: () => void;
@@ -97,21 +50,23 @@ const RequestItem: React.FC<RequestItemProps> = ({
});
const { addToast } = useToasts();
const intl = useIntl();
const { user, hasPermission } = useUser();
const { hasPermission } = useUser();
const [showEditModal, setShowEditModal] = useState(false);
const { locale } = useContext(LanguageContext);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? `${url}` : null
);
const { data: requestData, revalidate, mutate } = useSWR<MediaRequest>(
`/api/v1/request/${request.id}`,
{
initialData: request,
}
inView ? `${url}?language=${locale}` : null
);
const {
data: requestData,
revalidate,
mutate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
initialData: request,
});
const [isRetrying, setRetrying] = useState(false);
@@ -147,24 +102,22 @@ const RequestItem: React.FC<RequestItemProps> = ({
if (!title && !error) {
return (
<div
className="w-full h-64 bg-gray-800 rounded-xl xl:h-32 animate-pulse"
ref={ref}
/>
<tr className="w-full h-24 animate-pulse" ref={ref}>
<td colSpan={6}></td>
</tr>
);
}
if (!title || !requestData) {
return (
<RequestItemError
mediaId={requestData?.media.id}
revalidateList={revalidateList}
/>
<tr className="w-full h-24 animate-pulse">
<td colSpan={6}></td>
</tr>
);
}
return (
<>
<tr className="relative w-full h-24 p-2">
<RequestModal
show={showEditModal}
tmdbId={request.media.tmdbId}
@@ -177,26 +130,28 @@ const RequestItem: React.FC<RequestItemProps> = ({
setShowEditModal(false);
}}
/>
<div className="relative flex 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">
{title.backdropPath && (
<div className="absolute inset-0 z-0 w-full bg-center bg-cover xl:w-2/3">
<CachedImage
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt=""
layout="fill"
objectFit="cover"
/>
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
}}
/>
</div>
)}
<div className="relative flex 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">
<Table.TD>
<div className="flex items-center">
<Link
href={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<a className="flex-shrink-0 hidden mr-4 sm:block">
<img
src={
title.posterPath
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
className="w-12 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer transform-gpu hover:scale-105 hover:shadow-md"
/>
</a>
</Link>
<div className="flex-shrink overflow-hidden">
<Link
href={
requestData.type === 'movie'
@@ -204,285 +159,219 @@ const RequestItem: React.FC<RequestItemProps> = ({
: `/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">
<CachedImage
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/overseerr_poster_not_found.png'
}
alt=""
layout="responsive"
width={600}
height={900}
objectFit="cover"
/>
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
<div className="flex flex-col justify-center pl-2 overflow-hidden xl:pl-4">
<div className="font-medium pt-0.5 sm:pt-1 text-xs text-white">
{(isMovie(title)
? title.releaseDate
: title.firstAirDate
)?.slice(0, 4)}
</div>
<Link
href={
requestData.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="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}
<Link href={`/users/${requestData.requestedBy.id}`}>
<a className="flex items-center mt-1">
<img
src={requestData.requestedBy.avatar}
alt=""
className="w-5 mr-2 rounded-full"
/>
)}
</div>
<div className="card-field">
{hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
) ? (
<>
<span className="card-field-name">
{intl.formatMessage(messages.requested)}
</span>
<span className="flex 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 className="text-sm hover:underline">
{requestData.requestedBy.displayName}
</span>
<span className="flex text-sm text-gray-300 truncate">
{intl.formatMessage(messages.modifieduserdate, {
date: (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.updatedAt).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
numeric="auto"
/>
),
user: (
<Link href={`/users/${requestData.modifiedBy.id}`}>
<a className="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>
),
})}
</a>
</Link>
{requestData.seasons.length > 0 && (
<div className="items-center hidden mt-2 text-sm sm:flex">
<span className="mr-2">
{intl.formatMessage(messages.seasons)}
</span>
{requestData.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</div>
</div>
<div className="z-10 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">
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
className="w-full"
buttonType="primary"
disabled={isRetrying}
onClick={() => retryRequest()}
>
<RefreshIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ animationDirection: 'reverse' }}
</Table.TD>
<Table.TD>
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN ||
requestData.status === MediaRequestStatus.DECLINED ? (
<Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED
? intl.formatMessage(globalMessages.declined)
: intl.formatMessage(globalMessages.failed)}
</Badge>
) : (
<StatusBadge
status={requestData.media[requestData.is4k ? 'status4k' : 'status']}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
is4k={requestData.is4k}
/>
)}
</Table.TD>
<Table.TD>
<div className="flex flex-col">
<span className="text-sm text-gray-300">
<FormattedDate value={requestData.createdAt} />
</span>
</div>
</Table.TD>
<Table.TD>
<div className="flex flex-col">
{requestData.modifiedBy ? (
<span className="text-sm text-gray-300">
<div className="flex items-center">
<img
src={requestData.modifiedBy.avatar}
alt=""
className="w-5 mr-2 rounded-full"
/>
<span>
{intl.formatMessage(
isRetrying ? globalMessages.retrying : globalMessages.retry
)}
</span>
</Button>
)}
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<TrashIcon />
<span>{intl.formatMessage(messages.deleterequest)}</span>
</ConfirmButton>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="flex 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 className="text-sm">
{requestData.modifiedBy.displayName} (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.updatedAt).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
)
</span>
</div>
)}
{requestData.status === MediaRequestStatus.PENDING &&
(hasPermission(Permission.MANAGE_REQUESTS) ||
(requestData.requestedBy.id === user?.id &&
(requestData.type === 'tv' ||
hasPermission(Permission.REQUEST_ADVANCED)))) && (
<span className="w-full">
</span>
) : (
<span className="text-sm text-gray-300">N/A</span>
)}
</div>
</Table.TD>
<Table.TD alignText="right">
{requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.UNKNOWN &&
requestData.status !== MediaRequestStatus.DECLINED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
className="mr-2"
buttonType="primary"
buttonSize="sm"
disabled={isRetrying}
onClick={() => retryRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18px"
height="18px"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.retry)}
</span>
</Button>
)}
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => deleteRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.delete)}
</span>
</Button>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<span className="mr-2">
<Button
className="w-full"
buttonType="primary"
onClick={() => setShowEditModal(true)}
buttonType="success"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<PencilIcon />
<span>{intl.formatMessage(messages.editrequest)}</span>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
</span>
)}
{requestData.status === MediaRequestStatus.PENDING &&
!hasPermission(Permission.MANAGE_REQUESTS) &&
requestData.requestedBy.id === user?.id && (
<ConfirmButton
onClick={() => deleteRequest()}
confirmText={intl.formatMessage(globalMessages.areyousure)}
className="w-full"
>
<XIcon />
<span>{intl.formatMessage(messages.cancelRequest)}</span>
</ConfirmButton>
)}
</div>
</div>
</>
<span className="mr-2">
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => modifyRequest('decline')}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</span>
<span>
<Button
buttonType="primary"
buttonSize="sm"
onClick={() => setShowEditModal(true)}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.edit)}
</span>
</Button>
</span>
</>
)}
</Table.TD>
</tr>
);
};

View File

@@ -1,94 +1,51 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
FilterIcon,
SortDescendingIcon,
} from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import React, { useState } from 'react';
import useSWR from 'swr';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button';
import Header from '../Common/Header';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import RequestItem from './RequestItem';
import Header from '../Common/Header';
import Table from '../Common/Table';
import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
requests: 'Requests',
mediaInfo: 'Media Info',
status: 'Status',
requestedAt: 'Requested At',
modifiedBy: 'Last Modified By',
showingresults:
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
resultsperpage: 'Display {pageSize} results per page',
next: 'Next',
previous: 'Previous',
filterAll: 'All',
filterPending: 'Pending',
filterApproved: 'Approved',
filterAvailable: 'Available',
filterProcessing: 'Processing',
noresults: 'No results.',
showallrequests: 'Show All Requests',
sortAdded: 'Request Date',
sortModified: 'Last Modified',
});
enum Filter {
ALL = 'all',
PENDING = 'pending',
APPROVED = 'approved',
PROCESSING = 'processing',
AVAILABLE = 'available',
UNAVAILABLE = 'unavailable',
}
type Filter = 'all' | 'pending' | 'approved' | 'processing' | 'available';
type Sort = 'added' | 'modified';
const RequestList: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const { user } = useUser({
id: Number(router.query.userId),
});
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
const [pageIndex, setPageIndex] = useState(0);
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
const [currentSort, setCurrentSort] = useState<Sort>('added');
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
const page = router.query.page ? Number(router.query.page) : 1;
const pageIndex = page - 1;
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
`/api/v1/request?take=${currentPageSize}&skip=${
pageIndex * currentPageSize
}&filter=${currentFilter}&sort=${currentSort}${
router.query.userId ? `&requestedBy=${router.query.userId}` : ''
}`
}&filter=${currentFilter}&sort=${currentSort}`
);
// Restore last set filter values on component mount
useEffect(() => {
const filterString = window.localStorage.getItem('rl-filter-settings');
if (filterString) {
const filterSettings = JSON.parse(filterString);
setCurrentFilter(filterSettings.currentFilter);
setCurrentSort(filterSettings.currentSort);
setCurrentPageSize(filterSettings.currentPageSize);
}
// If filter value is provided in query, use that instead
if (Object.values(Filter).includes(router.query.filter as Filter)) {
setCurrentFilter(router.query.filter as Filter);
}
}, [router.query.filter]);
// Set filter values to local storage any time they are changed
useEffect(() => {
window.localStorage.setItem(
'rl-filter-settings',
JSON.stringify({
currentFilter,
currentSort,
currentPageSize,
})
);
}, [currentFilter, currentSort, currentPageSize]);
if (!data && !error) {
return <LoadingSpinner />;
}
@@ -102,81 +59,73 @@ const RequestList: React.FC = () => {
return (
<>
<PageTitle
title={[
intl.formatMessage(messages.requests),
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>
<PageTitle title={intl.formatMessage(messages.requests)} />
<div className="flex flex-col justify-between lg:items-end lg:flex-row">
<Header>{intl.formatMessage(messages.requests)}</Header>
<div className="flex flex-col flex-grow mt-2 sm:flex-row lg:flex-grow-0">
<div className="flex flex-grow mb-2 sm:mb-0 sm:mr-2 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
<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>
<select
id="filter"
name="filter"
onChange={(e) => {
setPageIndex(0);
setCurrentFilter(e.target.value as Filter);
router.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
});
}}
value={currentFilter}
className="rounded-r-only"
>
<option value="all">
{intl.formatMessage(globalMessages.all)}
{intl.formatMessage(messages.filterAll)}
</option>
<option value="pending">
{intl.formatMessage(globalMessages.pending)}
{intl.formatMessage(messages.filterPending)}
</option>
<option value="approved">
{intl.formatMessage(globalMessages.approved)}
{intl.formatMessage(messages.filterApproved)}
</option>
<option value="processing">
{intl.formatMessage(globalMessages.processing)}
{intl.formatMessage(messages.filterProcessing)}
</option>
<option value="available">
{intl.formatMessage(globalMessages.available)}
</option>
<option value="unavailable">
{intl.formatMessage(globalMessages.unavailable)}
{intl.formatMessage(messages.filterAvailable)}
</option>
</select>
</div>
<div className="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">
<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>
<select
id="sort"
name="sort"
onChange={(e) => {
setPageIndex(0);
setCurrentSort(e.target.value as Sort);
}}
onBlur={(e) => {
setPageIndex(0);
setCurrentSort(e.target.value as Sort);
router.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
});
}}
value={currentSort}
className="rounded-r-only"
@@ -191,104 +140,114 @@ const RequestList: React.FC = () => {
</div>
</div>
</div>
{data.results.map((request) => {
return (
<div className="py-2" key={`request-list-${request.id}`}>
<RequestItem
request={request}
revalidateList={() => revalidate()}
/>
</div>
);
})}
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.modifiedBy)}</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{data.results.map((request) => {
return (
<RequestItem
request={request}
key={`request-list-${request.id}`}
revalidateList={() => revalidate()}
/>
);
})}
{data.results.length === 0 && (
<div className="flex flex-col items-center justify-center w-full py-24 text-white">
<span className="text-2xl text-gray-400">
{intl.formatMessage(globalMessages.noresults)}
</span>
{currentFilter !== Filter.ALL && (
<div className="mt-4">
<Button
buttonType="primary"
onClick={() => setCurrentFilter(Filter.ALL)}
>
{intl.formatMessage(messages.showallrequests)}
</Button>
</div>
{data.results.length === 0 && (
<tr className="relative h-24 p-2 text-white">
<Table.TD colSpan={6} noPadding>
<div className="flex flex-col items-center justify-center w-screen p-6 lg:w-full">
<span className="text-base">
{intl.formatMessage(messages.noresults)}
</span>
{currentFilter !== 'all' && (
<div className="mt-4">
<Button
buttonSize="sm"
buttonType="primary"
onClick={() => setCurrentFilter('all')}
>
{intl.formatMessage(messages.showallrequests)}
</Button>
</div>
)}
</div>
</Table.TD>
</tr>
)}
</div>
)}
<div className="actions">
<nav
className="flex flex-col items-center mb-3 space-y-3 sm:space-y-0 sm:flex-row"
aria-label="Pagination"
>
<div className="hidden lg:flex lg:flex-1">
<p className="text-sm">
{data.results.length > 0 &&
intl.formatMessage(globalMessages.showingresults, {
from: pageIndex * currentPageSize + 1,
to:
data.results.length < currentPageSize
? pageIndex * currentPageSize + data.results.length
: (pageIndex + 1) * currentPageSize,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
},
})}
</p>
</div>
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
<span className="items-center -mt-3 text-sm truncate sm:mt-0">
{intl.formatMessage(globalMessages.resultsperpage, {
pageSize: (
<select
id="pageSize"
name="pageSize"
onChange={(e) => {
setCurrentPageSize(Number(e.target.value));
router
.push({
pathname: router.pathname,
query: router.query.userId
? { userId: router.query.userId }
: {},
})
.then(() => window.scrollTo(0, 0));
}}
value={currentPageSize}
className="inline short"
<tr className="bg-gray-700">
<Table.TD colSpan={6} noPadding>
<nav
className="flex flex-col items-center w-screen px-6 py-3 space-x-4 space-y-3 sm:space-y-0 sm:flex-row lg:w-full"
aria-label="Pagination"
>
<div className="hidden lg:flex lg:flex-1">
<p className="text-sm">
{data.results.length > 0 &&
intl.formatMessage(messages.showingresults, {
from: pageIndex * currentPageSize + 1,
to:
data.results.length < currentPageSize
? pageIndex * currentPageSize + data.results.length
: (pageIndex + 1) * currentPageSize,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
},
})}
</p>
</div>
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
<span className="items-center -mt-3 text-sm sm:-ml-4 lg:ml-0 sm:mt-0">
{intl.formatMessage(messages.resultsperpage, {
pageSize: (
<select
id="pageSize"
name="pageSize"
onChange={(e) => {
setPageIndex(0);
setCurrentPageSize(Number(e.target.value));
}}
value={currentPageSize}
className="inline short"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
),
})}
</span>
</div>
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
<Button
disabled={!hasPrevPage}
onClick={() => setPageIndex((current) => current - 1)}
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
),
})}
</span>
</div>
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
<Button
disabled={!hasPrevPage}
onClick={() => updateQueryParams('page', (page - 1).toString())}
>
<ChevronLeftIcon />
<span>{intl.formatMessage(globalMessages.previous)}</span>
</Button>
<Button
disabled={!hasNextPage}
onClick={() => updateQueryParams('page', (page + 1).toString())}
>
<span>{intl.formatMessage(globalMessages.next)}</span>
<ChevronRightIcon />
</Button>
</div>
</nav>
</div>
{intl.formatMessage(messages.previous)}
</Button>
<Button
disabled={!hasNextPage}
onClick={() => setPageIndex((current) => current + 1)}
>
{intl.formatMessage(messages.next)}
</Button>
</div>
</nav>
</Table.TD>
</tr>
</Table.TBody>
</Table>
</>
);
};

View File

@@ -97,21 +97,19 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
defaultOverrides?.tags ?? []
);
const {
data: serverData,
isValidating,
} = useSWR<ServiceCommonServerWithDetails>(
selectedServer !== null
? `/api/v1/service/${
type === 'movie' ? 'radarr' : 'sonarr'
}/${selectedServer}`
: null,
{
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
}
);
const { data: serverData, isValidating } =
useSWR<ServiceCommonServerWithDetails>(
selectedServer !== null
? `/api/v1/service/${
type === 'movie' ? 'radarr' : 'sonarr'
}/${selectedServer}`
: null,
{
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
}
);
const [selectedUser, setSelectedUser] = useState<User | null>(
requestUser ?? null

View File

@@ -51,10 +51,8 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
is4k = false,
}) => {
const [isUpdating, setIsUpdating] = useState(false);
const [
requestOverrides,
setRequestOverrides,
] = useState<RequestOverrides | null>(null);
const [requestOverrides, setRequestOverrides] =
useState<RequestOverrides | null>(null);
const { addToast } = useToasts();
const { data, error } = useSWR<MovieDetails>(`/api/v1/movie/${tmdbId}`, {
revalidateOnMount: true,

View File

@@ -74,10 +74,8 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
(season) => season.seasonNumber
);
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
const [
requestOverrides,
setRequestOverrides,
] = useState<RequestOverrides | null>(null);
const [requestOverrides, setRequestOverrides] =
useState<RequestOverrides | null>(null);
const [selectedSeasons, setSelectedSeasons] = useState<number[]>(
editRequest ? editingSeasons : []
);

View File

@@ -177,20 +177,19 @@ const NotificationsPushover: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.accessTokenTip, {
ApplicationRegistrationLink: function ApplicationRegistrationLink(
msg
) {
return (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
ApplicationRegistrationLink:
function ApplicationRegistrationLink(msg) {
return (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>

View File

@@ -306,9 +306,11 @@ const SettingsMain: React.FC = () => {
<div className="form-input">
<div className="form-input-field">
<Field as="select" id="locale" name="locale">
{(Object.keys(
availableLanguages
) as (keyof typeof availableLanguages)[]).map((key) => (
{(
Object.keys(
availableLanguages
) as (keyof typeof availableLanguages)[]
).map((key) => (
<option
key={key}
value={availableLanguages[key].code}

View File

@@ -33,10 +33,8 @@ const Setup: React.FC = () => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [
mediaServerSettingsComplete,
setMediaServerSettingsComplete,
] = useState(false);
const [mediaServerSettingsComplete, setMediaServerSettingsComplete] =
useState(false);
const [mediaServerType, setMediaServerType] = useState('');
const router = useRouter();
const { locale } = useLocale();

View File

@@ -1,20 +1,8 @@
import { SparklesIcon } from '@heroicons/react/outline';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
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 intl = useIntl();
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
refreshInterval: 60 * 1000,
});
@@ -27,28 +15,7 @@ const StatusChecker: React.FC = () => {
return null;
}
return (
<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>
);
return null;
};
export default StatusChecker;

View File

@@ -108,9 +108,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
`/api/v1/tv/${router.query.tvId}/ratings`
);
const sortedCrew = useMemo(() => sortCrewPriority(data?.credits.crew ?? []), [
data,
]);
const sortedCrew = useMemo(
() => sortCrewPriority(data?.credits.crew ?? []),
[data]
);
if (!data && !error) {
return <LoadingSpinner />;
@@ -197,8 +198,9 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
);
}
const seasonCount = data.seasons.filter((season) => season.seasonNumber !== 0)
.length;
const seasonCount = data.seasons.filter(
(season) => season.seasonNumber !== 0
).length;
if (seasonCount) {
seriesAttributes.push(

View File

@@ -223,9 +223,11 @@ const UserGeneralSettings: React.FC = () => {
availableLanguages[currentSettings.locale].display,
})}
</option>
{(Object.keys(
availableLanguages
) as (keyof typeof availableLanguages)[]).map((key) => (
{(
Object.keys(
availableLanguages
) as (keyof typeof availableLanguages)[]
).map((key) => (
<option
key={key}
value={availableLanguages[key].code}

View File

@@ -12,25 +12,24 @@ const CollectionPage: NextPage<CollectionPageProps> = ({ collection }) => {
return <CollectionDetails collection={collection} />;
};
export const getServerSideProps: GetServerSideProps<CollectionPageProps> = async (
ctx
) => {
const response = await axios.get<Collection>(
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
ctx.query.collectionId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
}
);
export const getServerSideProps: GetServerSideProps<CollectionPageProps> =
async (ctx) => {
const response = await axios.get<Collection>(
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
ctx.query.collectionId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
}
);
return {
props: {
collection: response.data,
},
return {
props: {
collection: response.data,
},
};
};
};
export default CollectionPage;

View File

@@ -2018,6 +2018,23 @@
"@octokit/openapi-types" "^1.2.0"
"@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":
version "9.2.0"
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"
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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77"