diff --git a/.all-contributorsrc b/.all-contributorsrc index 50d69cf15..e2d499126 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -196,7 +196,8 @@ "avatar_url": "https://avatars2.githubusercontent.com/u/20923978?v=4", "profile": "https://github.com/danshilm", "contributions": [ - "code" + "code", + "doc" ] }, { @@ -235,6 +236,15 @@ "contributions": [ "code" ] + }, + { + "login": "flying-sausages", + "name": "flying-sausages", + "avatar_url": "https://avatars1.githubusercontent.com/u/23618693?v=4", + "profile": "https://github.com/flying-sausages", + "contributions": [ + "doc" + ] } ], "badgeTemplate": "\"All-orange.svg\"/>", diff --git a/.gitbook.yaml b/.gitbook.yaml new file mode 100644 index 000000000..6c5133ed3 --- /dev/null +++ b/.gitbook.yaml @@ -0,0 +1,5 @@ +root: ./docs + +​structure: + readme: README.md + summary: SUMMARY.md​ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99ba8dd3d..0da054764 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ All help is welcome and greatly appreciated. If you would like to contribute to ``` yarn - yarn install + yarn dev ``` - Alternatively you can run using [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly. diff --git a/Dockerfile b/Dockerfile index 48bd94d42..8a0190bf8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM node:12.18-alpine AS BUILD_IMAGE +ARG COMMIT_TAG +ENV COMMIT_TAG=${COMMIT_TAG} + COPY . /app WORKDIR /app @@ -25,6 +28,8 @@ COPY --from=BUILD_IMAGE /app/dist ./dist COPY --from=BUILD_IMAGE /app/.next ./.next COPY --from=BUILD_IMAGE /app/node_modules ./node_modules +RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json + CMD yarn start EXPOSE 5055 diff --git a/README.md b/README.md index 2707710ae..179a3124f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Language grade: JavaScript GitHub -All Contributors +All Contributors

@@ -36,16 +36,21 @@ - User profiles. - User settings page (to give users the ability to modify their Overseerr experience to their liking). -- Version update notifications in-app. - 4K requests (Includes multi-radarr/sonarr management for media) ## Planned Features -- More notification types (Slack/Telegram/etc.). +- More notification types. - Issues system. This will allow users to report issues with content on your media server. - Local user system (for those who don't use Plex). - Compatibility APIs (to work with existing tools in your system). +## Getting Started + +Check out our documenation for steps on how to install and run Overseerr: + +https://docs.overseerr.dev/getting-started/installation + ## Running Overseerr Currently, Overseerr is only distributed through Docker images. If you have Docker, you can run Overseerr as per: @@ -70,6 +75,7 @@ After running Overseerr for the first time, configure it by visiting the web UI ## 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/PkCWJSeCk7). - You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions). - Bugs/Feature Requests can be opened via a [GitHub issue](https://github.com/sct/overseerr/issues). @@ -123,13 +129,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Paul Hagedorn

🌍
Shagon94

🌍
sebstrgg

🌍 -
Danshil Mungur

💻 +
Danshil Mungur

💻 📖
doob187

🚇
johnpyp

💻
Jakob Ankarhem

📖 💻
Jayesh

💻 +
flying-sausages

📖 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..ea46f76a4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# Introduction + +Welcome to the Overseerr Documentation. + +## Features + +- **Full Plex integration**. Login and manage user access with Plex. +- **Syncs to your Plex library** to show what titles you already have. +- **Integrates with Sonarr and Radarr**. With more services to come in the future. +- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI. +- **Simple request management UI**. Don't dig through the app to approve recent requests. +- **Mobile-friendly design**, for when you need to approve requests on the go. +- Granular permission system. +- Localization into other languages. + +## Motivation + +The primary motivation for starting this project was to have an incredibly performant and easy to use application. There is a heavy focus on the user experience for both the server owner and the users. We feel requesting should be **effortless for the user**. Find the media you want, click request, and branch off efficiently into other titles that interest you, all in one seamless flow. For the server owner, Overseerr takes all the hassle out of approving your users' requests. + +## We need your help! + +Overseerr is an ambitious project. We have already poured a lot of work into this, with more coming. We need your valuable feedback and help with finding bugs. Also, being that this is an open-source project, anyone is welcome to contribute. Contribution includes building features, patching bugs, or even translating the application. You can find the contribution guide on our GitHub. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 000000000..6b98b1a5a --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,17 @@ +# Table of contents + +* [Introduction](README.md) + +## Getting Started + +* [Installation](getting-started/installation.md) + +## Support + +* [Frequently Asked Questions](support/faq.md) +* [Asking for Support](support/asking-for-support.md) + +## Extending Overseerr + +* [Reverse Proxy Examples](extending-overseerr/reverse-proxy-examples.md) + diff --git a/docs/extending-overseerr/reverse-proxy-examples.md b/docs/extending-overseerr/reverse-proxy-examples.md new file mode 100644 index 000000000..b69e95040 --- /dev/null +++ b/docs/extending-overseerr/reverse-proxy-examples.md @@ -0,0 +1,132 @@ +# Reverse Proxy Examples + +{% hint style="warning" %} +Base URLs cannot be configured in Overseerr. With this limitation, only subdomain configurations are supported. +{% endhint %} + +## LE/SWAG + +### Subdomain + +Place in the `proxy-confs` folder as `overseerr.subdomain.conf` + +Example Configuration: + +```text +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name overseerr.*; + + include /config/nginx/ssl.conf; + + client_max_body_size 0; + + location / { + + include /config/nginx/proxy.conf; + resolver 127.0.0.11 valid=30s; + set $upstream_app overseerr; + set $upstream_port 5055; + set $upstream_proto http; + proxy_pass $upstream_proto://$upstream_app:$upstream_port; + + } + +} +``` + +## Traefik \(v2\) + +Add the labels to the Overseerr service in your `docker-compose` file. A basic example for a `docker-compose` file using Traefik can be found [here](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/). + +### Subdomain + +Example Configuration: + +```text +labels: + - "traefik.enable=true" + ## HTTP Routers + - "traefik.http.routers.overseerr-rtr.entrypoints=https" + - "traefik.http.routers.overseerr-rtr.rule=Host(`overseerr.domain.com`)" + - "traefik.http.routers.overseerr-rtr.tls=true" + ## HTTP Services + - "traefik.http.routers.overseerr-rtr.service=overseerr-svc" + - "traefik.http.services.overseerr-svc.loadbalancer.server.port=5055" +``` + +## LE/NGINX + +### Subdomain + +Take the configuration below and place it in `/etc/nginx/sites-available/overseerr.example.com.conf`. + +Create a symlink to `/etc/nginx/sites-enabled`: + +```text +sudo ln -s /etc/nginx/sites-available/overseerr.example.com.conf /etc/nginx/sites-enabled/overseerr.example.com.conf +``` + +Test the configuration: + +```text +sudo nginx -t +``` + +Reload your configuration for NGINX: + +```text +sudo systemctl reload nginx +``` + +Example Configuration: + +```text +server { + listen 80; + server_name overseerr.example.com; + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name overseerr.example.com; + + ssl_certificate /etc/letsencrypt/live/overseerr.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/overseerr.example.com/privkey.pem; + + proxy_set_header Referer $http_referer; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-Port $remote_port; + proxy_set_header X-Forwarded-Host $host:$remote_port; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Port $remote_port; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; + real_ip_header CF-Connecting-IP; + # Control the behavior of the Referer header (Referrer-Policy) + add_header Referrer-Policy "no-referrer"; + # HTTP Strict Transport Security + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + # Reduce XSS risks (Content-Security-Policy) + add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://plex.tv; style-src 'self' 'unsafe-inline' https://rsms.me/inter/inter.css; script-src 'self'; img-src 'self' data: https://plex.tv https://assets.plex.tv https://secure.gravatar.com https://i2.wp.com https://image.tmdb.org; font-src 'self' https://rsms.me/inter/font-files/" always; + # Prevent some categories of XSS attacks (X-XSS-Protection) + add_header X-XSS-Protection "1; mode=block" always; + # Provide clickjacking protection (X-Frame-Options) + add_header X-Frame-Options "SAMEORIGIN" always; + # Prevent Sniff Mimetype (X-Content-Type-Options) + add_header X-Content-Type-Options "nosniff" always; + + access_log /var/log/nginx/overseerr.example.com-access.log; + error_log /var/log/nginx/overseerr.example.com-error.log; + + location / { + proxy_pass http://127.0.0.1:5055; + } +} +``` + diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 000000000..6c8c38c72 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,196 @@ +# Installation + +{% hint style="danger" %} +Overseerr is currently under very heavy, rapid development and things are likely to break often. We need all the help we can get to find bugs and get them fixed to hit a more stable release. If you would like to help test the bleeding edge, please use the image **`sctx/overseerr:develop`** instead! +{% endhint %} + +{% hint style="info" %} +After running Overseerr for the first time, configure it by visiting the web UI at `http://[address]:5055` and completing the setup steps. +{% endhint %} + +## Docker + +{% tabs %} +{% tab title="Basic" %} +```bash +docker run -d \ + -e LOG_LEVEL=info \ + -e TZ=Asia/Tokyo \ + -p 5055:5055 \ + -v /path/to/appdata/config:/app/config \ + --restart unless-stopped \ + sctx/overseerr +``` +{% endtab %} + +{% tab title="UID/GID" %} +```text +docker run -d \ + --user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \ + -e LOG_LEVEL=info \ + -e TZ=Asia/Tokyo \ + -p 5055:5055 \ + -v /path/to/appdata/config:/app/config \ + --restart unless-stopped \ + sctx/overseerr +``` +{% endtab %} + +{% tab title="Manual Update" %} +```text +# Stop the Overseerr container +docker stop overseerr + +# Remove the Overseerr container +docker rm overseerr + +# Pull the latest update +docker pull sctx/overseerr + +# Run the Overseerr container with the same parameters as before +docker run -d ... +``` +{% endtab %} +{% endtabs %} + +{% hint style="info" %} +Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically. +{% endhint %} + +## Unraid + +1. Ensure you have the **Community Applications** plugin installed. +2. Inside the **Communtiy Applications** app store, search for **Overseerr**. +3. Click the **Install Button**. +4. On the following **Add Container** screen, make changes to the **Host Port** and **Host Path 1**\(Appdata\) as needed. +5. Click apply and access "Overseerr" at your `` in a web browser. + +## Windows + +Please refer to the [docker for windows documentation](https://docs.docker.com/docker-for-windows/) for installation. + +{% hint style="danger" %} +**WSL2 will need to be installed to prevent DB corruption! Please see** [**Docker Desktop WSL 2 backend**](https://docs.docker.com/docker-for-windows/wsl/) **on how to enable WSL2. The command below will only work with WSL2 installed! Details below.** +{% endhint %} + +```bash +docker run -d -e LOG_LEVEL=info -e TZ=Asia/Tokyo -p 5055:5055 -v "/your/path/here:/app/config" --restart unless-stopped sctx/overseerr +``` + +{% hint style="info" %} +Docker on Windows works differently than it does on Linux; it uses a VM to run a stripped-down Linux and then runs docker within that. The volume mounts are exposed to the docker in this VM via SMB mounts. While this is fine for media, it is unacceptable for the `/app/config` directory because SMB does not support file locking. This will eventually corrupt your database which can lead to slow behavior and crashes. If you must run in docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host. It's worth noting that this warning also extends to other containers which use SQLite databases. +{% endhint %} + +## Linux \(Unsupported\) +{% tabs %} + +{% tab title="Ubuntu 16.04+/Debian" %} +{% hint style="danger" %} +This install method is **not currently supported**. Docker is the only install method supported. Do not create issues or ask for support unless you are able to reproduce the issue with Docker. +{% endhint %} + +```bash +# Install nodejs +sudo apt-get install -y curl git gnupg2 +curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - +sudo apt-get install -y nodejs +# Install yarn +curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update && sudo apt-get install yarn +# Install Overseerr +cd ~ && git clone https://github.com/sct/overseerr.git +cd overseerr +yarn install +yarn build +yarn start +``` + +**Updating** + +In order to update, you will need to re-build overseer. +```bash +cd ~/.overseerr +git pull +yarn install +yarn build +yarn start +``` +{% endtab %} + +{% tab title="Ubuntu ARM" %} +{% hint style="danger" %} +This install method is **not currently supported**. Docker is the only install method supported. Do not create issues or ask for support unless you are able to reproduce the issue with Docker. +{% endhint %} + +```bash +# Install nodejs +sudo apt-get install -y curl git gnupg2 build-essential +curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - +sudo apt-get install -y nodejs +# Install yarn +curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update && sudo apt-get install yarn +# Install Overseerr +cd ~ && git clone https://github.com/sct/overseerr.git +cd overseerr +npm config set python "$(which python3)" +yarn install +yarn build +yarn start +``` + +**Updating** + +In order to update, you will need to re-build overseer. +```bash +cd ~/.overseerr +git pull +yarn install +yarn build +yarn start +``` +{% endtab %} + +{% tab title="ArchLinux \(3rd Party\)" %} +Built from tag \(master\): [https://aur.archlinux.org/packages/overseerr/](https://aur.archlinux.org/packages/overseerr/) +Built from latest \(develop\): [aur.archlinux.org/packages/overseerr-git](https://aur.archlinux.org/packages/overseerr-git/) +**To install these just use your favorite AUR package manager:** + +```bash +yay -S overseer +``` +{% endtab %} + +{% tab title="Gentoo \(3rd Party\)" %} +Portage overlay [GitHub Repository](https://github.com/chriscpritchard/overseerr-overlay) + +Efforts will be made to keep up to date with the latest releases, however, this cannot be guaranteed. + +To enable using eselect repository, run: +```bash +eselect repository add overseerr-overlay git https://github.com/chriscpritchard/overseerr-overlay.git +``` + +Once complete, you can just run: +```bash +emerge www-apps/overseerr +``` +{% endtab %} + +{% endtabs %} + +## Swizzin \(Third party\) +The installation is not implemented via docker, but barebones. The latest released version of overseerr will be used. +Please see the [swizzin documentation](https://swizzin.ltd/applications/overseerr) for more information. + +To install, run the following: +```bash +box install overseerr +``` + +To upgrade, run the following: +```bash +box upgrade overseerr +``` diff --git a/docs/support/asking-for-support.md b/docs/support/asking-for-support.md new file mode 100644 index 000000000..eecfc2c61 --- /dev/null +++ b/docs/support/asking-for-support.md @@ -0,0 +1,34 @@ +# Asking for Support + +## Before asking for support, make sure you try these things first + +* Make sure you have **updated** to the latest version. +* ["Have you tried turning it off and on again?"](https://www.youtube.com/watch?v=nn2FB1P_Mn8) +* **Analyzing** your logs, you just might find the solution yourself! +* **Search** the [Wiki](../), [Installation Guides](../getting-started/installation.md), and [FAQs](faq.md). +* If you have questions, feel free to ask them on [Discord](https://discord.gg/PkCWJSeCk7) \(Please review our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md)\). Please include a link to your logs. See [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) for more details. + +## What should I include when asking for support? + +When you contact support saying something like "it doesn't work" leaves little to go on to figure out what is wrong for you. When contacting support try to include information such as the following: + +* What did you try to do? When you describe what you did to reach the state you are in we may notice something you did different from the instructions, or something that your unique setup requires in addition. Some examples of what to provide here: + * What command did you enter? + * What did you click on? + * What settings did you change? + * Provide a step-by-step list of what you tried. +* What do you see? We cannot see your screen so some of the following is necessary for us to know what is going on: + * Did something happen? + * Did something not happen? + * Are there any error messages showing? + * Screenshots can help us see what you are seeing + * The Overseerr logs show exactly what happened and are often critical for identifying issues \(see [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) below\). + +## How can I share my logs? + +First you will need to gather your logs from the install directory. + +1. Collect the log file from `/logs/overseerr.log` +2. Open the log file and **upload the text** by going to [gist.github.com](https://gist.github.com/) and creating a new secret Gist of the contents. +3. **Share the link** with support in [Discord](https://discord.gg/PkCWJSeCk7) by copying the URL of the page. + diff --git a/docs/support/faq.md b/docs/support/faq.md new file mode 100644 index 000000000..b50111b80 --- /dev/null +++ b/docs/support/faq.md @@ -0,0 +1,95 @@ +# Frequently Asked Questions + +{% hint style="info" %} +If you can't find a solution here, please ask on [Discord](https://discord.gg/PkCWJSeCk7). Please do not post questions on the GitHub issues tracker. +{% endhint %} + +## General + +### I receive 409 or 400 errors when requesting a movie or tv show! + +**A:** Verify your are running radarr and sonarr v3. Overseerr was developed for v3 and is not currently backward compatible. + +### How do I keep Overseerr up-to-date? + +**A:** Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically. + +### How can I access Overseerr outside my home network? + +**A:** The easy and least secure method is to forward an external port \(`5055`\) on your router to the internal port used by Overseerr \(default is TCP `5055`\). Visit [Port Forward](http://portforward.com/) for instructions for your particular router. You will then be able to access Overseerr via `http://EXTERNAL-IP-ADDRESS:5055`. + +The more advanced and most preferred method \(and more secure if you use SSL\) is to set up a web server with NGINX/Apache, and use a reverse proxy to access Overseerr. You can lookup many guides on the internet to find out how to do this. There are several reverse proxy config examples located [here](../extending-overseerr/reverse-proxy-examples.md). + +The most secure method, but also the most inconvenient, is to set up a VPN tunnel to your home server, then you can access Overseerr as if it is on a local network via `http://LOCAL-IP-ADDRESS:5055`. + +### Overseerr is amazing! But it is not translated in my language yet! Can I help with translations? + +**A:** You sure can! We are using Weblate for translations! Check it out [here](https://hosted.weblate.org/engage/overseerr/). If your language is not listed please open an [enhancement request in issues](https://github.com/sct/overseerr/issues/new/choose). + +### Where can I find the changelog? + +**A:** You can find the changelog in the **Settings -> About** page in your instance. You can also find it on github [here](https://github.com/sct/overseerr/releases). + +### Can I make 4K requests? + +**A:** 4K requests are not supported just yet but they will be supported in the future! + +### Some media is missing from Overseerr that I know is in Plex! + +**A:** Overseerr supports the new Plex Movie, Legacy Plex Movie, TheTVDB agent, and the TMDb agent. Please verify that your library is using one of the agents previously listed. If you are changing agents, a full metadata refresh will need to be performed. Caution, this can take a long time depending on how many items you have in your movie library. + +**Troubleshooting Steps:** + +Check the Overseerr logs for media items that are missing. The logs will contain an error as to why that item could not be matched. One example might be `errorMessage":"SQLITE_CONSTRAINT: NOT NULL`. This means that the TMDb ID is missing from the Plex XML for that item. + +1. Verify that you are using one of the agents mentioned above. +2. Refresh the metadata for just that item. +3. Run a full scan in Overseerr to see if that item is now matched properly. +4. If the item is now seen by Overseerr then repeat step 2 for each missing item. If you have a large amount of items missing then a full metadata refresh is recommended for that library. +5. Run a full scan on Overseerr after refreshing all unmatched items. + +Perform these steps to verify the media item has a guid Overseerr can match. + +1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**. +2. Verify that the media item has the same format of one of the examples below. + +**Examples:** + +1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"` +2. The new Plex Movie agent `` +3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"` +4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"` + +### Where can I find the logs? + +**A:** The logs are located at `/logs/overseerr.log` + +## User Management + +### Why can't I see all my Plex users? + +**A:** Navigate to your **User List** in Overseerr and click **Import Users From Plex** button. Don't forget to check the default user permissions in the **Settings -> General Settings** page beforehand. + +### Can I create local users in Overseerr? + +**A:** Not at this time. But it is a planned feature! + +### Is is possible to set user roles in Overseerr? + +**A:** Unfortunately, this is not possible yet. It is planned! + +## Requests + +### I approved a requested movie and radarr didn't search for it! + +**A:** Check your minimum availability in radarr. If an added item does not meet the minimum availability, no search will be performed. Also verify that radarr did not search for it by checking the radarr logs. Lastly, verify the item was not already being monitored by radarr. Currently there is no state sync with radarr. + +### Help! My request still shows "requested" even though it's in Plex!?! + +**A:** See "[Some media is missing from Overseerr that I know is in Plex!](./faq.md#some-media-is-missing-from-overseerr-that-i-know-is-in-plex)" for troubleshooting steps. + +## Notifications + +### I am getting "Username and Password not accepted" when sending email notifications to gmail! + +**A:** If you have 2-Step Verification enabled on your account you will need to create an app password. More details can be found [here](https://support.google.com/mail/answer/185833). diff --git a/next.config.js b/next.config.js index a6ea87dfb..8c1766af8 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,7 @@ module.exports = { + env: { + commitTag: process.env.COMMIT_TAG || 'local', + }, webpack(config) { config.module.rules.push({ test: /\.svg$/, diff --git a/overseerr-api.yml b/overseerr-api.yml index 6fe132187..dccd60ba6 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -855,6 +855,22 @@ components: properties: webhookUrl: type: string + TelegramSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botAPI: + type: string + chatId: + type: string NotificationEmailSettings: type: object properties: @@ -870,6 +886,9 @@ components: emailFrom: type: string example: no-reply@example.com + senderName: + type: string + example: Overseerr smtpHost: type: string example: 127.0.0.1 @@ -1083,6 +1102,26 @@ components: name: X-Api-Key paths: + /status: + get: + summary: Return Overseerr version + description: Returns the current Overseerr version in JSON format + security: [] + tags: + - public + responses: + '200': + description: Returned version + content: + application/json: + schema: + type: object + properties: + version: + type: string + example: 1.0.0 + commitTag: + type: string /settings/main: get: summary: Returns main settings @@ -1635,6 +1674,52 @@ paths: responses: '204': description: Test notification attempted + /settings/notifications/telegram: + get: + summary: Return current telegram notification settings + description: Returns current telegram notification settings in JSON format + tags: + - settings + responses: + '200': + description: Returned telegram settings + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + post: + summary: Update telegram notification settings + description: Update current telegram notification settings with provided values + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + /settings/notifications/telegram/test: + post: + summary: Test the provided telegram settings + description: Sends a test notification to the telegram agent + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '204': + description: Test notification attempted /settings/notifications/slack: get: summary: Return current slack notification settings diff --git a/package.json b/package.json index 6a577b155..5cd62c423 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "docker build -t sctx/overseerr ." + "prepareCmd": "docker build --build-arg COMMIT_TAG=$GITHUB_SHA -t sctx/overseerr ." } ], "semantic-release-docker", diff --git a/public/images/rotate1.jpg b/public/images/rotate1.jpg index 0a6432e9a..8d04487ea 100644 Binary files a/public/images/rotate1.jpg and b/public/images/rotate1.jpg differ diff --git a/public/images/rotate2.jpg b/public/images/rotate2.jpg index 7ac006ab8..d9a206113 100644 Binary files a/public/images/rotate2.jpg and b/public/images/rotate2.jpg differ diff --git a/public/images/rotate3.jpg b/public/images/rotate3.jpg index 4bd99179a..94ca5a3fd 100644 Binary files a/public/images/rotate3.jpg and b/public/images/rotate3.jpg differ diff --git a/public/images/rotate4.jpg b/public/images/rotate4.jpg index 70dc7f968..bdcbe8088 100644 Binary files a/public/images/rotate4.jpg and b/public/images/rotate4.jpg differ diff --git a/public/images/rotate5.jpg b/public/images/rotate5.jpg new file mode 100644 index 000000000..d144c2ca7 Binary files /dev/null and b/public/images/rotate5.jpg differ diff --git a/public/images/rotate6.jpg b/public/images/rotate6.jpg new file mode 100644 index 000000000..da6d1f94d Binary files /dev/null and b/public/images/rotate6.jpg differ diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 7723fb9c9..92a747742 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -40,12 +40,15 @@ class Media { } } - public static async getMedia(id: number): Promise { + public static async getMedia( + id: number, + mediaType: MediaType + ): Promise { const mediaRepository = getRepository(Media); try { const media = await mediaRepository.findOne({ - where: { tmdbId: id }, + where: { tmdbId: id, mediaType }, relations: ['requests'], }); @@ -62,7 +65,7 @@ class Media { @Column({ type: 'varchar' }) public mediaType: MediaType; - @Column({ unique: true }) + @Column() @Index() public tmdbId: number; @@ -70,7 +73,7 @@ class Media { @Index() public tvdbId?: number; - @Column({ unique: true, nullable: true }) + @Column({ nullable: true }) @Index() public imdbId?: string; diff --git a/server/index.ts b/server/index.ts index 76371a94c..eaf36833e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,6 +17,7 @@ import { startJobs } from './job/schedule'; import notificationManager from './lib/notifications'; import DiscordAgent from './lib/notifications/agents/discord'; import EmailAgent from './lib/notifications/agents/email'; +import TelegramAgent from './lib/notifications/agents/telegram'; import { getAppVersion } from './utils/appVersion'; import SlackAgent from './lib/notifications/agents/slack'; @@ -47,6 +48,7 @@ app new DiscordAgent(), new EmailAgent(), new SlackAgent(), + new TelegramAgent(), ]); // Start Jobs diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts index 3f600c383..f92f5ee04 100644 --- a/server/job/plexsync/index.ts +++ b/server/job/plexsync/index.ts @@ -44,11 +44,11 @@ class JobPlexSync { this.isRecentOnly = isRecentOnly ?? false; } - private async getExisting(tmdbId: number) { + private async getExisting(tmdbId: number, mediaType: MediaType) { const mediaRepository = getRepository(Media); const existing = await mediaRepository.findOne({ - where: { tmdbId: tmdbId }, + where: { tmdbId: tmdbId, mediaType }, }); return existing; @@ -78,7 +78,10 @@ class JobPlexSync { } }); - const existing = await this.getExisting(newMedia.tmdbId); + const existing = await this.getExisting( + newMedia.tmdbId, + MediaType.MOVIE + ); if (existing && existing.status === MediaStatus.AVAILABLE) { this.log(`Title exists and is already available ${metadata.title}`); @@ -115,7 +118,7 @@ class JobPlexSync { throw new Error('Unable to find TMDB ID'); } - const existing = await this.getExisting(tmdbMovieId); + const existing = await this.getExisting(tmdbMovieId, MediaType.MOVIE); if (existing && existing.status === MediaStatus.AVAILABLE) { this.log(`Title exists and is already available ${plexitem.title}`); } else if (existing && existing.status !== MediaStatus.AVAILABLE) { @@ -184,9 +187,7 @@ class JobPlexSync { if (tvShow && metadata) { // Lets get the available seasons from plex const seasons = tvShow.seasons; - const media = await mediaRepository.findOne({ - where: { tmdbId: tvShow.id, mediaType: MediaType.TV }, - }); + const media = await this.getExisting(tvShow.id, MediaType.TV); const newSeasons: Season[] = []; diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index 954469453..9c1897d16 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import logger from '../../../logger'; import { getSettings, NotificationAgentDiscord } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; @@ -196,10 +196,12 @@ class DiscordAgent }; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { - if (this.getSettings().enabled && this.getSettings().options.webhookUrl) { + public shouldSend(type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.webhookUrl && + hasNotificationType(type, this.getSettings().types) + ) { return true; } diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 18cd3e594..d983a52ed 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,5 +1,5 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import path from 'path'; import { getSettings, NotificationAgentEmail } from '../../settings'; import nodemailer from 'nodemailer'; @@ -22,12 +22,13 @@ class EmailAgent return settings.notifications.agents.email; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { + public shouldSend(type: Notification): boolean { const settings = this.getSettings(); - if (settings.enabled) { + if ( + settings.enabled && + hasNotificationType(type, this.getSettings().types) + ) { return true; } @@ -60,7 +61,10 @@ class EmailAgent const settings = this.getSettings(); return new Email({ message: { - from: settings.options.emailFrom, + from: { + name: settings.options.senderName, + address: settings.options.emailFrom, + }, }, send: true, transport: this.getSmtpTransport(), diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index 221228d85..03f901b2b 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Notification } from '..'; +import { hasNotificationType, Notification } from '..'; import logger from '../../../logger'; import { getSettings, NotificationAgentSlack } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; @@ -187,10 +187,12 @@ class SlackAgent }; } - // TODO: Add checking for type here once we add notification type filters for agents - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public shouldSend(_type: Notification): boolean { - if (this.getSettings().enabled && this.getSettings().options.webhookUrl) { + public shouldSend(type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.webhookUrl && + hasNotificationType(type, this.getSettings().types) + ) { return true; } diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts new file mode 100644 index 000000000..2b5a9fdfc --- /dev/null +++ b/server/lib/notifications/agents/telegram.ts @@ -0,0 +1,127 @@ +import axios from 'axios'; +import { hasNotificationType, Notification } from '..'; +import logger from '../../../logger'; +import { getSettings, NotificationAgentTelegram } from '../../settings'; +import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; + +interface TelegramPayload { + text: string; + parse_mode: string; + chat_id: string; +} + +class TelegramAgent + extends BaseAgent + implements NotificationAgent { + private baseUrl = 'https://api.telegram.org/'; + + protected getSettings(): NotificationAgentTelegram { + if (this.settings) { + return this.settings; + } + + const settings = getSettings(); + + return settings.notifications.agents.telegram; + } + + public shouldSend(type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.botAPI && + this.getSettings().options.chatId && + hasNotificationType(type, this.getSettings().types) + ) { + return true; + } + + return false; + } + + private escapeText(text: string | undefined): string { + return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; + } + + private buildMessage( + type: Notification, + payload: NotificationPayload + ): string { + const settings = getSettings(); + let message = ''; + + const title = this.escapeText(payload.subject); + const plot = this.escapeText(payload.message); + const user = this.escapeText(payload.notifyUser.username); + + /* eslint-disable no-useless-escape */ + switch (type) { + case Notification.MEDIA_PENDING: + message += `\*New Request\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nPending Approval\n`; + + break; + case Notification.MEDIA_APPROVED: + message += `\*Request Approved\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nProcessing Request\n`; + + break; + case Notification.MEDIA_AVAILABLE: + message += `\*Now available\\!\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nAvailable\n`; + + break; + case Notification.TEST_NOTIFICATION: + message += `\*Test Notification\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n`; + + break; + } + + if (settings.main.applicationUrl && payload.media) { + const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; + message += `\[Open in Overseerr\]\(${actionUrl}\)`; + } + /* eslint-enable */ + + return message; + } + + public async send( + type: Notification, + payload: NotificationPayload + ): Promise { + logger.debug('Sending telegram notification', { label: 'Notifications' }); + try { + const endpoint = `${this.baseUrl}bot${ + this.getSettings().options.botAPI + }/sendMessage`; + + await axios.post(endpoint, { + text: this.buildMessage(type, payload), + parse_mode: 'MarkdownV2', + chat_id: `${this.getSettings().options.chatId}`, + } as TelegramPayload); + + return true; + } catch (e) { + logger.error('Error sending Telegram notification', { + label: 'Notifications', + message: e.message, + }); + return false; + } + } +} + +export default TelegramAgent; diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 0c711abe4..07127cf2f 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -9,6 +9,27 @@ export enum Notification { TEST_NOTIFICATION = 32, } +export const hasNotificationType = ( + types: Notification | Notification[], + value: number +): boolean => { + let total = 0; + + // If we are not checking any notifications, bail out and return true + if (types === 0) { + return true; + } + + if (Array.isArray(types)) { + // Combine all notification values into one + total = types.reduce((a, v) => a + v, 0); + } else { + total = types; + } + + return !!(value & total); +}; + class NotificationManager { private activeAgents: NotificationAgent[] = []; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index c3cdfee66..75d3c72fc 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -81,6 +81,14 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { authUser?: string; authPass?: string; allowSelfSigned: boolean; + senderName: string; + }; +} + +export interface NotificationAgentTelegram extends NotificationAgentConfig { + options: { + botAPI: string; + chatId: string; }; } @@ -88,6 +96,7 @@ interface NotificationAgents { email: NotificationAgentEmail; discord: NotificationAgentDiscord; slack: NotificationAgentSlack; + telegram: NotificationAgentTelegram; } interface NotificationSettings { @@ -140,6 +149,7 @@ class Settings { smtpPort: 587, secure: false, allowSelfSigned: false, + senderName: 'Overseerr', }, }, discord: { @@ -156,6 +166,14 @@ class Settings { webhookUrl: '', }, }, + telegram: { + enabled: false, + types: 0, + options: { + botAPI: '', + chatId: '', + }, + }, }, }, }; diff --git a/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts b/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts new file mode 100644 index 000000000..0be266993 --- /dev/null +++ b/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveTmdbIdUniqueConstraint1609236552057 + implements MigrationInterface { + name = 'RemoveTmdbIdUniqueConstraint1609236552057'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query( + `CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"` + ); + await queryRunner.query(`DROP TABLE "media"`); + await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`); + await queryRunner.query( + `CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"))` + ); + await queryRunner.query( + `INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "temporary_media"` + ); + await queryRunner.query(`DROP TABLE "temporary_media"`); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + } +} diff --git a/server/models/Collection.ts b/server/models/Collection.ts index f80a1ad79..64709502c 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -1,4 +1,5 @@ import { TmdbCollection } from '../api/themoviedb'; +import { MediaType } from '../constants/media'; import Media from '../entity/Media'; import { mapMovieResult, MovieResult } from './Search'; @@ -23,7 +24,9 @@ export const mapCollection = ( parts: collection.parts.map((part) => mapMovieResult( part, - media?.find((req) => req.tmdbId === part.id) + media?.find( + (req) => req.tmdbId === part.id && req.mediaType === MediaType.MOVIE + ) ) ), }); diff --git a/server/models/Search.ts b/server/models/Search.ts index 68d8dab89..7d347207d 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -3,6 +3,7 @@ import type { TmdbPersonResult, TmdbTvResult, } from '../api/themoviedb'; +import { MediaType as MainMediaType } from '../constants/media'; import Media from '../entity/Media'; export type MediaType = 'tv' | 'movie' | 'person'; @@ -122,12 +123,18 @@ export const mapSearchResults = ( case 'movie': return mapMovieResult( result, - media?.find((req) => req.tmdbId === result.id) + media?.find( + (req) => + req.tmdbId === result.id && req.mediaType === MainMediaType.MOVIE + ) ); case 'tv': return mapTvResult( result, - media?.find((req) => req.tmdbId === result.id) + media?.find( + (req) => + req.tmdbId === result.id && req.mediaType === MainMediaType.TV + ) ); default: return mapPersonResult(result); diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 1193354ce..ec2116aa0 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -26,7 +26,9 @@ discoverRoutes.get('/movies', async (req, res) => { results: data.results.map((result) => mapMovieResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + ) ) ), }); diff --git a/server/routes/index.ts b/server/routes/index.ts index bf094ec08..29466bd70 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -13,10 +13,19 @@ import tvRoutes from './tv'; import mediaRoutes from './media'; import personRoutes from './person'; import collectionRoutes from './collection'; +import { getAppVersion, getCommitTag } from '../utils/appVersion'; const router = Router(); router.use(checkUser); + +router.get('/status', (req, res) => { + return res.status(200).json({ + version: getAppVersion(), + commitTag: getCommitTag(), + }); +}); + router.use('/user', isAuthenticated(Permission.MANAGE_USERS), user); router.get('/settings/public', (_req, res) => { const settings = getSettings(); diff --git a/server/routes/movie.ts b/server/routes/movie.ts index af4992ed3..cadaf5a7e 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -5,6 +5,7 @@ import { mapMovieResult } from '../models/Search'; import Media from '../entity/Media'; import RottenTomatoes from '../api/rottentomatoes'; import logger from '../logger'; +import { MediaType } from '../constants/media'; const movieRoutes = Router(); @@ -17,7 +18,7 @@ movieRoutes.get('/:id', async (req, res, next) => { language: req.query.language as string, }); - const media = await Media.getMedia(tmdbMovie.id); + const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE); return res.status(200).json(mapMovieDetails(tmdbMovie, media)); } catch (e) { @@ -49,7 +50,9 @@ movieRoutes.get('/:id/recommendations', async (req, res) => { results: results.results.map((result) => mapMovieResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + ) ) ), }); @@ -75,7 +78,9 @@ movieRoutes.get('/:id/similar', async (req, res) => { results: results.results.map((result) => mapMovieResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + ) ) ), }); diff --git a/server/routes/person.ts b/server/routes/person.ts index dfdd4ce24..add0b0f1c 100644 --- a/server/routes/person.ts +++ b/server/routes/person.ts @@ -45,13 +45,19 @@ personRoutes.get('/:id/combined_credits', async (req, res) => { cast: combinedCredits.cast.map((result) => mapCastCredits( result, - castMedia.find((med) => med.tmdbId === result.id) + castMedia.find( + (med) => + med.tmdbId === result.id && med.mediaType === result.media_type + ) ) ), crew: combinedCredits.crew.map((result) => mapCrewCredits( result, - crewMedia.find((med) => med.tmdbId === result.id) + crewMedia.find( + (med) => + med.tmdbId === result.id && med.mediaType === result.media_type + ) ) ), id: combinedCredits.id, diff --git a/server/routes/request.ts b/server/routes/request.ts index 857d11f74..ea82825c8 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -102,7 +102,7 @@ requestRoutes.post( : await tmdb.getTvShow({ tvId: req.body.mediaId }); let media = await mediaRepository.findOne({ - where: { tmdbId: req.body.mediaId }, + where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType }, relations: ['requests'], }); @@ -164,7 +164,7 @@ requestRoutes.post( if (finalSeasons.length === 0) { return next({ - status: 500, + status: 202, message: 'No seasons available to request', }); } diff --git a/server/routes/settings.ts b/server/routes/settings.ts index 4f22fe01f..ba9b91bc1 100644 --- a/server/routes/settings.ts +++ b/server/routes/settings.ts @@ -25,6 +25,7 @@ import { Notification } from '../lib/notifications'; import DiscordAgent from '../lib/notifications/agents/discord'; import EmailAgent from '../lib/notifications/agents/email'; import SlackAgent from '../lib/notifications/agents/slack'; +import TelegramAgent from '../lib/notifications/agents/telegram'; const settingsRoutes = Router(); @@ -503,6 +504,40 @@ settingsRoutes.post('/notifications/slack/test', (req, res, next) => { return res.status(204).send(); }); +settingsRoutes.get('/notifications/telegram', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.notifications.agents.telegram); +}); + +settingsRoutes.post('/notifications/telegram', (req, res) => { + const settings = getSettings(); + + settings.notifications.agents.telegram = req.body; + settings.save(); + + res.status(200).json(settings.notifications.agents.telegram); +}); + +settingsRoutes.post('/notifications/telegram/test', (req, res, next) => { + if (!req.user) { + return next({ + status: 500, + message: 'User information missing from request', + }); + } + + const telegramAgent = new TelegramAgent(req.body); + telegramAgent.send(Notification.TEST_NOTIFICATION, { + notifyUser: req.user, + subject: 'Test Notification', + message: + 'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?', + }); + + return res.status(204).send(); +}); + settingsRoutes.get('/notifications/email', (_req, res) => { const settings = getSettings(); diff --git a/server/routes/tv.ts b/server/routes/tv.ts index be3915b03..1ddf1f80c 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -5,6 +5,7 @@ import { mapTvResult } from '../models/Search'; import Media from '../entity/Media'; import RottenTomatoes from '../api/rottentomatoes'; import logger from '../logger'; +import { MediaType } from '../constants/media'; const tvRoutes = Router(); @@ -16,7 +17,7 @@ tvRoutes.get('/:id', async (req, res, next) => { language: req.query.language as string, }); - const media = await Media.getMedia(tv.id); + const media = await Media.getMedia(tv.id, MediaType.TV); return res.status(200).json(mapTvDetails(tv, media)); } catch (e) { @@ -60,7 +61,9 @@ tvRoutes.get('/:id/recommendations', async (req, res) => { results: results.results.map((result) => mapTvResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.TV + ) ) ), }); @@ -86,7 +89,9 @@ tvRoutes.get('/:id/similar', async (req, res) => { results: results.results.map((result) => mapTvResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.TV + ) ) ), }); diff --git a/server/utils/appVersion.ts b/server/utils/appVersion.ts index ef9f35c3b..923d47089 100644 --- a/server/utils/appVersion.ts +++ b/server/utils/appVersion.ts @@ -1,3 +1,20 @@ +import { existsSync } from 'fs'; +import path from 'path'; +import logger from '../logger'; + +const COMMIT_TAG_PATH = path.join(__dirname, '../../committag.json'); +let commitTag = 'local'; + +if (existsSync(COMMIT_TAG_PATH)) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + commitTag = require(COMMIT_TAG_PATH).commitTag; + logger.info(`Commit Tag: ${commitTag}`); +} + +export const getCommitTag = (): string => { + return commitTag; +}; + export const getAppVersion = (): string => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../package.json'); @@ -5,7 +22,7 @@ export const getAppVersion = (): string => { let finalVersion = version; if (version === '0.1.0') { - finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`; + finalVersion = `develop-${getCommitTag()}`; } return finalVersion; diff --git a/src/assets/extlogos/telegram.svg b/src/assets/extlogos/telegram.svg new file mode 100644 index 000000000..d10e5c88b --- /dev/null +++ b/src/assets/extlogos/telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/xcircle.svg b/src/assets/xcircle.svg new file mode 100644 index 000000000..6fee85051 --- /dev/null +++ b/src/assets/xcircle.svg @@ -0,0 +1 @@ + diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index 93e954ea6..38e3fb7b3 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -102,7 +102,7 @@ const CollectionDetails: React.FC = ({ return (
= ({ title, children }) => { return ( -
+
{title}
{children} diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx index 9420b85ea..2cdcb4233 100644 --- a/src/components/Common/Modal/index.tsx +++ b/src/components/Common/Modal/index.tsx @@ -98,7 +98,7 @@ const Modal: React.FC = ({ show={!loading} >
= ({ + )}
diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index b6e44f37d..c85727a9b 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -175,11 +175,9 @@ const Sidebar: React.FC = ({ open, setClosed }) => { >
- - - Overseerr Logo - - + + Overseerr Logo +