Compare commits
41 Commits
preview-pr
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03b9b1dbb | ||
|
|
73672e29f8 | ||
|
|
cc5192209f | ||
|
|
278dcf4b44 | ||
|
|
36e092f225 | ||
|
|
46d5c737a2 | ||
|
|
cba4878db3 | ||
|
|
57cc48a699 | ||
|
|
84f488be06 | ||
|
|
f885f2a0f3 | ||
|
|
eef3e5ea4c | ||
|
|
8db821c1c1 | ||
|
|
a39b882f09 | ||
|
|
754dccc4bf | ||
|
|
f97ee11430 | ||
|
|
54868fd486 | ||
|
|
eea389879f | ||
|
|
5c917f95b4 | ||
|
|
dd4d42fd31 | ||
|
|
e5c6b9cd74 | ||
|
|
508fccae4e | ||
|
|
f77573c838 | ||
|
|
7dfe38001e | ||
|
|
48f55da43e | ||
|
|
1e97503802 | ||
|
|
42ff34bb3d | ||
|
|
107b766c44 | ||
|
|
fb51ce5570 | ||
|
|
3357343d98 | ||
|
|
9d61092f37 | ||
|
|
29274614c3 | ||
|
|
19b51592ea | ||
|
|
757c0fc29e | ||
|
|
3eb48abc14 | ||
|
|
01cd9d3872 | ||
|
|
9582196e1f | ||
|
|
3743edab8d | ||
|
|
d81e7cdbab | ||
|
|
6e1d7f7075 | ||
|
|
91cf2de33a | ||
|
|
a6ec2d5220 |
@@ -540,11 +540,22 @@
|
||||
"code"
|
||||
]
|
||||
}
|
||||
{
|
||||
"login": "Fallenbagel",
|
||||
"name": "Mohamed Nuvaas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/98979876?s=96&v=4",
|
||||
"profile": "https://github.com/nicospz",
|
||||
"contributions": [
|
||||
"code",
|
||||
"logo",
|
||||
"design"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "overseerr",
|
||||
"projectOwner": "sct",
|
||||
"projectName": "jellyseerr",
|
||||
"projectOwner": "Fallenbagel",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
|
||||
@@ -7,7 +7,6 @@ module.exports = {
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
@@ -26,7 +25,6 @@ module.exports = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'formatjs/no-offset': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
@@ -40,7 +38,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
|
||||
plugins: ['jsx-a11y', 'react-hooks', 'formatjs'],
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
|
||||
11
.github/CODEOWNERS
vendored
@@ -1,12 +1,7 @@
|
||||
# Global code ownership
|
||||
* @sct
|
||||
|
||||
# Documentation
|
||||
docs/ @TheCatLady @samwiseg0
|
||||
|
||||
# Snap-related files
|
||||
.github/workflows/snap.yaml @samwiseg0
|
||||
snap/ @samwiseg0
|
||||
- @Fallenbagel
|
||||
|
||||
# i18n locale files
|
||||
src/i18n/locale/ @sct @TheCatLady
|
||||
|
||||
src/i18n/locale/ @Fallenbagel
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,10 +4,4 @@
|
||||
|
||||
#### To-Dos
|
||||
|
||||
- [ ] Successful build `yarn build`
|
||||
- [ ] Translation keys `yarn i18n:extract`
|
||||
- [ ] Database migration (if required)
|
||||
|
||||
#### Issues Fixed or Closed
|
||||
|
||||
- Fixes #XXXX
|
||||
|
||||
14
.github/dependabot.yml
vendored
@@ -1,14 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '20:00'
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '20:00'
|
||||
open-pull-requests-limit: 10
|
||||
4
.husky/commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS
|
||||
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm test
|
||||
4
.husky/prepare-commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
exec < /dev/tty && git cz --hook || true
|
||||
1
.vscode/settings.json
vendored
@@ -15,6 +15,7 @@
|
||||
"database": "./config/db/db.sqlite3"
|
||||
}
|
||||
],
|
||||
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locale"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
|
||||
158
README.md
@@ -1,158 +1,50 @@
|
||||
<p align="center">
|
||||
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
|
||||
<img src="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/stable/public/logo.png" 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/ckbvBtDJgC"><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 | armv7 |
|
||||
|
||||
## 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/ckbvBtDJgC).
|
||||
- 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!
|
||||
|
||||
[](https://www.buymeacoffee.com/fallen.bagel)
|
||||
|
||||
@@ -55,10 +55,13 @@ components:
|
||||
readOnly: true
|
||||
username:
|
||||
type: string
|
||||
plexUsername:
|
||||
type: string
|
||||
readOnly: true
|
||||
plexToken:
|
||||
type: string
|
||||
readOnly: true
|
||||
plexUsername:
|
||||
jellyfinAuthToken:
|
||||
type: string
|
||||
readOnly: true
|
||||
userType:
|
||||
@@ -127,6 +130,9 @@ components:
|
||||
localLogin:
|
||||
type: boolean
|
||||
example: true
|
||||
mediaServerType:
|
||||
type: number
|
||||
example: 1
|
||||
newPlexLogin:
|
||||
type: boolean
|
||||
example: true
|
||||
@@ -298,6 +304,47 @@ components:
|
||||
- provides
|
||||
- owned
|
||||
- connection
|
||||
JellyfinLibrary:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
example: Movies
|
||||
enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- enabled
|
||||
JellyfinSettings:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: 'Main Server'
|
||||
readOnly: true
|
||||
hostname:
|
||||
type: string
|
||||
example: 'http://my.jellyfin.host'
|
||||
adminUser:
|
||||
type: string
|
||||
example: 'admin'
|
||||
adminPass:
|
||||
type: string
|
||||
example: 'mypassword'
|
||||
libraries:
|
||||
type: array
|
||||
readOnly: true
|
||||
items:
|
||||
$ref: '#/components/schemas/JellyfinLibrary'
|
||||
serverID:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- hostname
|
||||
RadarrSettings:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1762,6 +1809,136 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MainSettings'
|
||||
/settings/jellyfin:
|
||||
get:
|
||||
summary: Get Jellyfin settings
|
||||
description: Retrieves current Jellyfin settings.
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JellyfinSettings'
|
||||
post:
|
||||
summary: Update Jellyfin settings
|
||||
description: Updates Jellyfin settings with the provided values.
|
||||
tags:
|
||||
- settings
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JellyfinSettings'
|
||||
responses:
|
||||
'200':
|
||||
description: 'Values were successfully updated'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JellyfinSettings'
|
||||
/settings/jellyfin/library:
|
||||
get:
|
||||
summary: Get Jellyfin libraries
|
||||
description: Returns a list of Jellyfin libraries in a JSON array.
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: query
|
||||
name: sync
|
||||
description: Syncs the current libraries with the current Jellyfin server
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- in: query
|
||||
name: enable
|
||||
explode: false
|
||||
allowReserved: true
|
||||
description: Comma separated list of libraries to enable. Any libraries not passed will be disabled!
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: 'Jellyfin libraries returned'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/JellyfinLibrary'
|
||||
/settings/jellyfin/sync:
|
||||
get:
|
||||
summary: Get status of full Jellyfin library sync
|
||||
description: Returns sync progress in a JSON array.
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
'200':
|
||||
description: Status of Jellyfin sync
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
running:
|
||||
type: boolean
|
||||
example: false
|
||||
progress:
|
||||
type: number
|
||||
example: 0
|
||||
total:
|
||||
type: number
|
||||
example: 100
|
||||
currentLibrary:
|
||||
$ref: '#/components/schemas/JellyfinLibrary'
|
||||
libraries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/JellyfinLibrary'
|
||||
post:
|
||||
summary: Start full Jellyfin library sync
|
||||
description: Runs a full Jellyfin library sync and returns the progress in a JSON array.
|
||||
tags:
|
||||
- settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cancel:
|
||||
type: boolean
|
||||
example: false
|
||||
start:
|
||||
type: boolean
|
||||
example: false
|
||||
responses:
|
||||
'200':
|
||||
description: Status of Jellyfin sync
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
running:
|
||||
type: boolean
|
||||
example: false
|
||||
progress:
|
||||
type: number
|
||||
example: 0
|
||||
total:
|
||||
type: number
|
||||
example: 100
|
||||
currentLibrary:
|
||||
$ref: '#/components/schemas/JellyfinLibrary'
|
||||
libraries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/JellyfinLibrary'
|
||||
/settings/plex:
|
||||
get:
|
||||
summary: Get Plex settings
|
||||
@@ -2879,6 +3056,38 @@ paths:
|
||||
type: string
|
||||
required:
|
||||
- authToken
|
||||
/auth/jellyfin:
|
||||
post:
|
||||
summary: Sign in using a Jellyfin username and password
|
||||
description: Takes the user's username and password to log the user in. Generates a session cookie for use in further requests. If the user does not exist, and there are no other users, then a user will be created with full admin privileges. If a user logs in with access to the Jellyfin server, they will also have an account created, but without any permissions.
|
||||
security: []
|
||||
tags:
|
||||
- auth
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
hostname:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
/auth/local:
|
||||
post:
|
||||
summary: Sign in using a local account
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"swr": "^0.5.6",
|
||||
"typeorm": "0.2.37",
|
||||
"uuid": "^8.3.2",
|
||||
"web-push": "^3.4.5",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.5",
|
||||
@@ -77,6 +78,7 @@
|
||||
"@babel/cli": "^7.15.7",
|
||||
"@commitlint/cli": "^13.1.0",
|
||||
"@commitlint/config-conventional": "^13.1.0",
|
||||
"@fullhuman/postcss-purgecss": "3.0.0",
|
||||
"@semantic-release/changelog": "^5.0.1",
|
||||
"@semantic-release/commit-analyzer": "^9.0.1",
|
||||
"@semantic-release/exec": "^5.0.0",
|
||||
@@ -103,6 +105,7 @@
|
||||
"@types/react-transition-group": "^4.4.3",
|
||||
"@types/secure-random-password": "^0.2.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@types/web-push": "^3.3.2",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -1 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.2;fill-rule:evenodd;clip-rule:evenodd;fill:#131928;enable-background:new ;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);}
|
||||
.st2{fill:#000B25;}
|
||||
.st3{fill:#FFFFFF;}
|
||||
.st4{fill:url(#SVGID_00000095336865710271825490000009683653333454385338_);}
|
||||
.st5{fill-rule:evenodd;clip-rule:evenodd;fill:#1D1D1B;}
|
||||
</style>
|
||||
<path class="st0" d="M80,52c0,15.5-12.5,28-28,28c-15.1,0-27.5-12-28-27c0,0.3,0,0.7,0,1c0,16.6,13.4,30,30,30s30-13.4,30-30
|
||||
S70.6,24,54,24c-0.3,0-0.7,0-1,0C68,24.5,80,36.9,80,52z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="28" y1="90" x2="28" y2="50" gradientTransform="matrix(1 0 0 -1 0 98)">
|
||||
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:0.4"/>
|
||||
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M48,12c-19.9,0-36,16.1-36,36c0,2.2-1.8,4-4,4c-2.2,0-4-1.8-4-4C4,23.7,23.7,4,48,4c2.2,0,4,1.8,4,4
|
||||
C52,10.2,50.2,12,48,12z"/>
|
||||
<ellipse class="st2" cx="48" cy="48" rx="48" ry="46.1"/>
|
||||
<rect x="34.8" y="21.7" class="st3" width="12.9" height="5.3"/>
|
||||
<linearGradient id="SVGID_00000124123951135562400370000011621398912477202562_" gradientUnits="userSpaceOnUse" x1="-178.0748" y1="163.4019" x2="-111.501" y2="124.9661" gradientTransform="matrix(1 0 0 -1 194 202)">
|
||||
<stop offset="0" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0" style="stop-color:#A85DC3"/>
|
||||
<stop offset="0.15" style="stop-color:#9863C5"/>
|
||||
<stop offset="0.43" style="stop-color:#6F74CB"/>
|
||||
<stop offset="0.83" style="stop-color:#2D90D5"/>
|
||||
<stop offset="1" style="stop-color:#0F9DDA"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000124123951135562400370000011621398912477202562_);" d="M82.4,51.4C82,33.8,65.4,17.2,48.2,17
|
||||
C30.8,16.8,13.9,33,13,50.5c-0.4,8.2,0.2,9,8.5,10.4c0.9,9.2-0.5,14.4-6.2,20.8c0.8,0.7,1.5,1.3,2.4,2c0.2-0.2,0.4-0.4,0.5-0.7
|
||||
c5.1-7,7.6-9.9,7-18c-0.4-5.4,1.1-7.9,6.9-7.6c0.9,10.8-1.4,16.1-5.9,26.2c-0.7,1.5-1.4,2.9-2.3,4.2c1.1,0.6,2.2,1.2,3.3,1.7
|
||||
c1.2-2,2.1-4.2,2.8-6.6c2.6-9.2,4.8-13.8,5.7-23.4c0.3-3.7,1.4-5.1,5.5-4.2c1.4,11.7-1.2,17.9-4.8,29.2c-0.8,2.6-1.8,5-3,7.3
|
||||
c1.1,0.3,2.2,0.6,3.3,0.9c0.7-1.4,1.4-2.9,2-4.4c4.4-11.3,7.1-17.9,6.3-29.9c-0.3-4.4,1.4-5.2,4.8-3.9c0.5,8.6-1.1,11.6-0.1,19.9
|
||||
c0.8,7.2,2.8,13.5,5.7,19c1.1-0.2,2.1-0.4,3.1-0.6c-5.1-10.1-4.9-22.8-4.7-37.5c4.4-0.9,5.6,0.3,5.8,4.1c0.5,8.5-0.7,11.7,1,20
|
||||
c0.9,4.2,2.5,8,4.9,11.3c1.1-0.4,2.3-0.9,3.4-1.4c-7.3-8.5-6-18.8-5.9-32.2c7.1,1.4,6.5,1.5,7.1,7.5c0.8,7.6,1.3,8.6,3.3,16
|
||||
c0.3,0.9,1.3,2.7,2.5,4.6c1-0.7,1.9-1.4,2.8-2.1c-5.2-10.7-6.4-15-4.5-22.7C81.9,59.8,82.6,59,82.4,51.4L82.4,51.4z M19.6,51.1
|
||||
C17.6,42.4,25.1,29,32,28C28,35.4,23.8,43.3,19.6,51.1L19.6,51.1z M35.4,27.2c-0.2-0.9-0.5-1.8-0.7-2.7c4.1-1,8.2-2,12.3-3l0.7,2.7
|
||||
C43.5,25.2,39.4,26.2,35.4,27.2z"/>
|
||||
<path class="st3" d="M19.6,51.1C17.6,42.4,25.1,29,32,28C28,35.4,23.8,43.3,19.6,51.1L19.6,51.1z"/>
|
||||
<path class="st5" d="M48,30.2c-1.6,0-3.1,0.5-4.3,1.4c0.2,0,0.4,0,0.5,0c2.1,0,3.8,1.7,3.8,3.8S46.3,39,44.2,39
|
||||
c-1.7,0-3.2-1.1-3.6-2.7c-0.1,0.4-0.1,0.9-0.1,1.4c0,4.1,3.4,7.5,7.5,7.5s7.5-3.4,7.5-7.5S52.1,30.2,48,30.2L48,30.2z"/>
|
||||
<circle class="st3" cx="44.3" cy="35.1" r="3.7"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 1.7 MiB |
3
public/preview.jpg:Zone.Identifier
Normal file
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
|
||||
ZoneId=3
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Overseerr",
|
||||
"short_name": "Overseerr",
|
||||
"name": "Jellyseerr",
|
||||
"short_name": "Jellyseerr",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -83,7 +83,7 @@ class GithubAPI extends ExternalAPI {
|
||||
} = {}): Promise<GitHubRelease[]> {
|
||||
try {
|
||||
const data = await this.get<GitHubRelease[]>(
|
||||
'/repos/sct/overseerr/releases',
|
||||
'/repos/Fallenbagel/jellyseerr/releases',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
@@ -110,7 +110,7 @@ class GithubAPI extends ExternalAPI {
|
||||
} = {}): Promise<GithubCommit[]> {
|
||||
try {
|
||||
const data = await this.get<GithubCommit[]>(
|
||||
'/repos/sct/overseerr/commits',
|
||||
'/repos/Fallenbagel/jellyseerr/commits',
|
||||
{
|
||||
params: {
|
||||
per_page: take,
|
||||
@@ -122,7 +122,7 @@ class GithubAPI extends ExternalAPI {
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
|
||||
{ label: 'GitHub API', errorMessage: e.message }
|
||||
);
|
||||
return [];
|
||||
|
||||
267
server/api/jellyfin.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import logger from '../logger';
|
||||
|
||||
export interface JellyfinUserResponse {
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
ServerName: string;
|
||||
Id: string;
|
||||
PrimaryImageTag?: string;
|
||||
}
|
||||
|
||||
export interface JellyfinLoginResponse {
|
||||
User: JellyfinUserResponse;
|
||||
AccessToken: string;
|
||||
}
|
||||
|
||||
export interface JellyfinLibrary {
|
||||
type: 'show' | 'movie';
|
||||
key: string;
|
||||
title: string;
|
||||
agent: string;
|
||||
}
|
||||
|
||||
export interface JellyfinLibraryItem {
|
||||
Name: string;
|
||||
Id: string;
|
||||
HasSubtitles: boolean;
|
||||
Type: 'Movie' | 'Episode' | 'Season' | 'Series';
|
||||
SeriesName?: string;
|
||||
SeriesId?: string;
|
||||
SeasonId?: string;
|
||||
SeasonName?: string;
|
||||
IndexNumber?: number;
|
||||
ParentIndexNumber?: number;
|
||||
MediaType: string;
|
||||
}
|
||||
|
||||
export interface JellyfinMediaStream {
|
||||
Codec: string;
|
||||
Type: 'Video' | 'Audio' | 'Subtitle';
|
||||
Height?: number;
|
||||
Width?: number;
|
||||
AverageFrameRate?: number;
|
||||
RealFrameRate?: number;
|
||||
Language?: string;
|
||||
DisplayTitle: string;
|
||||
}
|
||||
|
||||
export interface JellyfinMediaSource {
|
||||
Protocol: string;
|
||||
Id: string;
|
||||
Path: string;
|
||||
Type: string;
|
||||
VideoType: string;
|
||||
MediaStreams: JellyfinMediaStream[];
|
||||
}
|
||||
|
||||
export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
||||
ProviderIds: {
|
||||
Tmdb?: string;
|
||||
Imdb?: string;
|
||||
Tvdb?: string;
|
||||
};
|
||||
MediaSources?: JellyfinMediaSource[];
|
||||
Width?: number;
|
||||
Height?: number;
|
||||
IsHD?: boolean;
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
class JellyfinAPI {
|
||||
private authToken?: string;
|
||||
private userId?: string;
|
||||
private jellyfinHost: string;
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||
this.jellyfinHost = jellyfinHost;
|
||||
this.authToken = authToken;
|
||||
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
} else {
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
}
|
||||
|
||||
this.axios = axios.create({
|
||||
baseURL: this.jellyfinHost,
|
||||
headers: {
|
||||
'X-Emby-Authorization': authHeaderVal,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async login(
|
||||
Username?: string,
|
||||
Password?: string
|
||||
): Promise<JellyfinLoginResponse> {
|
||||
try {
|
||||
const account = await this.axios.post<JellyfinLoginResponse>(
|
||||
'/Users/AuthenticateByName',
|
||||
{
|
||||
Username: Username,
|
||||
Pw: Password,
|
||||
}
|
||||
);
|
||||
return account.data;
|
||||
} catch (e) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
}
|
||||
|
||||
public setUserId(userId: string): void {
|
||||
this.userId = userId;
|
||||
return;
|
||||
}
|
||||
|
||||
public async getServerName(): Promise<string> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
`/System/Info/Public'}`
|
||||
);
|
||||
return account.data.ServerName;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the server name from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('girl idk');
|
||||
}
|
||||
}
|
||||
|
||||
public async getUser(): Promise<JellyfinUserResponse> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
`/Users/${this.userId ?? 'Me'}`
|
||||
);
|
||||
return account.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
|
||||
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
||||
try {
|
||||
const account = await this.axios.get<any>(
|
||||
`/Users/${this.userId ?? 'Me'}/Views`
|
||||
);
|
||||
|
||||
const response: JellyfinLibrary[] = account.data.Items.filter(
|
||||
(Item: any) => {
|
||||
return (
|
||||
Item.Type === 'CollectionFolder' &&
|
||||
(Item.CollectionType === 'tvshows' ||
|
||||
Item.CollectionType === 'movies')
|
||||
);
|
||||
}
|
||||
).map((Item: any) => {
|
||||
return <JellyfinLibrary>{
|
||||
key: Item.Id,
|
||||
title: Item.Name,
|
||||
type: Item.CollectionType === 'movies' ? 'movie' : 'show',
|
||||
agent: 'jellyfin',
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data.Items;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
|
||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
|
||||
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${this.userId}/Items/${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
|
||||
public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
|
||||
|
||||
return contents.data.Items;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
|
||||
public async getEpisodes(
|
||||
seriesID: string,
|
||||
seasonID: string
|
||||
): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||
);
|
||||
|
||||
return contents.data.Items;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default JellyfinAPI;
|
||||
@@ -122,9 +122,9 @@ class PlexAPI {
|
||||
// },
|
||||
options: {
|
||||
identifier: settings.clientId,
|
||||
product: 'Overseerr',
|
||||
deviceName: 'Overseerr',
|
||||
platform: 'Overseerr',
|
||||
product: 'Jellyseerr',
|
||||
deviceName: 'Jellyseerr',
|
||||
platform: 'Jellyseerr',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
6
server/constants/server.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum MediaServerType {
|
||||
PLEX = 1,
|
||||
JELLYFIN,
|
||||
EMBY,
|
||||
NOT_CONFIGURED,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum UserType {
|
||||
PLEX = 1,
|
||||
LOCAL = 2,
|
||||
JELLYFIN = 3,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import RadarrAPI from '../api/servarr/radarr';
|
||||
import SonarrAPI from '../api/servarr/sonarr';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
@@ -133,36 +134,41 @@ class Media {
|
||||
@Column({ nullable: true })
|
||||
public ratingKey4k?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinMediaId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinMediaId4k?: string;
|
||||
|
||||
public serviceUrl?: string;
|
||||
public serviceUrl4k?: string;
|
||||
public downloadStatus?: DownloadingItem[] = [];
|
||||
public downloadStatus4k?: DownloadingItem[] = [];
|
||||
|
||||
public plexUrl?: string;
|
||||
public plexUrl4k?: string;
|
||||
public mediaUrl?: string;
|
||||
public mediaUrl4k?: string;
|
||||
|
||||
constructor(init?: Partial<Media>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setPlexUrls(): void {
|
||||
const { machineId, webAppUrl } = getSettings().plex;
|
||||
|
||||
if (this.ratingKey) {
|
||||
this.plexUrl = `${
|
||||
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
||||
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
||||
this.ratingKey
|
||||
}`;
|
||||
}
|
||||
|
||||
if (this.ratingKey4k) {
|
||||
this.plexUrl4k = `${
|
||||
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
||||
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
||||
this.ratingKey4k
|
||||
}`;
|
||||
public setMediaUrls(): void {
|
||||
const settings = getSettings();
|
||||
if (settings.main.mediaServerType == MediaServerType.PLEX) {
|
||||
if (this.ratingKey) {
|
||||
this.mediaUrl = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`;
|
||||
}
|
||||
if (this.ratingKey4k) {
|
||||
this.mediaUrl4k = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
|
||||
}
|
||||
} else {
|
||||
if (this.jellyfinMediaId) {
|
||||
this.mediaUrl = `${settings.jellyfin.hostname}/web/index.html#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
}
|
||||
if (this.jellyfinMediaId4k) {
|
||||
this.mediaUrl4k = `${settings.jellyfin.hostname}/web/index.html#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ export class User {
|
||||
@Column({ nullable: true })
|
||||
public plexUsername?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinUsername: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public username?: string;
|
||||
|
||||
@@ -75,10 +78,19 @@ export class User {
|
||||
@Column({ type: 'integer', default: UserType.PLEX })
|
||||
public userType: UserType;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
@Column({ nullable: true })
|
||||
public plexId?: number;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
@Column({ nullable: true })
|
||||
public jellyfinUserId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinDeviceId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinAuthToken?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public plexToken?: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
@@ -226,6 +238,8 @@ export class User {
|
||||
|
||||
@AfterLoad()
|
||||
public setDisplayName(): void {
|
||||
this.displayName =
|
||||
this.username || this.plexUsername || this.jellyfinUsername;
|
||||
this.displayName = this.username || this.plexUsername || this.email;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import { getAppVersion } from './utils/appVersion';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const app = next({ dev });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface SettingsAboutResponse {
|
||||
}
|
||||
|
||||
export interface PublicSettingsResponse {
|
||||
jellyfinHost?: string;
|
||||
jellyfinServerName?: string;
|
||||
initialized: boolean;
|
||||
applicationTitle: string;
|
||||
applicationUrl: string;
|
||||
@@ -29,6 +31,7 @@ export interface PublicSettingsResponse {
|
||||
series4kEnabled: boolean;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
|
||||
665
server/job/jellyfinsync/index.ts
Normal file
@@ -0,0 +1,665 @@
|
||||
import { randomUUID as uuid } from 'crypto';
|
||||
import { uniqWith } from 'lodash';
|
||||
import { getRepository } from 'typeorm';
|
||||
import JellyfinAPI, { JellyfinLibraryItem } from '../../api/jellyfin';
|
||||
import TheMovieDb from '../../api/themoviedb';
|
||||
import { TmdbTvDetails } from '../../api/themoviedb/interfaces';
|
||||
import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import { MediaServerType } from '../../constants/server';
|
||||
import Media from '../../entity/Media';
|
||||
import Season from '../../entity/Season';
|
||||
import { User } from '../../entity/User';
|
||||
import { getSettings, Library } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import AsyncLock from '../../utils/asyncLock';
|
||||
|
||||
const BUNDLE_SIZE = 20;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
|
||||
interface SyncStatus {
|
||||
running: boolean;
|
||||
progress: number;
|
||||
total: number;
|
||||
currentLibrary: Library;
|
||||
libraries: Library[];
|
||||
}
|
||||
|
||||
class JobJellyfinSync {
|
||||
private sessionId: string;
|
||||
private tmdb: TheMovieDb;
|
||||
private jfClient: JellyfinAPI;
|
||||
private items: JellyfinLibraryItem[] = [];
|
||||
private progress = 0;
|
||||
private libraries: Library[];
|
||||
private currentLibrary: Library;
|
||||
private running = false;
|
||||
private isRecentOnly = false;
|
||||
private enable4kMovie = false;
|
||||
private enable4kShow = false;
|
||||
private asyncLock = new AsyncLock();
|
||||
|
||||
constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
|
||||
this.tmdb = new TheMovieDb();
|
||||
this.isRecentOnly = isRecentOnly ?? false;
|
||||
}
|
||||
|
||||
private async getExisting(tmdbId: number, mediaType: MediaType) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
|
||||
const existing = await mediaRepository.findOne({
|
||||
where: { tmdbId: tmdbId, mediaType },
|
||||
});
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
private async processMovie(jellyfinitem: JellyfinLibraryItem) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
|
||||
try {
|
||||
const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
|
||||
const newMedia = new Media();
|
||||
|
||||
if (!metadata.Id) {
|
||||
logger.debug('No Id metadata for this title. Skipping', {
|
||||
label: 'Plex Sync',
|
||||
ratingKey: jellyfinitem.Id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newMedia.tmdbId = Number(metadata.ProviderIds.Tmdb ?? null);
|
||||
newMedia.imdbId = metadata.ProviderIds.Imdb;
|
||||
if (newMedia.imdbId && !isNaN(newMedia.tmdbId)) {
|
||||
const tmdbMovie = await this.tmdb.getMovieByImdbId({
|
||||
imdbId: newMedia.imdbId,
|
||||
});
|
||||
newMedia.tmdbId = tmdbMovie.id;
|
||||
}
|
||||
if (!newMedia.tmdbId) {
|
||||
throw new Error('Unable to find TMDb ID');
|
||||
}
|
||||
|
||||
const has4k = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
return (MediaStream.Width ?? 0) > 2000;
|
||||
});
|
||||
});
|
||||
|
||||
const hasOtherResolution = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
return (MediaStream.Width ?? 0) <= 2000;
|
||||
});
|
||||
});
|
||||
|
||||
await this.asyncLock.dispatch(newMedia.tmdbId, async () => {
|
||||
const existing = await this.getExisting(
|
||||
newMedia.tmdbId,
|
||||
MediaType.MOVIE
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
let changedExisting = false;
|
||||
|
||||
if (
|
||||
(hasOtherResolution || (!this.enable4kMovie && has4k)) &&
|
||||
existing.status !== MediaStatus.AVAILABLE
|
||||
) {
|
||||
existing.status = MediaStatus.AVAILABLE;
|
||||
existing.mediaAddedAt = new Date(metadata.DateCreated ?? '');
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
if (
|
||||
has4k &&
|
||||
this.enable4kMovie &&
|
||||
existing.status4k !== MediaStatus.AVAILABLE
|
||||
) {
|
||||
existing.status4k = MediaStatus.AVAILABLE;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
if (!existing.mediaAddedAt && !changedExisting) {
|
||||
existing.mediaAddedAt = new Date(metadata.DateCreated ?? '');
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
if (
|
||||
(hasOtherResolution || (has4k && !this.enable4kMovie)) &&
|
||||
existing.jellyfinMediaId !== metadata.Id
|
||||
) {
|
||||
existing.jellyfinMediaId = metadata.Id;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
if (
|
||||
has4k &&
|
||||
this.enable4kMovie &&
|
||||
existing.jellyfinMediaId4k !== metadata.Id
|
||||
) {
|
||||
existing.jellyfinMediaId4k = metadata.Id;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
if (changedExisting) {
|
||||
await mediaRepository.save(existing);
|
||||
this.log(
|
||||
`Request for ${metadata.Name} exists. New media types set to AVAILABLE`,
|
||||
'info'
|
||||
);
|
||||
} else {
|
||||
this.log(
|
||||
`Title already exists and no new media types found ${metadata.Name}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newMedia.status =
|
||||
hasOtherResolution || (!this.enable4kMovie && has4k)
|
||||
? MediaStatus.AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
newMedia.status4k =
|
||||
has4k && this.enable4kMovie
|
||||
? MediaStatus.AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
newMedia.mediaAddedAt = new Date(metadata.DateCreated ?? '');
|
||||
newMedia.jellyfinMediaId =
|
||||
hasOtherResolution || (!this.enable4kMovie && has4k)
|
||||
? metadata.Id
|
||||
: undefined;
|
||||
newMedia.jellyfinMediaId4k =
|
||||
has4k && this.enable4kMovie ? metadata.Id : undefined;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${metadata.Name}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Failed to process Jellyfin item, id: ${jellyfinitem.Id}`,
|
||||
'error',
|
||||
{
|
||||
errorMessage: e.message,
|
||||
jellyfinitem,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processShow(jellyfinitem: JellyfinLibraryItem) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
|
||||
let tvShow: TmdbTvDetails | null = null;
|
||||
|
||||
try {
|
||||
const Id =
|
||||
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id;
|
||||
const metadata = await this.jfClient.getItemData(Id);
|
||||
|
||||
if (metadata.ProviderIds.Tvdb) {
|
||||
tvShow = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||
});
|
||||
} else if (metadata.ProviderIds.Tmdb) {
|
||||
tvShow = await this.tmdb.getTvShow({
|
||||
tvId: Number(metadata.ProviderIds.Tmdb),
|
||||
});
|
||||
}
|
||||
|
||||
if (tvShow) {
|
||||
await this.asyncLock.dispatch(tvShow.id, async () => {
|
||||
if (!tvShow) {
|
||||
// this will never execute, but typescript thinks somebody could reset tvShow from
|
||||
// outer scope back to null before this async gets called
|
||||
return;
|
||||
}
|
||||
|
||||
// Lets get the available seasons from Jellyfin
|
||||
const seasons = tvShow.seasons;
|
||||
const media = await this.getExisting(tvShow.id, MediaType.TV);
|
||||
|
||||
const newSeasons: Season[] = [];
|
||||
|
||||
const currentStandardSeasonAvailable = (
|
||||
media?.seasons.filter(
|
||||
(season) => season.status === MediaStatus.AVAILABLE
|
||||
) ?? []
|
||||
).length;
|
||||
const current4kSeasonAvailable = (
|
||||
media?.seasons.filter(
|
||||
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
for (const season of seasons) {
|
||||
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
|
||||
const matchedJellyfinSeason = JellyfinSeasons.find(
|
||||
(md) => Number(md.IndexNumber) === season.season_number
|
||||
);
|
||||
|
||||
const existingSeason = media?.seasons.find(
|
||||
(es) => es.seasonNumber === season.season_number
|
||||
);
|
||||
|
||||
// Check if we found the matching season and it has all the available episodes
|
||||
if (matchedJellyfinSeason) {
|
||||
// If we have a matched Jellyfin season, get its children metadata so we can check details
|
||||
const episodes = await this.jfClient.getEpisodes(
|
||||
Id,
|
||||
matchedJellyfinSeason.Id
|
||||
);
|
||||
|
||||
//Get count of episodes that are HD and 4K
|
||||
let totalStandard = 0;
|
||||
let total4k = 0;
|
||||
|
||||
//use for loop to make sure this loop _completes_ in full
|
||||
//before the next section
|
||||
for (const episode of episodes) {
|
||||
if (!this.enable4kShow) {
|
||||
totalStandard++;
|
||||
} else {
|
||||
const ExtendedEpisodeData = await this.jfClient.getItemData(
|
||||
episode.Id
|
||||
);
|
||||
|
||||
ExtendedEpisodeData.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
if (MediaStream.Type === 'Video') {
|
||||
if (MediaStream.Width ?? 0 < 2000) {
|
||||
totalStandard++;
|
||||
}
|
||||
} else {
|
||||
total4k++;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
media &&
|
||||
(totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) &&
|
||||
media.jellyfinMediaId !== Id
|
||||
) {
|
||||
media.jellyfinMediaId = Id;
|
||||
}
|
||||
|
||||
if (
|
||||
media &&
|
||||
total4k > 0 &&
|
||||
this.enable4kShow &&
|
||||
media.jellyfinMediaId4k !== Id
|
||||
) {
|
||||
media.jellyfinMediaId4k = Id;
|
||||
}
|
||||
|
||||
if (existingSeason) {
|
||||
// These ternary statements look super confusing, but they are simply
|
||||
// setting the status to AVAILABLE if all of a type is there, partially if some,
|
||||
// and then not modifying the status if there are 0 items
|
||||
existingSeason.status =
|
||||
totalStandard === season.episode_count
|
||||
? MediaStatus.AVAILABLE
|
||||
: totalStandard > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: existingSeason.status;
|
||||
existingSeason.status4k =
|
||||
this.enable4kShow && total4k === season.episode_count
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow && total4k > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: existingSeason.status4k;
|
||||
} else {
|
||||
newSeasons.push(
|
||||
new Season({
|
||||
seasonNumber: season.season_number,
|
||||
// This ternary is the same as the ones above, but it just falls back to "UNKNOWN"
|
||||
// if we dont have any items for the season
|
||||
status:
|
||||
totalStandard === season.episode_count
|
||||
? MediaStatus.AVAILABLE
|
||||
: totalStandard > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
this.enable4kShow && total4k === season.episode_count
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow && total4k > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove extras season. We dont count it for determining availability
|
||||
const filteredSeasons = tvShow.seasons.filter(
|
||||
(season) => season.season_number !== 0
|
||||
);
|
||||
|
||||
const isAllStandardSeasons =
|
||||
newSeasons.filter(
|
||||
(season) => season.status === MediaStatus.AVAILABLE
|
||||
).length +
|
||||
(media?.seasons.filter(
|
||||
(season) => season.status === MediaStatus.AVAILABLE
|
||||
).length ?? 0) >=
|
||||
filteredSeasons.length;
|
||||
|
||||
const isAll4kSeasons =
|
||||
newSeasons.filter(
|
||||
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||
).length +
|
||||
(media?.seasons.filter(
|
||||
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||
).length ?? 0) >=
|
||||
filteredSeasons.length;
|
||||
|
||||
if (media) {
|
||||
// Update existing
|
||||
media.seasons = [...media.seasons, ...newSeasons];
|
||||
|
||||
const newStandardSeasonAvailable = (
|
||||
media.seasons.filter(
|
||||
(season) => season.status === MediaStatus.AVAILABLE
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
const new4kSeasonAvailable = (
|
||||
media.seasons.filter(
|
||||
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
// If at least one new season has become available, update
|
||||
// the lastSeasonChange field so we can trigger notifications
|
||||
if (newStandardSeasonAvailable > currentStandardSeasonAvailable) {
|
||||
this.log(
|
||||
`Detected ${
|
||||
newStandardSeasonAvailable - currentStandardSeasonAvailable
|
||||
} new standard season(s) for ${tvShow.name}`,
|
||||
'debug'
|
||||
);
|
||||
media.lastSeasonChange = new Date();
|
||||
media.mediaAddedAt = new Date(metadata.DateCreated ?? '');
|
||||
}
|
||||
|
||||
if (new4kSeasonAvailable > current4kSeasonAvailable) {
|
||||
this.log(
|
||||
`Detected ${
|
||||
new4kSeasonAvailable - current4kSeasonAvailable
|
||||
} new 4K season(s) for ${tvShow.name}`,
|
||||
'debug'
|
||||
);
|
||||
media.lastSeasonChange = new Date();
|
||||
}
|
||||
|
||||
if (!media.mediaAddedAt) {
|
||||
media.mediaAddedAt = new Date(metadata.DateCreated ?? '');
|
||||
}
|
||||
|
||||
// If the show is already available, and there are no new seasons, dont adjust
|
||||
// the status
|
||||
const shouldStayAvailable =
|
||||
media.status === MediaStatus.AVAILABLE &&
|
||||
newSeasons.filter(
|
||||
(season) => season.status !== MediaStatus.UNKNOWN
|
||||
).length === 0;
|
||||
const shouldStayAvailable4k =
|
||||
media.status4k === MediaStatus.AVAILABLE &&
|
||||
newSeasons.filter(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
).length === 0;
|
||||
|
||||
media.status =
|
||||
isAllStandardSeasons || shouldStayAvailable
|
||||
? MediaStatus.AVAILABLE
|
||||
: media.seasons.some(
|
||||
(season) => season.status !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
media.status4k =
|
||||
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow &&
|
||||
media.seasons.some(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
this.log(`Updating existing title: ${tvShow.name}`);
|
||||
} else {
|
||||
const newMedia = new Media({
|
||||
mediaType: MediaType.TV,
|
||||
seasons: newSeasons,
|
||||
tmdbId: tvShow.id,
|
||||
tvdbId: tvShow.external_ids.tvdb_id,
|
||||
mediaAddedAt: new Date(metadata.DateCreated ?? ''),
|
||||
jellyfinMediaId: Id,
|
||||
jellyfinMediaId4k: Id,
|
||||
status: isAllStandardSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: newSeasons.some(
|
||||
(season) => season.status !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
isAll4kSeasons && this.enable4kShow
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow &&
|
||||
newSeasons.some(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
});
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${tvShow.name}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.log(`failed show: ${metadata.Name}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.log(
|
||||
`Failed to process Jellyfin item. Id: ${
|
||||
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id
|
||||
}`,
|
||||
'error',
|
||||
{
|
||||
errorMessage: e.message,
|
||||
jellyfinitem,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processItems(slicedItems: JellyfinLibraryItem[]) {
|
||||
await Promise.all(
|
||||
slicedItems.map(async (item) => {
|
||||
if (item.Type === 'Movie') {
|
||||
await this.processMovie(item);
|
||||
} else if (item.Type === 'Series') {
|
||||
await this.processShow(item);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async loop({
|
||||
start = 0,
|
||||
end = BUNDLE_SIZE,
|
||||
sessionId,
|
||||
}: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
sessionId?: string;
|
||||
} = {}) {
|
||||
const slicedItems = this.items.slice(start, end);
|
||||
|
||||
if (!this.running) {
|
||||
throw new Error('Sync was aborted.');
|
||||
}
|
||||
|
||||
if (this.sessionId !== sessionId) {
|
||||
throw new Error('New session was started. Old session aborted.');
|
||||
}
|
||||
|
||||
if (start < this.items.length) {
|
||||
this.progress = start;
|
||||
await this.processItems(slicedItems);
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
this.loop({
|
||||
start: start + BUNDLE_SIZE,
|
||||
end: end + BUNDLE_SIZE,
|
||||
sessionId,
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((e) => reject(new Error(e.message)));
|
||||
}, UPDATE_RATE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private log(
|
||||
message: string,
|
||||
level: 'info' | 'error' | 'debug' | 'warn' = 'debug',
|
||||
optional?: Record<string, unknown>
|
||||
): void {
|
||||
logger[level](message, { label: 'Jellyfin Sync', ...optional });
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings.main.mediaServerType != MediaServerType.JELLYFIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = uuid();
|
||||
this.sessionId = sessionId;
|
||||
logger.info('Jellyfin Sync Starting', {
|
||||
sessionId,
|
||||
label: 'Jellyfin Sync',
|
||||
});
|
||||
try {
|
||||
this.running = true;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: [
|
||||
'id',
|
||||
'jellyfinAuthToken',
|
||||
'jellyfinUserId',
|
||||
'jellyfinDeviceId',
|
||||
],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
|
||||
}
|
||||
|
||||
this.jfClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
this.jfClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
|
||||
this.libraries = settings.jellyfin.libraries.filter(
|
||||
(library) => library.enabled
|
||||
);
|
||||
|
||||
this.enable4kMovie = settings.radarr.some((radarr) => radarr.is4k);
|
||||
if (this.enable4kMovie) {
|
||||
this.log(
|
||||
'At least one 4K Radarr server was detected. 4K movie detection is now enabled',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
this.enable4kShow = settings.sonarr.some((sonarr) => sonarr.is4k);
|
||||
if (this.enable4kShow) {
|
||||
this.log(
|
||||
'At least one 4K Sonarr server was detected. 4K series detection is now enabled',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isRecentOnly) {
|
||||
for (const library of this.libraries) {
|
||||
this.currentLibrary = library;
|
||||
this.log(
|
||||
`Beginning to process recently added for library: ${library.name}`,
|
||||
'info'
|
||||
);
|
||||
const libraryItems = await this.jfClient.getRecentlyAdded(library.id);
|
||||
|
||||
// Bundle items up by rating keys
|
||||
this.items = uniqWith(libraryItems, (mediaA, mediaB) => {
|
||||
if (mediaA.SeriesId && mediaB.SeriesId) {
|
||||
return mediaA.SeriesId === mediaB.SeriesId;
|
||||
}
|
||||
|
||||
if (mediaA.SeasonId && mediaB.SeasonId) {
|
||||
return mediaA.SeasonId === mediaB.SeasonId;
|
||||
}
|
||||
|
||||
return mediaA.Id === mediaB.Id;
|
||||
});
|
||||
|
||||
await this.loop({ sessionId });
|
||||
}
|
||||
} else {
|
||||
for (const library of this.libraries) {
|
||||
this.currentLibrary = library;
|
||||
this.log(`Beginning to process library: ${library.name}`, 'info');
|
||||
this.items = await this.jfClient.getLibraryContents(library.id);
|
||||
await this.loop({ sessionId });
|
||||
}
|
||||
}
|
||||
this.log(
|
||||
this.isRecentOnly
|
||||
? 'Recently Added Scan Complete'
|
||||
: 'Full Scan Complete',
|
||||
'info'
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Sync interrupted', {
|
||||
label: 'Jellyfin Sync',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
} finally {
|
||||
// If a new scanning session hasnt started, set running back to false
|
||||
if (this.sessionId === sessionId) {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public status(): SyncStatus {
|
||||
return {
|
||||
running: this.running,
|
||||
progress: this.progress,
|
||||
total: this.items.length,
|
||||
currentLibrary: this.currentLibrary,
|
||||
libraries: this.libraries,
|
||||
};
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobJellyfinFullSync = new JobJellyfinSync();
|
||||
export const jobJellyfinRecentSync = new JobJellyfinSync({
|
||||
isRecentOnly: true,
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import schedule from 'node-schedule';
|
||||
import logger from '../logger';
|
||||
import downloadTracker from '../lib/downloadtracker';
|
||||
import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex';
|
||||
import { radarrScanner } from '../lib/scanners/radarr';
|
||||
import { sonarrScanner } from '../lib/scanners/sonarr';
|
||||
import logger from '../logger';
|
||||
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
|
||||
|
||||
interface ScheduledJob {
|
||||
id: string;
|
||||
@@ -47,6 +48,36 @@ export const startJobs = (): void => {
|
||||
cancelFn: () => plexFullScanner.cancel(),
|
||||
});
|
||||
|
||||
// Run recently added jellyfin sync every 5 minutes
|
||||
scheduledJobs.push({
|
||||
id: 'jellyfin-recently-added-sync',
|
||||
name: 'Jellyfin Recently Added Sync',
|
||||
type: 'process',
|
||||
job: schedule.scheduleJob('0 */5 * * * *', () => {
|
||||
logger.info('Starting scheduled job: Jellyfin Recently Added Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
jobJellyfinRecentSync.run();
|
||||
}),
|
||||
running: () => jobJellyfinRecentSync.status().running,
|
||||
cancelFn: () => jobJellyfinRecentSync.cancel(),
|
||||
});
|
||||
|
||||
// Run full jellyfin sync every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'jellyfin-full-sync',
|
||||
name: 'Jellyfin Full Library Sync',
|
||||
type: 'process',
|
||||
job: schedule.scheduleJob('0 0 3 * * *', () => {
|
||||
logger.info('Starting scheduled job: Jellyfin Full Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
jobJellyfinFullSync.run();
|
||||
}),
|
||||
running: () => jobJellyfinFullSync.status().running,
|
||||
cancelFn: () => jobJellyfinFullSync.cancel(),
|
||||
});
|
||||
|
||||
// Run full radarr scan every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'radarr-scan',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import MailMessage from 'nodemailer/lib/mailer/mail-message';
|
||||
import * as openpgp from 'openpgp';
|
||||
import { Transform, TransformCallback } from 'stream';
|
||||
|
||||
@@ -25,7 +26,7 @@ class PGPEncryptor extends Transform {
|
||||
|
||||
// just save the whole message
|
||||
_transform = (
|
||||
chunk: any,
|
||||
chunk: Uint8Array,
|
||||
_encoding: BufferEncoding,
|
||||
callback: TransformCallback
|
||||
): void => {
|
||||
@@ -166,17 +167,16 @@ class PGPEncryptor extends Transform {
|
||||
}
|
||||
|
||||
export const openpgpEncrypt = (options: EncryptorOptions) => {
|
||||
return function (mail: any, callback: () => unknown): void {
|
||||
return function (mail: MailMessage, callback: () => unknown): void {
|
||||
if (!options.encryptionKeys.length) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
mail.message.transform(
|
||||
() =>
|
||||
new PGPEncryptor({
|
||||
signingKey: options.signingKey,
|
||||
password: options.password,
|
||||
encryptionKeys: options.encryptionKeys,
|
||||
})
|
||||
new PGPEncryptor({
|
||||
signingKey: options.signingKey,
|
||||
password: options.password,
|
||||
encryptionKeys: options.encryptionKeys,
|
||||
})
|
||||
);
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
@@ -91,8 +91,7 @@ interface DiscordWebhookPayload {
|
||||
|
||||
class DiscordAgent
|
||||
extends BaseAgent<NotificationAgentDiscord>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentDiscord {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
|
||||
@@ -16,8 +16,7 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
class EmailAgent
|
||||
extends BaseAgent<NotificationAgentEmail>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentEmail {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
|
||||
@@ -7,8 +7,7 @@ import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||
|
||||
class LunaSeaAgent
|
||||
extends BaseAgent<NotificationAgentLunaSea>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentLunaSea {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
|
||||
@@ -12,8 +12,7 @@ interface PushbulletPayload {
|
||||
|
||||
class PushbulletAgent
|
||||
extends BaseAgent<NotificationAgentPushbullet>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentPushbullet {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
|
||||
@@ -18,8 +18,7 @@ interface PushoverPayload {
|
||||
|
||||
class PushoverAgent
|
||||
extends BaseAgent<NotificationAgentPushover>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentPushover {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
@@ -176,8 +175,13 @@ class PushoverAgent
|
||||
try {
|
||||
const endpoint = 'https://api.pushover.net/1/messages.json';
|
||||
|
||||
const { title, message, url, url_title, priority } =
|
||||
this.constructMessageDetails(type, payload);
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
url,
|
||||
url_title,
|
||||
priority,
|
||||
} = this.constructMessageDetails(type, payload);
|
||||
|
||||
await axios.post(endpoint, {
|
||||
token: settings.options.accessToken,
|
||||
|
||||
@@ -203,7 +203,7 @@ class SlackAgent
|
||||
action_id: 'button-action',
|
||||
type: 'button',
|
||||
url: actionUrl,
|
||||
value: 'open_overseerr',
|
||||
value: 'open_jellyseerr',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `Open in ${settings.main.applicationTitle}`,
|
||||
|
||||
@@ -29,8 +29,7 @@ interface TelegramPhotoPayload {
|
||||
|
||||
class TelegramAgent
|
||||
extends BaseAgent<NotificationAgentTelegram>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
private baseUrl = 'https://api.telegram.org/';
|
||||
|
||||
protected getSettings(): NotificationAgentTelegram {
|
||||
|
||||
@@ -40,8 +40,7 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
|
||||
|
||||
class WebhookAgent
|
||||
extends BaseAgent<NotificationAgentWebhook>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentWebhook {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
|
||||
@@ -26,8 +26,7 @@ interface PushNotificationPayload {
|
||||
|
||||
class WebPushAgent
|
||||
extends BaseAgent<NotificationAgentConfig>
|
||||
implements NotificationAgent
|
||||
{
|
||||
implements NotificationAgent {
|
||||
protected getSettings(): NotificationAgentConfig {
|
||||
if (this.settings) {
|
||||
return this.settings;
|
||||
|
||||
@@ -146,8 +146,9 @@ class BaseScanner<T> {
|
||||
existing[is4k ? 'externalServiceId4k' : 'externalServiceId'] !==
|
||||
externalServiceId
|
||||
) {
|
||||
existing[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
externalServiceId;
|
||||
existing[
|
||||
is4k ? 'externalServiceId4k' : 'externalServiceId'
|
||||
] = externalServiceId;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
@@ -156,8 +157,9 @@ class BaseScanner<T> {
|
||||
existing[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !==
|
||||
externalServiceSlug
|
||||
) {
|
||||
existing[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
externalServiceSlug;
|
||||
existing[
|
||||
is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
|
||||
] = externalServiceSlug;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
@@ -388,13 +390,15 @@ class BaseScanner<T> {
|
||||
}
|
||||
|
||||
if (externalServiceId !== undefined) {
|
||||
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
externalServiceId;
|
||||
media[
|
||||
is4k ? 'externalServiceId4k' : 'externalServiceId'
|
||||
] = externalServiceId;
|
||||
}
|
||||
|
||||
if (externalServiceSlug !== undefined) {
|
||||
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
externalServiceSlug;
|
||||
media[
|
||||
is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
|
||||
] = externalServiceSlug;
|
||||
}
|
||||
|
||||
// If the show is already available, and there are no new seasons, dont adjust
|
||||
|
||||
@@ -31,8 +31,7 @@ type SyncStatus = StatusBase & {
|
||||
|
||||
class PlexScanner
|
||||
extends BaseScanner<PlexLibraryItem>
|
||||
implements RunnableScanner<SyncStatus>
|
||||
{
|
||||
implements RunnableScanner<SyncStatus> {
|
||||
private plexClient: PlexAPI;
|
||||
private libraries: Library[];
|
||||
private currentLibrary: Library;
|
||||
|
||||
@@ -10,8 +10,7 @@ type SyncStatus = StatusBase & {
|
||||
|
||||
class RadarrScanner
|
||||
extends BaseScanner<RadarrMovie>
|
||||
implements RunnableScanner<SyncStatus>
|
||||
{
|
||||
implements RunnableScanner<SyncStatus> {
|
||||
private servers: RadarrSettings[];
|
||||
private currentServer: RadarrSettings;
|
||||
private radarrApi: RadarrAPI;
|
||||
|
||||
@@ -16,8 +16,7 @@ type SyncStatus = StatusBase & {
|
||||
|
||||
class SonarrScanner
|
||||
extends BaseScanner<SonarrSeries>
|
||||
implements RunnableScanner<SyncStatus>
|
||||
{
|
||||
implements RunnableScanner<SyncStatus> {
|
||||
private servers: SonarrSettings[];
|
||||
private currentServer: SonarrSettings;
|
||||
private sonarrApi: SonarrAPI;
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import webpush from 'web-push';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
import { Permission } from './permissions';
|
||||
|
||||
export interface Library {
|
||||
@@ -35,6 +36,13 @@ export interface PlexSettings {
|
||||
webAppUrl?: string;
|
||||
}
|
||||
|
||||
export interface JellyfinSettings {
|
||||
name: string;
|
||||
hostname?: string;
|
||||
libraries: Library[];
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export interface DVRSettings {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -90,6 +98,7 @@ export interface MainSettings {
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
trustProxy: boolean;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
locale: string;
|
||||
}
|
||||
@@ -107,6 +116,9 @@ interface FullPublicSettings extends PublicSettings {
|
||||
series4kEnabled: boolean;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
jellyfinHost?: string;
|
||||
jellyfinServerName?: string;
|
||||
partialRequestsEnabled: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
@@ -221,6 +233,7 @@ interface AllSettings {
|
||||
vapidPrivate: string;
|
||||
main: MainSettings;
|
||||
plex: PlexSettings;
|
||||
jellyfin: JellyfinSettings;
|
||||
radarr: RadarrSettings[];
|
||||
sonarr: SonarrSettings[];
|
||||
public: PublicSettings;
|
||||
@@ -241,7 +254,7 @@ class Settings {
|
||||
vapidPublic: '',
|
||||
main: {
|
||||
apiKey: '',
|
||||
applicationTitle: 'Overseerr',
|
||||
applicationTitle: 'Jellyseerr',
|
||||
applicationUrl: '',
|
||||
csrfProtection: false,
|
||||
cacheImages: false,
|
||||
@@ -256,6 +269,7 @@ class Settings {
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
trustProxy: false,
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
locale: 'en',
|
||||
},
|
||||
@@ -266,6 +280,12 @@ class Settings {
|
||||
useSsl: false,
|
||||
libraries: [],
|
||||
},
|
||||
jellyfin: {
|
||||
name: '',
|
||||
hostname: '',
|
||||
libraries: [],
|
||||
serverId: '',
|
||||
},
|
||||
radarr: [],
|
||||
sonarr: [],
|
||||
public: {
|
||||
@@ -283,7 +303,7 @@ class Settings {
|
||||
ignoreTls: false,
|
||||
requireTls: false,
|
||||
allowSelfSigned: false,
|
||||
senderName: 'Overseerr',
|
||||
senderName: 'Jellyseerr',
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
@@ -372,6 +392,14 @@ class Settings {
|
||||
this.data.plex = data;
|
||||
}
|
||||
|
||||
get jellyfin(): JellyfinSettings {
|
||||
return this.data.jellyfin;
|
||||
}
|
||||
|
||||
set jellyfin(data: JellyfinSettings) {
|
||||
this.data.jellyfin = data;
|
||||
}
|
||||
|
||||
get radarr(): RadarrSettings[] {
|
||||
return this.data.radarr;
|
||||
}
|
||||
@@ -411,6 +439,8 @@ class Settings {
|
||||
),
|
||||
region: this.data.main.region,
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
mediaServerType: this.main.mediaServerType,
|
||||
jellyfinHost: this.jellyfin.hostname,
|
||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||
cacheImages: this.data.main.cacheImages,
|
||||
vapidPublic: this.vapidPublic,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Migrate away from old log
|
||||
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/overseerr.log');
|
||||
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/Jellyseerr.log');
|
||||
if (fs.existsSync(OLD_LOG_FILE)) {
|
||||
const file = fs.lstatSync(OLD_LOG_FILE);
|
||||
|
||||
@@ -43,14 +43,14 @@ const logger = winston.createLogger({
|
||||
}),
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
|
||||
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/Jellyseerr-%DATE%.log`
|
||||
: path.join(__dirname, '../config/logs/Jellyseerr-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '7d',
|
||||
createSymlink: true,
|
||||
symlinkName: 'overseerr.log',
|
||||
symlinkName: 'Jellyseerr.log',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUserRequestDeleteCascades1608219049304
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'AddUserRequestDeleteCascades1608219049304';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddLastSeasonChangeMedia1608477467935
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'AddLastSeasonChangeMedia1608477467935';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class ForceDropImdbUniqueConstraint1608477467935
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'ForceDropImdbUniqueConstraint1608477467936';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveTmdbIdUniqueConstraint1609236552057
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'RemoveTmdbIdUniqueConstraint1609236552057';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddMediaAddedFieldToMedia1610522845513
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'AddMediaAddedFieldToMedia1610522845513';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class SonarrRadarrSyncServiceFields1611757511674
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'SonarrRadarrSyncServiceFields1611757511674';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddResetPasswordGuidAndExpiryDate1612482778137
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'AddResetPasswordGuidAndExpiryDate1612482778137';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
67
server/migration/1613379909641-AddJellyfinUserParams.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddJellyfinUserParams1613379909641 implements MigrationInterface {
|
||||
name = 'AddJellyfinUserParams1613379909641';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinId" varchar, "jellyfinAuthToken" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||
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), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaID" varchar, "jellyfinMediaID4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" FROM "media"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "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") `
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||
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), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" FROM "temporary_media"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_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") `
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
}
|
||||
}
|
||||
91
server/migration/1613412948344-ServerTypeEnum.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class ServerTypeEnum1613412948344 implements MigrationInterface {
|
||||
name = 'ServerTypeEnum1613412948344';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" 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") `
|
||||
);
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaID" varchar, "jellyfinMediaID4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" 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<void> {
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" 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") `
|
||||
);
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" 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") `
|
||||
);
|
||||
}
|
||||
}
|
||||
123
server/migration/1613670041760-AddJellyfinDeviceId.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddJellyfinDeviceId1613670041760 implements MigrationInterface {
|
||||
name = 'AddJellyfinDeviceId1613670041760';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" FROM "media"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "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") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" FROM "media"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "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") `
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" FROM "temporary_media"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_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") `
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||
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), "status4k" 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), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaID" varchar, "jellyfinMediaID4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k" FROM "temporary_media"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_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") `
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinId" varchar, "jellyfinAuthToken" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateUserSettingsRegions1613955393450
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'UpdateUserSettingsRegions1613955393450';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTelegramSettingsToUserSettings1614334195680
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'AddTelegramSettingsToUserSettings1614334195680';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -5,10 +5,10 @@ export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "user"`
|
||||
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
@@ -17,10 +17,10 @@ export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "temporary_user"`
|
||||
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateTagsFieldonMediaRequest1617624225464
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'CreateTagsFieldonMediaRequest1617624225464';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUserSettingsNotificationAgentsField1617730837489
|
||||
implements MigrationInterface
|
||||
{
|
||||
implements MigrationInterface {
|
||||
name = 'AddUserSettingsNotificationAgentsField1617730837489';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
|
||||