Merge branch 'develop' into settings-wip

This commit is contained in:
tidusjar 2022-11-18 20:32:22 +00:00
commit 4c6346091a
47 changed files with 1313 additions and 301 deletions

View file

@ -1,3 +1,84 @@
# [4.31.0](https://github.com/Ombi-app/Ombi/compare/v4.30.0...v4.31.0) (2022-11-18)
### Features
* **sonarr:** Added the ability to add default tags when sending to Sonarr ([#4803](https://github.com/Ombi-app/Ombi/issues/4803)) ([ecfbb8e](https://github.com/Ombi-app/Ombi/commit/ecfbb8eda91e1a90239dcf8be847afcc2394a78e))
# [4.30.0](https://github.com/Ombi-app/Ombi/compare/v4.29.3...v4.30.0) (2022-11-17)
### Features
* **sonarr:** :sparkles: Add the username to a Sonarr tag when sent to Sonarr ([#4802](https://github.com/Ombi-app/Ombi/issues/4802)) ([1d5fabd](https://github.com/Ombi-app/Ombi/commit/1d5fabd317e3ce8f6dd31f06d15dc81277f39dbd))
## [4.29.3](https://github.com/Ombi-app/Ombi/compare/v4.29.2...v4.29.3) (2022-11-14)
### Bug Fixes
* **notifications:** Fixed the Partially TV notifications going to the admin [#4797](https://github.com/Ombi-app/Ombi/issues/4797) ([#4799](https://github.com/Ombi-app/Ombi/issues/4799)) ([bcb3e7f](https://github.com/Ombi-app/Ombi/commit/bcb3e7f00380a4c4278f59dc55febf43e6d05d47))
* Only log error messages from Microsoft ([#4787](https://github.com/Ombi-app/Ombi/issues/4787)) ([c614e0c](https://github.com/Ombi-app/Ombi/commit/c614e0ca5fe5023cbe7ced326145273cd75be85d))
## [4.29.2](https://github.com/Ombi-app/Ombi/compare/v4.29.1...v4.29.2) (2022-10-24)
### Bug Fixes
* **plex:** Fixed an issue where sometimes the availability checker would throw an exception when checking episodes ([17ba202](https://github.com/Ombi-app/Ombi/commit/17ba2020ee0950c2c0e0e03fdb7835b579da75a9))
## [4.29.1](https://github.com/Ombi-app/Ombi/compare/v4.29.0...v4.29.1) (2022-10-22)
### Bug Fixes
* Consistently reset loading flag when requesting movies on discover page. ([#4777](https://github.com/Ombi-app/Ombi/issues/4777)) ([a40ab5c](https://github.com/Ombi-app/Ombi/commit/a40ab5cddf769d4147696eca50c1610b466ab99b))
* **sonarr:** :bug: Fixed an issue where the language list didn't correctly load for power users in the advanced options [#4782](https://github.com/Ombi-app/Ombi/issues/4782) ([2173670](https://github.com/Ombi-app/Ombi/commit/217367047d1568070dd507e54ad3fd2c68f05b88))
# [4.29.0](https://github.com/Ombi-app/Ombi/compare/v4.28.1...v4.29.0) (2022-10-19)
### Bug Fixes
* Partially Available prevents further TV requests ([#4768](https://github.com/Ombi-app/Ombi/issues/4768)) ([#4779](https://github.com/Ombi-app/Ombi/issues/4779)) ([031e2b9](https://github.com/Ombi-app/Ombi/commit/031e2b9283b239827cabaca4e35f69f2f93a4d7b))
* Unable to Delete Jellyfin Server ([#4705](https://github.com/Ombi-app/Ombi/issues/4705)) ([#4780](https://github.com/Ombi-app/Ombi/issues/4780)) ([76a0d0d](https://github.com/Ombi-app/Ombi/commit/76a0d0d26893bd480fea4735f77522ac6261a425))
### Features
* Provide a flag for missing users on Plex Server ([#4688](https://github.com/Ombi-app/Ombi/issues/4688)) ([#4778](https://github.com/Ombi-app/Ombi/issues/4778)) ([b4a14c2](https://github.com/Ombi-app/Ombi/commit/b4a14c2d28218409390e517b226130e3e84efee1))
## [4.28.1](https://github.com/Ombi-app/Ombi/compare/v4.28.0...v4.28.1) (2022-10-19)
### Bug Fixes
* **plex:** :bug: Fixed not being able to enable watchlist requests in the Plex settings ([3e5158e](https://github.com/Ombi-app/Ombi/commit/3e5158ef9cda58ea2dd3be143f07aa5433691d79))
* Reworked the version check ([#4719](https://github.com/Ombi-app/Ombi/issues/4719)) ([#4781](https://github.com/Ombi-app/Ombi/issues/4781)) ([55855c5](https://github.com/Ombi-app/Ombi/commit/55855c5adda3cd1c51b7fbd0c19b469fc813f98e))
# [4.28.0](https://github.com/Ombi-app/Ombi/compare/v4.27.8...v4.28.0) (2022-10-07)
### Features
* **plex:** ✨ Added the ability to configure the watchlist to request the whole TV show rather than latest season ([#4774](https://github.com/Ombi-app/Ombi/issues/4774)) ([fa65712](https://github.com/Ombi-app/Ombi/commit/fa65712bd570fe8d5d21b8ca0abe182b84960017))
## [4.27.8](https://github.com/Ombi-app/Ombi/compare/v4.27.7...v4.27.8) (2022-10-07) ## [4.27.8](https://github.com/Ombi-app/Ombi/compare/v4.27.7...v4.27.8) (2022-10-07)
@ -303,106 +384,3 @@
# [4.17.0](https://github.com/Ombi-app/Ombi/compare/v4.16.17...v4.17.0) (2022-04-25)
### Features
* **discover:** Add original language filter ([ef7ec86](https://github.com/Ombi-app/Ombi/commit/ef7ec861d8aede2a4817752c990617f583805391))
## [4.16.17](https://github.com/Ombi-app/Ombi/compare/v4.16.16...v4.16.17) (2022-04-25)
## [4.16.16](https://github.com/Ombi-app/Ombi/compare/v4.16.15...v4.16.16) (2022-04-25)
### Bug Fixes
* **4616:** :bug: fixed mandatory fields ([d8f2260](https://github.com/Ombi-app/Ombi/commit/d8f2260c7ae3ed48386743b7adbd06e284487034))
## [4.16.15](https://github.com/Ombi-app/Ombi/compare/v4.16.14...v4.16.15) (2022-04-24)
## [4.16.14](https://github.com/Ombi-app/Ombi/compare/v4.16.13...v4.16.14) (2022-04-19)
## [4.16.13](https://github.com/Ombi-app/Ombi/compare/v4.16.12...v4.16.13) (2022-04-19)
## [4.16.12](https://github.com/Ombi-app/Ombi/compare/v4.16.11...v4.16.12) (2022-04-19)
## [4.16.11](https://github.com/Ombi-app/Ombi/compare/v4.16.10...v4.16.11) (2022-04-14)
### Bug Fixes
* Set the default job for the watchlist import to hourly instead of daily ([75906af](https://github.com/Ombi-app/Ombi/commit/75906af0adee3e3c68d825c3aaa8f7b918461b1f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0e8a64b](https://github.com/Ombi-app/Ombi/commit/0e8a64b8ca00d210fbe843ac2c3f6af218d80cbc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7b0ad61](https://github.com/Ombi-app/Ombi/commit/7b0ad61bfcff3986b33180dc64022cba7ea8eefb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4fc2c1f](https://github.com/Ombi-app/Ombi/commit/4fc2c1f24534085a783a3d5791f5533b68272153))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76ab733](https://github.com/Ombi-app/Ombi/commit/76ab733b91791e4d93d184f3c7d0779c6a388695))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([06e4cef](https://github.com/Ombi-app/Ombi/commit/06e4cefa7b4e55b860da9a64f461f6ec8fa17367))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c12d89d](https://github.com/Ombi-app/Ombi/commit/c12d89d6781a337520977ad285f8d08c93f434dd))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bc0c2f6](https://github.com/Ombi-app/Ombi/commit/bc0c2f622e34fb5a2711039d9ed7aad34f982b15))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([e4b00e6](https://github.com/Ombi-app/Ombi/commit/e4b00e6b3468bd9389eeb02fc6ad7daf27abc3b3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1998d3](https://github.com/Ombi-app/Ombi/commit/d1998d326f999a38586d0a351a20c5448df95842))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([bee4ccb](https://github.com/Ombi-app/Ombi/commit/bee4ccb804594e7385b1fbdc9fe2ef5c42e0d21f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([80233ed](https://github.com/Ombi-app/Ombi/commit/80233ed560cc976e83570d0655c3472f20171fb3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a78adc](https://github.com/Ombi-app/Ombi/commit/8a78adc9bb62f277f2b213dcb3847ed6d0089fcb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d04c60a](https://github.com/Ombi-app/Ombi/commit/d04c60aa5909b47ba6bffa6f66b03079cbd43521))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([92a785e](https://github.com/Ombi-app/Ombi/commit/92a785e736fa4b72a45270da2d0f4661df433078))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([634982d](https://github.com/Ombi-app/Ombi/commit/634982df2661cefab5ea9f5163fe04a005cc0171))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([b404baa](https://github.com/Ombi-app/Ombi/commit/b404baad6d0aeaa1561701e0db8db4e78613a364))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d14f11e](https://github.com/Ombi-app/Ombi/commit/d14f11e0eb20ab0a68e765ee77968b3b3e54e995))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([7cf64f9](https://github.com/Ombi-app/Ombi/commit/7cf64f909d78908edaabeffb8a39a7d02e73fe7e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c9e1ec](https://github.com/Ombi-app/Ombi/commit/0c9e1ec090827080cc8f7393e5e91456ff37d691))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([3b0b730](https://github.com/Ombi-app/Ombi/commit/3b0b730cb02efe24f6d4026e5fdb20d37e495119))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6ed1a03](https://github.com/Ombi-app/Ombi/commit/6ed1a03b7ff4077f09ea9e13394b18b0d138f4c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([2941acd](https://github.com/Ombi-app/Ombi/commit/2941acd3b2ec74a5e6aeea275ab5a39d2653f37f))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([c075a1a](https://github.com/Ombi-app/Ombi/commit/c075a1a66784d975eaf60f2dfbbcbe048f2f63d7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([76bd81c](https://github.com/Ombi-app/Ombi/commit/76bd81c3ca55a98c6ec944a838dc01294a6193a6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0d38275](https://github.com/Ombi-app/Ombi/commit/0d3827507e002bcf58f673e97ffcc3bd25dcf337))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5c99601](https://github.com/Ombi-app/Ombi/commit/5c99601b07aec1a65d0186a4c4327440811e64c6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01546a0](https://github.com/Ombi-app/Ombi/commit/01546a0f7f86379528b486463246ef9bdfb9033e))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d7fea78](https://github.com/Ombi-app/Ombi/commit/d7fea7843aaaab7ddff8dc31ca6d2a9117471dcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([1a6b95d](https://github.com/Ombi-app/Ombi/commit/1a6b95d45c220310213b8d811272a63f0f6ff42b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([fa10174](https://github.com/Ombi-app/Ombi/commit/fa1017422c4efd4b0897871bd3c671151774d7c3))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0c31e62](https://github.com/Ombi-app/Ombi/commit/0c31e628df376aac6d56ae67c7c705a9a4a7c080))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6399643](https://github.com/Ombi-app/Ombi/commit/63996437a02fe10ffae6822ffa15369bec0a6b36))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([5826e2d](https://github.com/Ombi-app/Ombi/commit/5826e2d9a1c3f1210a87fa270dc0c81bac32944a))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d434514](https://github.com/Ombi-app/Ombi/commit/d43451405be489254d7cdc7755d5f516a1e495a5))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([0b9596d](https://github.com/Ombi-app/Ombi/commit/0b9596d807178f5e071113ec0347868ec7f0960b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8c4c0b2](https://github.com/Ombi-app/Ombi/commit/8c4c0b262978c1303767af360d802c4b4c2b4d24))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([289ab77](https://github.com/Ombi-app/Ombi/commit/289ab77b0e04aae235b6f6cebc86e0a8d1f0cf2b))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([30e3417](https://github.com/Ombi-app/Ombi/commit/30e3417285a4eed18d429d7776f0e74096e834c0))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([6c0a5da](https://github.com/Ombi-app/Ombi/commit/6c0a5dadd4b8f37760252eb0fe7f88908f55506d))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d5bf969](https://github.com/Ombi-app/Ombi/commit/d5bf9692ce1fc0ccfe7beca6dd200c78be177bdc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8a9e7ea](https://github.com/Ombi-app/Ombi/commit/8a9e7ea588aefbcd73ed82625887e3614e1703ea))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([01047a3](https://github.com/Ombi-app/Ombi/commit/01047a3fd67153f3ff16f860d2c7b50213e8d9b2))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([698a23f](https://github.com/Ombi-app/Ombi/commit/698a23fb83f323cdd1dd57cb49803079d44214a7))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([24eb842](https://github.com/Ombi-app/Ombi/commit/24eb842fc4424f7bcc3ec2949d7f5472492e96f6))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([ac8b16a](https://github.com/Ombi-app/Ombi/commit/ac8b16a3051ad71dbd54a8973c7dd847b564a515))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([f428ce6](https://github.com/Ombi-app/Ombi/commit/f428ce6a700c081437703839bc84d2f2b1138bcc))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([94b16df](https://github.com/Ombi-app/Ombi/commit/94b16dfe09bf1d2cd6286777d74eb5d4496abbbb))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([4881775](https://github.com/Ombi-app/Ombi/commit/4881775eda69a8f136ce0d8fbbf970e3d0406dc9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([8297db9](https://github.com/Ombi-app/Ombi/commit/8297db91e85da308bde6fb09ad78347dee063630))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([d1152ab](https://github.com/Ombi-app/Ombi/commit/d1152ab7674243daa528c524c0cdc87d81ad49c9))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([eb2788b](https://github.com/Ombi-app/Ombi/commit/eb2788b761b55c487a59a049427ca08f6c10e836))
* **translations:** 🌐 New translations from Crowdin [skip ci] ([21a794c](https://github.com/Ombi-app/Ombi/commit/21a794cbc0a5fa735ca0347c8f7f1ac04a487fbc))
## [4.10.2](https://github.com/Ombi-app/Ombi/compare/v4.10.1...v4.10.2) (2022-01-22)

View file

@ -257,6 +257,13 @@ Here are some of the features Ombi has:
<sub><b>Patrick Collins</b></sub> <sub><b>Patrick Collins</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/xweskingx">
<img src="https://avatars.githubusercontent.com/u/6268446?v=4" width="50;" alt="xweskingx"/>
<br />
<sub><b>Wesley King</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/chriscpritchard"> <a href="https://github.com/chriscpritchard">
<img src="https://avatars.githubusercontent.com/u/1839074?v=4" width="50;" alt="chriscpritchard"/> <img src="https://avatars.githubusercontent.com/u/1839074?v=4" width="50;" alt="chriscpritchard"/>
@ -277,15 +284,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Stephen Panzer</b></sub> <sub><b>Stephen Panzer</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/aptalca"> <a href="https://github.com/aptalca">
<img src="https://avatars.githubusercontent.com/u/541623?v=4" width="50;" alt="aptalca"/> <img src="https://avatars.githubusercontent.com/u/541623?v=4" width="50;" alt="aptalca"/>
<br /> <br />
<sub><b>Aptalca</b></sub> <sub><b>Aptalca</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/dr3am37"> <a href="https://github.com/dr3am37">
<img src="https://avatars.githubusercontent.com/u/91037083?v=4" width="50;" alt="dr3am37"/> <img src="https://avatars.githubusercontent.com/u/91037083?v=4" width="50;" alt="dr3am37"/>
@ -320,15 +327,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Andrew Metzger</b></sub> <sub><b>Andrew Metzger</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/au5ton"> <a href="https://github.com/au5ton">
<img src="https://avatars.githubusercontent.com/u/4109551?v=4" width="50;" alt="au5ton"/> <img src="https://avatars.githubusercontent.com/u/4109551?v=4" width="50;" alt="au5ton"/>
<br /> <br />
<sub><b>Austin Jackson</b></sub> <sub><b>Austin Jackson</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/D34DC3N73R"> <a href="https://github.com/D34DC3N73R">
<img src="https://avatars.githubusercontent.com/u/9123670?v=4" width="50;" alt="D34DC3N73R"/> <img src="https://avatars.githubusercontent.com/u/9123670?v=4" width="50;" alt="D34DC3N73R"/>
@ -363,15 +370,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Jack Steel</b></sub> <sub><b>Jack Steel</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/jpeters"> <a href="https://github.com/jpeters">
<img src="https://avatars.githubusercontent.com/u/167401?v=4" width="50;" alt="jpeters"/> <img src="https://avatars.githubusercontent.com/u/167401?v=4" width="50;" alt="jpeters"/>
<br /> <br />
<sub><b>Jeffrey Peters</b></sub> <sub><b>Jeffrey Peters</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/MariusSchiffer"> <a href="https://github.com/MariusSchiffer">
<img src="https://avatars.githubusercontent.com/u/183124?v=4" width="50;" alt="MariusSchiffer"/> <img src="https://avatars.githubusercontent.com/u/183124?v=4" width="50;" alt="MariusSchiffer"/>
@ -406,15 +413,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Javier Pastor</b></sub> <sub><b>Javier Pastor</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/AbeKline"> <a href="https://github.com/AbeKline">
<img src="https://avatars.githubusercontent.com/u/8125653?v=4" width="50;" alt="AbeKline"/> <img src="https://avatars.githubusercontent.com/u/8125653?v=4" width="50;" alt="AbeKline"/>
<br /> <br />
<sub><b>Abe Kline</b></sub> <sub><b>Abe Kline</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/XanderStrike"> <a href="https://github.com/XanderStrike">
<img src="https://avatars.githubusercontent.com/u/1565303?v=4" width="50;" alt="XanderStrike"/> <img src="https://avatars.githubusercontent.com/u/1565303?v=4" width="50;" alt="XanderStrike"/>
@ -449,15 +456,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Calvin</b></sub> <sub><b>Calvin</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/origamirobot"> <a href="https://github.com/origamirobot">
<img src="https://avatars.githubusercontent.com/u/1346803?v=4" width="50;" alt="origamirobot"/> <img src="https://avatars.githubusercontent.com/u/1346803?v=4" width="50;" alt="origamirobot"/>
<br /> <br />
<sub><b>Chris Lees</b></sub> <sub><b>Chris Lees</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/cdemi"> <a href="https://github.com/cdemi">
<img src="https://avatars.githubusercontent.com/u/8025435?v=4" width="50;" alt="cdemi"/> <img src="https://avatars.githubusercontent.com/u/8025435?v=4" width="50;" alt="cdemi"/>
@ -492,15 +499,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>David Torosyan</b></sub> <sub><b>David Torosyan</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/onedr0p"> <a href="https://github.com/onedr0p">
<img src="https://avatars.githubusercontent.com/u/213795?v=4" width="50;" alt="onedr0p"/> <img src="https://avatars.githubusercontent.com/u/213795?v=4" width="50;" alt="onedr0p"/>
<br /> <br />
<sub><b>Devin Buhl</b></sub> <sub><b>Devin Buhl</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/elisspace"> <a href="https://github.com/elisspace">
<img src="https://avatars.githubusercontent.com/u/18365129?v=4" width="50;" alt="elisspace"/> <img src="https://avatars.githubusercontent.com/u/18365129?v=4" width="50;" alt="elisspace"/>
@ -535,15 +542,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Igor Borges</b></sub> <sub><b>Igor Borges</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/ImgBotApp"> <a href="https://github.com/ImgBotApp">
<img src="https://avatars.githubusercontent.com/u/31427850?v=4" width="50;" alt="ImgBotApp"/> <img src="https://avatars.githubusercontent.com/u/31427850?v=4" width="50;" alt="ImgBotApp"/>
<br /> <br />
<sub><b>Imgbot</b></sub> <sub><b>Imgbot</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/JPyke3"> <a href="https://github.com/JPyke3">
<img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="JPyke3"/> <img src="https://avatars.githubusercontent.com/u/13283054?v=4" width="50;" alt="JPyke3"/>
@ -578,15 +585,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Jon Bloom</b></sub> <sub><b>Jon Bloom</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/jonocairns"> <a href="https://github.com/jonocairns">
<img src="https://avatars.githubusercontent.com/u/182836?v=4" width="50;" alt="jonocairns"/> <img src="https://avatars.githubusercontent.com/u/182836?v=4" width="50;" alt="jonocairns"/>
<br /> <br />
<sub><b>Jono Cairns</b></sub> <sub><b>Jono Cairns</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/krisklosterman"> <a href="https://github.com/krisklosterman">
<img src="https://avatars.githubusercontent.com/u/7139579?v=4" width="50;" alt="krisklosterman"/> <img src="https://avatars.githubusercontent.com/u/7139579?v=4" width="50;" alt="krisklosterman"/>
@ -621,15 +628,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Madeleine Schönemann</b></sub> <sub><b>Madeleine Schönemann</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/marleypowell"> <a href="https://github.com/marleypowell">
<img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/> <img src="https://avatars.githubusercontent.com/u/55280588?v=4" width="50;" alt="marleypowell"/>
<br /> <br />
<sub><b>Marley</b></sub> <sub><b>Marley</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/mattmattmatt"> <a href="https://github.com/mattmattmatt">
<img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/> <img src="https://avatars.githubusercontent.com/u/927830?v=4" width="50;" alt="mattmattmatt"/>
@ -664,15 +671,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Nathan Miller</b></sub> <sub><b>Nathan Miller</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/cqxmzz"> <a href="https://github.com/cqxmzz">
<img src="https://avatars.githubusercontent.com/u/3071863?v=4" width="50;" alt="cqxmzz"/> <img src="https://avatars.githubusercontent.com/u/3071863?v=4" width="50;" alt="cqxmzz"/>
<br /> <br />
<sub><b>Qiming Chen</b></sub> <sub><b>Qiming Chen</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/randallbruder"> <a href="https://github.com/randallbruder">
<img src="https://avatars.githubusercontent.com/u/6447487?v=4" width="50;" alt="randallbruder"/> <img src="https://avatars.githubusercontent.com/u/6447487?v=4" width="50;" alt="randallbruder"/>
@ -707,15 +714,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Shoghi</b></sub> <sub><b>Shoghi</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Teifun2"> <a href="https://github.com/Teifun2">
<img src="https://avatars.githubusercontent.com/u/7461832?v=4" width="50;" alt="Teifun2"/> <img src="https://avatars.githubusercontent.com/u/7461832?v=4" width="50;" alt="Teifun2"/>
<br /> <br />
<sub><b>Teifun2</b></sub> <sub><b>Teifun2</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/thomasvt1"> <a href="https://github.com/thomasvt1">
<img src="https://avatars.githubusercontent.com/u/2271011?v=4" width="50;" alt="thomasvt1"/> <img src="https://avatars.githubusercontent.com/u/2271011?v=4" width="50;" alt="thomasvt1"/>
@ -750,15 +757,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Travis Bybee</b></sub> <sub><b>Travis Bybee</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Xirg"> <a href="https://github.com/Xirg">
<img src="https://avatars.githubusercontent.com/u/6020502?v=4" width="50;" alt="Xirg"/> <img src="https://avatars.githubusercontent.com/u/6020502?v=4" width="50;" alt="Xirg"/>
<br /> <br />
<sub><b>Xirg</b></sub> <sub><b>Xirg</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/bazhip"> <a href="https://github.com/bazhip">
<img src="https://avatars.githubusercontent.com/u/10350445?v=4" width="50;" alt="bazhip"/> <img src="https://avatars.githubusercontent.com/u/10350445?v=4" width="50;" alt="bazhip"/>
@ -793,15 +800,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Dorian ALKOUM</b></sub> <sub><b>Dorian ALKOUM</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/echel0n"> <a href="https://github.com/echel0n">
<img src="https://avatars.githubusercontent.com/u/1128022?v=4" width="50;" alt="echel0n"/> <img src="https://avatars.githubusercontent.com/u/1128022?v=4" width="50;" alt="echel0n"/>
<br /> <br />
<sub><b>Echel0n</b></sub> <sub><b>Echel0n</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/m4tta"> <a href="https://github.com/m4tta">
<img src="https://avatars.githubusercontent.com/u/427218?v=4" width="50;" alt="m4tta"/> <img src="https://avatars.githubusercontent.com/u/427218?v=4" width="50;" alt="m4tta"/>
@ -836,15 +843,15 @@ Here are some of the features Ombi has:
<br /> <br />
<sub><b>Sirmarv</b></sub> <sub><b>Sirmarv</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/tdorsey"> <a href="https://github.com/tdorsey">
<img src="https://avatars.githubusercontent.com/u/1218404?v=4" width="50;" alt="tdorsey"/> <img src="https://avatars.githubusercontent.com/u/1218404?v=4" width="50;" alt="tdorsey"/>
<br /> <br />
<sub><b>Tdorsey</b></sub> <sub><b>Tdorsey</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/thegame3202"> <a href="https://github.com/thegame3202">
<img src="https://avatars.githubusercontent.com/u/22148848?v=4" width="50;" alt="thegame3202"/> <img src="https://avatars.githubusercontent.com/u/22148848?v=4" width="50;" alt="thegame3202"/>

View file

@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Api.Sonarr.Models; using Ombi.Api.Sonarr.Models;
using System.Net.Http;
using Ombi.Api.Sonarr.Models.V3; using Ombi.Api.Sonarr.Models.V3;
namespace Ombi.Api.Sonarr namespace Ombi.Api.Sonarr
@ -9,5 +8,7 @@ namespace Ombi.Api.Sonarr
public interface ISonarrV3Api : ISonarrApi public interface ISonarrV3Api : ISonarrApi
{ {
Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl); Task<IEnumerable<LanguageProfiles>> LanguageProfiles(string apiKey, string baseUrl);
Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName);
Task<Tag> GetTag(int tagId, string apiKey, string baseUrl);
} }
} }

View file

@ -26,6 +26,7 @@ namespace Ombi.Api.Sonarr.Models
public string seriesType { get; set; } public string seriesType { get; set; }
public int id { get; set; } public int id { get; set; }
public List<SonarrImage> images { get; set; } public List<SonarrImage> images { get; set; }
public List<int> tags { get; set; }
// V3 Property // V3 Property
public int languageProfileId { get; set; } public int languageProfileId { get; set; }

View file

@ -40,7 +40,7 @@ namespace Ombi.Api.Sonarr.Models
public string titleSlug { get; set; } public string titleSlug { get; set; }
public string certification { get; set; } public string certification { get; set; }
public string[] genres { get; set; } public string[] genres { get; set; }
public object[] tags { get; set; } public List<int> tags { get; set; }
public DateTime added { get; set; } public DateTime added { get; set; }
public Ratings ratings { get; set; } public Ratings ratings { get; set; }
public int qualityProfileId { get; set; } public int qualityProfileId { get; set; }

View file

@ -11,7 +11,6 @@ namespace Ombi.Api.Sonarr
{ {
public SonarrV3Api(IApi api) : base(api) public SonarrV3Api(IApi api) : base(api)
{ {
} }
protected override string ApiBaseUrl => "/api/v3/"; protected override string ApiBaseUrl => "/api/v3/";
@ -30,5 +29,22 @@ namespace Ombi.Api.Sonarr
request.AddHeader("X-Api-Key", apiKey); request.AddHeader("X-Api-Key", apiKey);
return await Api.Request<List<SonarrProfile>>(request); return await Api.Request<List<SonarrProfile>>(request);
} }
public Task<Tag> CreateTag(string apiKey, string baseUrl, string tagName)
{
var request = new Request($"{ApiBaseUrl}tag", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(new { Label = tagName });
return Api.Request<Tag>(request);
}
public Task<Tag> GetTag(int tagId, string apiKey, string baseUrl)
{
var request = new Request($"{ApiBaseUrl}tag/{tagId}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey);
return Api.Request<Tag>(request);
}
} }
} }

View file

@ -3,6 +3,7 @@
public class TesterResultModel public class TesterResultModel
{ {
public bool IsValid { get; set; } public bool IsValid { get; set; }
public string Version { get; set; }
public string ExpectedSubDir { get; set; } public string ExpectedSubDir { get; set; }
} }
} }

View file

@ -0,0 +1,10 @@
using Ombi.Api.Sonarr.Models;
using System.Collections.Generic;
namespace Ombi.Core.Senders
{
internal class SonarrSendOptions
{
public List<int> Tags { get; set; } = new List<int>();
}
}

View file

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
using Ombi.Api.DogNzb; using Ombi.Api.DogNzb;
using Ombi.Api.DogNzb.Models; using Ombi.Api.DogNzb.Models;
using Ombi.Api.SickRage; using Ombi.Api.SickRage;
@ -155,11 +157,13 @@ namespace Ombi.Core.Senders
{ {
return null; return null;
} }
var options = new SonarrSendOptions();
int qualityToUse; int qualityToUse;
var languageProfileId = s.LanguageProfile; var languageProfileId = s.LanguageProfile;
string rootFolderPath; string rootFolderPath;
string seriesType; string seriesType;
int? tagToUse = null;
var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId); var profiles = await UserQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == model.RequestedUserId);
@ -190,6 +194,7 @@ namespace Ombi.Core.Senders
} }
} }
seriesType = "anime"; seriesType = "anime";
tagToUse = s.AnimeTag;
} }
else else
{ {
@ -209,6 +214,7 @@ namespace Ombi.Core.Senders
} }
} }
seriesType = "standard"; seriesType = "standard";
tagToUse = s.Tag;
} }
// Overrides on the request take priority // Overrides on the request take priority
@ -240,6 +246,16 @@ namespace Ombi.Core.Senders
try try
{ {
if (tagToUse.HasValue)
{
options.Tags.Add(tagToUse.Value);
}
if (s.SendUserTags)
{
var userTag = await GetOrCreateTag(model, s);
options.Tags.Add(userTag.id);
}
// Does the series actually exist? // Does the series actually exist?
var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri); var allSeries = await SonarrApi.GetSeries(s.ApiKey, s.FullUri);
var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId); var existingSeries = allSeries.FirstOrDefault(x => x.tvdbId == model.ParentRequest.TvDbId);
@ -265,11 +281,11 @@ namespace Ombi.Core.Senders
ignoreEpisodesWithoutFiles = false, // We want all missing ignoreEpisodesWithoutFiles = false, // We want all missing
searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly. searchForMissingEpisodes = false // we want dont want to search yet. We want to make sure everything is unmonitored/monitored correctly.
}, },
languageProfileId = languageProfileId languageProfileId = languageProfileId,
tags = options.Tags
}; };
// Montitor the correct seasons, // Montitor the correct seasons,
// If we have that season in the model then it's monitored! // If we have that season in the model then it's monitored!
var seasonsToAdd = GetSeasonsToCreate(model); var seasonsToAdd = GetSeasonsToCreate(model);
@ -280,11 +296,11 @@ namespace Ombi.Core.Senders
throw new Exception(string.Join(',', result.ErrorMessages)); throw new Exception(string.Join(',', result.ErrorMessages));
} }
existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri);
await SendToSonarr(model, existingSeries, s); await SendToSonarr(model, existingSeries, s, options);
} }
else else
{ {
await SendToSonarr(model, existingSeries, s); await SendToSonarr(model, existingSeries, s, options);
} }
return new NewSeries return new NewSeries
@ -303,7 +319,30 @@ namespace Ombi.Core.Senders
} }
} }
private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s) private async Task<Tag> GetOrCreateTag(ChildRequests model, SonarrSettings s)
{
var tagName = model.RequestedUser.UserName;
// Does tag exist?
var allTags = await SonarrV3Api.GetTags(s.ApiKey, s.FullUri);
var existingTag = allTags.FirstOrDefault(x => x.label.Equals(tagName, StringComparison.InvariantCultureIgnoreCase));
existingTag ??= await SonarrV3Api.CreateTag(s.ApiKey, s.FullUri, tagName);
return existingTag;
}
private async Task<Tag> GetTag(int tagId, SonarrSettings s)
{
var tag = await SonarrV3Api.GetTag(tagId, s.ApiKey, s.FullUri);
if (tag == null)
{
Logger.LogError($"Tag ID {tagId} does not exist in sonarr. Please update the settings");
return null;
}
return tag;
}
private async Task SendToSonarr(ChildRequests model, SonarrSeries result, SonarrSettings s, SonarrSendOptions options)
{ {
// Check to ensure we have the all the seasons, ensure the Sonarr metadata has grabbed all the data // Check to ensure we have the all the seasons, ensure the Sonarr metadata has grabbed all the data
Season existingSeason = null; Season existingSeason = null;
@ -321,15 +360,27 @@ namespace Ombi.Core.Senders
} }
} }
var episodesToUpdate = new List<Episode>(); // Does the show have the correct tags we are expecting
// Ok, now let's sort out the episodes. if (options.Tags.Any())
{
result.tags ??= options.Tags;
var tagsToAdd = options.Tags.Except(result.tags);
if (tagsToAdd.Any())
{
result.tags.AddRange(tagsToAdd);
}
result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
}
if (model.SeriesType == SeriesType.Anime) if (model.SeriesType == SeriesType.Anime)
{ {
result.seriesType = "anime"; result.seriesType = "anime";
await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri); result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
} }
var episodesToUpdate = new List<Episode>();
// Ok, now let's sort out the episodes.
var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri); var sonarrEpisodes = await SonarrApi.GetEpisodes(result.id, s.ApiKey, s.FullUri);
var sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>(); var sonarrEpList = sonarrEpisodes.ToList() ?? new List<Episode>();
while (!sonarrEpList.Any()) while (!sonarrEpList.Any())

View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NewAlbums" xml:space="preserve">
<value>Nous Àlbums</value>
</data>
<data name="NewMovies" xml:space="preserve">
<value>Noves Pel·lícules</value>
</data>
<data name="NewTV" xml:space="preserve">
<value>Sèries Noves</value>
</data>
<data name="GenresLabel" xml:space="preserve">
<value>Gèneres:</value>
</data>
<data name="AlbumTypeLabel" xml:space="preserve">
<value>Tipus:</value>
</data>
<data name="SeasonLabel" xml:space="preserve">
<value>Temporada:</value>
</data>
<data name="EpisodesLabel" xml:space="preserve">
<value>Episodis:</value>
</data>
<data name="PoweredBy" xml:space="preserve">
<value>Desenvolupat per</value>
</data>
<data name="Unsubscribe" xml:space="preserve">
<value>Cancel·la la subscripció</value>
</data>
<data name="Album" xml:space="preserve">
<value>Àlbum</value>
</data>
<data name="Movie" xml:space="preserve">
<value>Pel·lícula</value>
</data>
<data name="TvShow" xml:space="preserve">
<value>Sèries de TV</value>
</data>
</root>

View file

@ -63,7 +63,7 @@ namespace Ombi.Notifications.Agents
// Get admin devices // Get admin devices
var playerIds = await GetPrivilegedUsersPlayerIds(); var playerIds = await GetPrivilegedUsersPlayerIds();
await Send(playerIds, notification, settings, model, true); await Send(playerIds, notification);
} }
protected override async Task NewIssue(NotificationOptions model, MobileNotificationSettings settings) protected override async Task NewIssue(NotificationOptions model, MobileNotificationSettings settings)
@ -83,7 +83,7 @@ namespace Ombi.Notifications.Agents
// Get admin devices // Get admin devices
var playerIds = await GetAdmins(); var playerIds = await GetAdmins();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task IssueComment(NotificationOptions model, MobileNotificationSettings settings) protected override async Task IssueComment(NotificationOptions model, MobileNotificationSettings settings)
@ -107,13 +107,13 @@ namespace Ombi.Notifications.Agents
model.Substitutes.TryGetValue("IssueId", out var issueId); model.Substitutes.TryGetValue("IssueId", out var issueId);
// Send to user // Send to user
var playerIds = await GetUsersForIssue(model, int.Parse(issueId), NotificationType.IssueComment); var playerIds = await GetUsersForIssue(model, int.Parse(issueId), NotificationType.IssueComment);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
else else
{ {
// Send to admin // Send to admin
var playerIds = await GetAdmins(); var playerIds = await GetAdmins();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
} }
} }
@ -136,7 +136,7 @@ namespace Ombi.Notifications.Agents
// Send to user // Send to user
var playerIds = await GetUsers(model, NotificationType.IssueResolved); var playerIds = await GetUsers(model, NotificationType.IssueResolved);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
@ -158,7 +158,7 @@ namespace Ombi.Notifications.Agents
// Get admin devices // Get admin devices
var playerIds = await GetAdmins(); var playerIds = await GetAdmins();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task RequestDeclined(NotificationOptions model, MobileNotificationSettings settings) protected override async Task RequestDeclined(NotificationOptions model, MobileNotificationSettings settings)
@ -179,7 +179,7 @@ namespace Ombi.Notifications.Agents
// Send to user // Send to user
var playerIds = await GetUsers(model, NotificationType.RequestDeclined); var playerIds = await GetUsers(model, NotificationType.RequestDeclined);
await AddSubscribedUsers(playerIds); await AddSubscribedUsers(playerIds);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task RequestApproved(NotificationOptions model, MobileNotificationSettings settings) protected override async Task RequestApproved(NotificationOptions model, MobileNotificationSettings settings)
@ -201,7 +201,7 @@ namespace Ombi.Notifications.Agents
var playerIds = await GetUsers(model, NotificationType.RequestApproved); var playerIds = await GetUsers(model, NotificationType.RequestApproved);
await AddSubscribedUsers(playerIds); await AddSubscribedUsers(playerIds);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
protected override async Task AvailableRequest(NotificationOptions model, MobileNotificationSettings settings) protected override async Task AvailableRequest(NotificationOptions model, MobileNotificationSettings settings)
@ -225,7 +225,7 @@ namespace Ombi.Notifications.Agents
var playerIds = await GetUsers(model, NotificationType.RequestAvailable); var playerIds = await GetUsers(model, NotificationType.RequestAvailable);
await AddSubscribedUsers(playerIds); await AddSubscribedUsers(playerIds);
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
private static Dictionary<string,string> GetNotificationData(NotificationMessageContent parsed, NotificationType type) private static Dictionary<string,string> GetNotificationData(NotificationMessageContent parsed, NotificationType type)
@ -240,7 +240,7 @@ namespace Ombi.Notifications.Agents
throw new NotImplementedException(); throw new NotImplementedException();
} }
protected async Task Send(List<string> playerIds, NotificationMessage model, MobileNotificationSettings settings, NotificationOptions requestModel, bool isAdminNotification = false) protected async Task Send(List<string> playerIds, NotificationMessage model)
{ {
if (playerIds == null || !playerIds.Any()) if (playerIds == null || !playerIds.Any())
{ {
@ -276,7 +276,7 @@ namespace Ombi.Notifications.Agents
} }
var playerIds = user.NotificationUserIds.Select(x => x.PlayerId).ToList(); var playerIds = user.NotificationUserIds.Select(x => x.PlayerId).ToList();
await Send(playerIds, notification, settings, model); await Send(playerIds, notification);
} }
private async Task<List<string>> GetAdmins() private async Task<List<string>> GetAdmins()
@ -382,13 +382,15 @@ namespace Ombi.Notifications.Agents
var notification = new NotificationMessage var notification = new NotificationMessage
{ {
Message = parsed.Message, Message = parsed.Message,
Subject = "New Request", Subject = "Request Partially Available",
Data = GetNotificationData(parsed, NotificationType.PartiallyAvailable) Data = GetNotificationData(parsed, NotificationType.PartiallyAvailable)
}; };
// Get admin devices
var playerIds = await GetAdmins(); var playerIds = await GetUsers(model, NotificationType.PartiallyAvailable);
await Send(playerIds, notification, settings, model, true);
await AddSubscribedUsers(playerIds);
await Send(playerIds, notification);
} }
} }
} }

View file

@ -176,7 +176,7 @@ namespace Ombi.Schedule.Tests
Series = new PlexServerContent Series = new PlexServerContent
{ {
TheMovieDbId = 33.ToString(), TheMovieDbId = 33.ToString(),
Title = "Test" Title = "abc"
}, },
EpisodeNumber = 1, EpisodeNumber = 1,
SeasonNumber = 2, SeasonNumber = 2,
@ -226,7 +226,7 @@ namespace Ombi.Schedule.Tests
{ {
Series = new PlexServerContent Series = new PlexServerContent
{ {
Title = "UNITTEST", Title = "UnitTest",
ImdbId = "invlaid", ImdbId = "invlaid",
}, },
EpisodeNumber = 1, EpisodeNumber = 1,

View file

@ -324,5 +324,84 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<OmbiUserManager>(x => x.UpdateAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "email" && x.UserName == "user")), Times.Once); _mocker.Verify<OmbiUserManager>(x => x.UpdateAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "email" && x.UserName == "user")), Times.Once);
} }
[Test]
public async Task Import_Cleanup_Missing_Plex_Users()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings
{
ImportPlexAdmin = true,
ImportPlexUsers = true,
DefaultRoles = new List<string>
{
OmbiRoles.RequestMovie
},
CleanupPlexUsers = true,
});
_mocker.Setup<IPlexApi, Task<PlexFriends>>(x => x.GetUsers(It.IsAny<string>())).ReturnsAsync(new PlexFriends
{
User = new UserFriends[]
{
}
});
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "email",
authentication_token = "user_token",
title = "user_title",
username = "user_username",
id = "user_id",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "user_username" && x.Email == "email" && x.ProviderUserId == "user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "user_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.DeleteAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "dupe" && x.UserName == "plex")), Times.Once);
}
[Test]
public async Task Import_Cleanup_Missing_Plex_Admin()
{
_mocker.Setup<ISettingsService<UserManagementSettings>, Task<UserManagementSettings>>(x => x.GetSettingsAsync())
.ReturnsAsync(new UserManagementSettings
{
ImportPlexAdmin = true,
ImportPlexUsers = false,
DefaultRoles = new List<string>
{
OmbiRoles.RequestMovie
},
CleanupPlexUsers = true,
});
_mocker.Setup<IPlexApi, Task<PlexAccount>>(x => x.GetAccount(It.IsAny<string>())).ReturnsAsync(new PlexAccount
{
user = new User
{
email = "diff_email",
authentication_token = "user_token",
title = "user_title",
username = "diff_username",
id = "diff_user_id",
}
});
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.CreateAsync(It.Is<OmbiUser>(x => x.UserName == "diff_username" && x.Email == "diff_email" && x.ProviderUserId == "diff_user_id" && x.UserType == UserType.PlexUser)))
.ReturnsAsync(IdentityResult.Success);
_mocker.Setup<OmbiUserManager, Task<IdentityResult>>(x => x.AddToRoleAsync(It.Is<OmbiUser>(x => x.UserName == "diff_username"), It.Is<string>(x => x == OmbiRoles.Admin)))
.ReturnsAsync(IdentityResult.Success);
await _subject.Execute(null);
_mocker.Verify<OmbiUserManager>(x => x.DeleteAsync(It.Is<OmbiUser>(x => x.ProviderUserId == "PLEX_ID" && x.Email == "dupe" && x.UserName == "plex")), Times.Once);
}
} }
} }

View file

@ -345,7 +345,6 @@ namespace Ombi.Schedule.Tests
[Test] [Test]
public async Task TvRequestFromWatchList_AlreadyRequested() public async Task TvRequestFromWatchList_AlreadyRequested()
{ {
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer _mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{ {
@ -540,5 +539,56 @@ namespace Ombi.Schedule.Tests
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once); _mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
} }
[Test]
public async Task TvRequestFromWatchList_RequestAllSeasons()
{
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true });
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
{
MediaContainer = new PlexWatchlist
{
Metadata = new List<Metadata>
{
new Metadata
{
type = "show",
ratingKey = "abc"
}
}
}
});
_mocker.Setup<IPlexApi, Task<PlexWatchlistMetadataContainer>>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new PlexWatchlistMetadataContainer
{
MediaContainer = new PlexWatchlistMetadata
{
Metadata = new WatchlistMetadata[]
{
new WatchlistMetadata
{
Guid = new List<PlexGuids>
{
new PlexGuids
{
Id = "tmdb://123"
}
}
}
}
}
});
_mocker.Setup<ITvRequestEngine, Task<RequestEngineResult>>(x => x.RequestTvShow(It.IsAny<TvRequestViewModelV2>()))
.ReturnsAsync(new RequestEngineResult { RequestId = 1 });
await _subject.Execute(_context.Object);
_mocker.Verify<ITvRequestEngine>(x => x.RequestTvShow(It.Is<TvRequestViewModelV2>(x => x.TheMovieDbId == 123 && x.LatestSeason == false && x.RequestAll == true)), Times.Once);
_mocker.Verify<IPlexApi>(x => x.GetWatchlistMetadata("abc", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_mocker.Verify<ITvRequestEngine>(x => x.SetUser(It.Is<OmbiUser>(x => x.Id == "abc")), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Once);
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Once);
}
} }
} }

View file

@ -101,7 +101,7 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
// Let's try and match the series by name // Let's try and match the series by name
seriesEpisodes = plexEpisodes.Where(x => seriesEpisodes = plexEpisodes.Where(x =>
x.Series.Title.Equals(child.Title, StringComparison.InvariantCultureIgnoreCase)); x.Series.Title == child.Title);
} }
await ProcessTvShow(seriesEpisodes, child); await ProcessTvShow(seriesEpisodes, child);

View file

@ -312,7 +312,7 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
break; break;
} }
if (quality.Equals(existing.Quality)) if (quality == null || quality.Equals(existing.Quality))
{ {
// We got it // We got it
continue; continue;

View file

@ -56,6 +56,8 @@ namespace Ombi.Schedule.Jobs.Plex
await _notification.SendNotificationToAdmins("Plex User Importer Started"); await _notification.SendNotificationToAdmins("Plex User Importer Started");
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.PlexUser).ToListAsync(); var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.PlexUser).ToListAsync();
List<OmbiUser> newOrUpdatedUsers = new List<OmbiUser>();
foreach (var server in settings.Servers) foreach (var server in settings.Servers)
{ {
if (string.IsNullOrEmpty(server.PlexAuthToken)) if (string.IsNullOrEmpty(server.PlexAuthToken))
@ -63,23 +65,46 @@ namespace Ombi.Schedule.Jobs.Plex
continue; continue;
} }
if (userManagementSettings.ImportPlexAdmin) if (userManagementSettings.ImportPlexAdmin)
{ {
await ImportAdmin(userManagementSettings, server, allUsers); OmbiUser newOrUpdatedAdmin = await ImportAdmin(userManagementSettings, server, allUsers);
if (newOrUpdatedAdmin != null)
{
newOrUpdatedUsers.Add(newOrUpdatedAdmin);
}
} }
if (userManagementSettings.ImportPlexUsers) if (userManagementSettings.ImportPlexUsers)
{ {
await ImportPlexUsers(userManagementSettings, allUsers, server); newOrUpdatedUsers.AddRange(await ImportPlexUsers(userManagementSettings, allUsers, server));
} }
} }
if (userManagementSettings.CleanupPlexUsers)
{
// Refresh users from updates
allUsers = await _userManager.Users.Where(x => x.UserType == UserType.PlexUser)
.ToListAsync();
var missingUsers = allUsers
.Where(x => !newOrUpdatedUsers.Contains(x));
foreach (var ombiUser in missingUsers)
{
_log.LogInformation("Deleting user {0} not found in Plex Server.", ombiUser.UserName);
await _userManager.DeleteAsync(ombiUser);
}
}
await _notification.SendNotificationToAdmins("Plex User Importer Finished"); await _notification.SendNotificationToAdmins("Plex User Importer Finished");
} }
private async Task ImportPlexUsers(UserManagementSettings userManagementSettings, List<OmbiUser> allUsers, PlexServers server) private async Task<List<OmbiUser>> ImportPlexUsers(UserManagementSettings userManagementSettings,
List<OmbiUser> allUsers, PlexServers server)
{ {
var users = await _api.GetUsers(server.PlexAuthToken); var users = await _api.GetUsers(server.PlexAuthToken);
List<OmbiUser> newOrUpdatedUsers = new List<OmbiUser>();
foreach (var plexUser in users.User) foreach (var plexUser in users.User)
{ {
// Check if we should import this user // Check if we should import this user
@ -129,19 +154,21 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
continue; continue;
} }
if (userManagementSettings.DefaultRoles.Any())
{
// Get the new user object to avoid any concurrency failures // Get the new user object to avoid any concurrency failures
var dbUser = var dbUser =
await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == newUser.UserName); await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == newUser.UserName);
if (userManagementSettings.DefaultRoles.Any())
{
foreach (var defaultRole in userManagementSettings.DefaultRoles) foreach (var defaultRole in userManagementSettings.DefaultRoles)
{ {
await _userManager.AddToRoleAsync(dbUser, defaultRole); await _userManager.AddToRoleAsync(dbUser, defaultRole);
} }
} }
newOrUpdatedUsers.Add(dbUser);
} }
else else
{ {
newOrUpdatedUsers.Add(existingPlexUser);
// Do we need to update this user? // Do we need to update this user?
existingPlexUser.Email = plexUser.Email; existingPlexUser.Email = plexUser.Email;
existingPlexUser.UserName = plexUser.Username; existingPlexUser.UserName = plexUser.Username;
@ -149,9 +176,12 @@ namespace Ombi.Schedule.Jobs.Plex
await _userManager.UpdateAsync(existingPlexUser); await _userManager.UpdateAsync(existingPlexUser);
} }
} }
return newOrUpdatedUsers;
} }
private async Task ImportAdmin(UserManagementSettings settings, PlexServers server, List<OmbiUser> allUsers) private async Task<OmbiUser> ImportAdmin(UserManagementSettings settings, PlexServers server,
List<OmbiUser> allUsers)
{ {
var plexAdmin = (await _api.GetAccount(server.PlexAuthToken)).user; var plexAdmin = (await _api.GetAccount(server.PlexAuthToken)).user;
@ -166,7 +196,7 @@ namespace Ombi.Schedule.Jobs.Plex
adminUserFromDb.UserName = plexAdmin.username; adminUserFromDb.UserName = plexAdmin.username;
adminUserFromDb.ProviderUserId = plexAdmin.id; adminUserFromDb.ProviderUserId = plexAdmin.id;
await _userManager.UpdateAsync(adminUserFromDb); await _userManager.UpdateAsync(adminUserFromDb);
return; return adminUserFromDb;
} }
// Ensure we don't have a user with the same username // Ensure we don't have a user with the same username
@ -174,7 +204,7 @@ namespace Ombi.Schedule.Jobs.Plex
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == normalUsername)) if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == normalUsername))
{ {
_log.LogWarning($"Cannot add user {plexAdmin.username} because their username is already in Ombi, skipping this user"); _log.LogWarning($"Cannot add user {plexAdmin.username} because their username is already in Ombi, skipping this user");
return; return null;
} }
var newUser = new OmbiUser var newUser = new OmbiUser
@ -190,11 +220,12 @@ namespace Ombi.Schedule.Jobs.Plex
var result = await _userManager.CreateAsync(newUser); var result = await _userManager.CreateAsync(newUser);
if (!LogResult(result)) if (!LogResult(result))
{ {
return; return null;
} }
var roleResult = await _userManager.AddToRoleAsync(newUser, OmbiRoles.Admin); var roleResult = await _userManager.AddToRoleAsync(newUser, OmbiRoles.Admin);
LogResult(roleResult); LogResult(roleResult);
return newUser;
} }
private bool LogResult(IdentityResult result) private bool LogResult(IdentityResult result)

View file

@ -125,7 +125,7 @@ namespace Ombi.Schedule.Jobs.Plex
switch (item.type) switch (item.type)
{ {
case "show": case "show":
await ProcessShow(int.Parse(providerIds.TheMovieDb), user); await ProcessShow(int.Parse(providerIds.TheMovieDb), user, settings.MonitorAll);
break; break;
case "movie": case "movie":
await ProcessMovie(int.Parse(providerIds.TheMovieDb), user); await ProcessMovie(int.Parse(providerIds.TheMovieDb), user);
@ -165,10 +165,16 @@ namespace Ombi.Schedule.Jobs.Plex
} }
} }
private async Task ProcessShow(int theMovieDbId, OmbiUser user) private async Task ProcessShow(int theMovieDbId, OmbiUser user, bool requestAll)
{ {
_tvRequestEngine.SetUser(user); _tvRequestEngine.SetUser(user);
var response = await _tvRequestEngine.RequestTvShow(new TvRequestViewModelV2 { LatestSeason = true, TheMovieDbId = theMovieDbId, Source = RequestSource.PlexWatchlist }); var requestModel = new TvRequestViewModelV2 { LatestSeason = true, TheMovieDbId = theMovieDbId, Source = RequestSource.PlexWatchlist };
if (requestAll)
{
requestModel.RequestAll = true;
requestModel.LatestSeason = false;
}
var response = await _tvRequestEngine.RequestTvShow(requestModel);
if (response.IsError) if (response.IsError)
{ {
if (response.ErrorCode == ErrorCode.AlreadyRequested) if (response.ErrorCode == ErrorCode.AlreadyRequested)

View file

@ -40,6 +40,8 @@ namespace Ombi.Schedule.Processor
private UpdateModel TransformUpdate(Release release) private UpdateModel TransformUpdate(Release release)
{ {
Version updateVersion = Version.Parse(release.Version.TrimStart('v'));
Version currentVersion = Version.Parse(AssemblyHelper.GetRuntimeVersion());
var newUpdate = new UpdateModel var newUpdate = new UpdateModel
{ {
UpdateVersionString = release.Version, UpdateVersionString = release.Version,
@ -47,7 +49,7 @@ namespace Ombi.Schedule.Processor
UpdateDate = DateTime.Now, UpdateDate = DateTime.Now,
ChangeLogs = release.Description, ChangeLogs = release.Description,
Downloads = new List<Downloads>(), Downloads = new List<Downloads>(),
UpdateAvailable = release.Version != "v" + AssemblyHelper.GetRuntimeVersion() UpdateAvailable = updateVersion > currentVersion
}; };
foreach (var dl in release.Downloads) foreach (var dl in release.Downloads)

View file

@ -8,6 +8,7 @@ namespace Ombi.Core.Settings.Models.External
{ {
public bool Enable { get; set; } public bool Enable { get; set; }
public bool EnableWatchlistImport { get; set; } public bool EnableWatchlistImport { get; set; }
public bool MonitorAll { get; set; }
/// <summary> /// <summary>
/// This is the ClientId for OAuth /// This is the ClientId for OAuth
/// </summary> /// </summary>

View file

@ -17,9 +17,13 @@
public string QualityProfileAnime { get; set; } public string QualityProfileAnime { get; set; }
public string RootPathAnime { get; set; } public string RootPathAnime { get; set; }
public int? AnimeTag { get; set; }
public int? Tag { get; set; }
public bool AddOnly { get; set; } public bool AddOnly { get; set; }
public int LanguageProfile { get; set; } public int LanguageProfile { get; set; }
public int LanguageProfileAnime { get; set; } public int LanguageProfileAnime { get; set; }
public bool ScanForAvailability { get; set; } public bool ScanForAvailability { get; set; }
public bool SendUserTags { get; set; }
} }
} }

View file

@ -7,6 +7,7 @@ namespace Ombi.Settings.Settings.Models
{ {
public bool ImportPlexAdmin { get; set; } public bool ImportPlexAdmin { get; set; }
public bool ImportPlexUsers { get; set; } public bool ImportPlexUsers { get; set; }
public bool CleanupPlexUsers { get; set; }
public bool ImportEmbyUsers { get; set; } public bool ImportEmbyUsers { get; set; }
public bool ImportJellyfinUsers { get; set; } public bool ImportJellyfinUsers { get; set; }
public int MovieRequestLimit { get; set; } public int MovieRequestLimit { get; set; }

View file

@ -23,7 +23,8 @@
"availability-rules", "availability-rules",
"details", "details",
"requests", "requests",
"sonarr" "sonarr",
"plex"
], ],
"rpc.enabled": true "rpc.enabled": true
} }

View file

@ -8,7 +8,7 @@ import { IDiscoverCardResult } from "../../interfaces";
import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2"; import { ISearchMovieResultV2 } from "../../../interfaces/ISearchMovieResultV2";
import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../../interfaces/ISearchTvResultV2";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
import { RequestType } from "../../../interfaces"; import { IMovieRequestModel, RequestType } from "../../../interfaces";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
@Component({ @Component({
@ -131,44 +131,74 @@ export class DiscoverCardComponent implements OnInit {
this.loading = true; this.loading = true;
switch (this.result.type) { switch (this.result.type) {
case RequestType.tvShow: case RequestType.tvShow:
const dia = this.dialog.open(EpisodeRequestComponent, { width: "700px", data: { series: this.tvSearchResult, isAdmin: this.isAdmin }, panelClass: 'modal-panel' }); const dialog = this.dialog.open(EpisodeRequestComponent, {
dia.afterClosed().subscribe(x => this.loading = false); width: "700px",
return; data: { series: this.tvSearchResult, isAdmin: this.isAdmin },
panelClass: "modal-panel",
});
dialog.afterClosed().subscribe(() => (this.loading = false));
break;
case RequestType.movie: case RequestType.movie:
if (this.isAdmin) { const movieRequest: IMovieRequestModel = {
const dialog = this.dialog.open(AdminRequestDialogComponent, { width: "700px", data: { type: RequestType.movie, id: this.result.id, }, panelClass: 'modal-panel' }); theMovieDbId: +this.result.id,
dialog.afterClosed().subscribe((result) => {
if (result) {
this.requestService.requestMovie({ theMovieDbId: +this.result.id,
languageCode: this.translate.currentLang, languageCode: this.translate.currentLang,
qualityPathOverride: result.radarrPathId, requestOnBehalf: null,
requestOnBehalf: result.username?.id, qualityPathOverride: null,
rootFolderOverride: result.radarrFolderId, rootFolderOverride: null,
is4KRequest: is4k }).subscribe(x => { is4KRequest: is4k,
if (x.result) { };
this.result.requested = true;
this.messageService.send(this.translate.instant("Requests.RequestAddedSuccessfully", { title: this.result.title }), "Ok"); if (!this.isAdmin) {
} else { this.requestMovie(movieRequest);
this.messageService.sendRequestEngineResultError(x); break;
} }
});
} const adminRequestDialog = this.dialog.open(
}); AdminRequestDialogComponent,
} else { {
this.requestService.requestMovie({ theMovieDbId: +this.result.id, languageCode: this.translate.currentLang, requestOnBehalf: null, qualityPathOverride: null, rootFolderOverride: null, is4KRequest: is4k }).subscribe(x => { width: "700px",
if (x.result) { data: { type: RequestType.movie, id: this.result.id },
this.result.requested = true; panelClass: "modal-panel",
this.messageService.send(this.translate.instant("Requests.RequestAddedSuccessfully", { title: this.result.title }), "Ok");
} else {
this.messageService.sendRequestEngineResultError(x);
} }
);
adminRequestDialog.afterClosed().subscribe((result) => {
if (!result) {
this.loading = false; this.loading = false;
});
return; return;
} }
movieRequest.requestOnBehalf = result.username?.id;
movieRequest.qualityPathOverride = result.radarrPathId;
movieRequest.rootFolderOverride = result.radarrFolderId;
this.requestMovie(movieRequest);
});
break;
} }
} }
private requestMovie(movieRequest: IMovieRequestModel) {
this.requestService.requestMovie(movieRequest).subscribe({
next: (response) => {
if (response.result) {
this.result.requested = true;
const message = this.translate.instant(
"Requests.RequestAddedSuccessfully",
{ title: this.result.title }
);
this.messageService.send(message, "Ok");
} else {
this.messageService.sendRequestEngineResultError(response);
}
this.loading = false;
},
error: (error) => {
this.messageService.sendRequestEngineResultError(error);
this.loading = false;
},
});
}
public onImageError(event: any) { public onImageError(event: any) {
const originalSrc = event.target.src; const originalSrc = event.target.src;

View file

@ -16,7 +16,7 @@
</div> </div>
<div *ngIf="discoverResults" class="row full-height"> <div *ngIf="discoverResults" class="row full-height">
<div class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults"> <div class="col-xl-2 col-lg-3 col-md-3 col-6 col-sm-4 small-padding" *ngFor="let result of discoverResults">
<discover-card [isAdmin]="isAdmins" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card> <discover-card [isAdmin]="isAdmin" [result]="result" [is4kEnabled]="is4kEnabled"></discover-card>
</div> </div>
</div> </div>
</div> </div>

View file

@ -113,6 +113,7 @@ export interface IPublicInfo {
export interface IPlexSettings extends ISettings { export interface IPlexSettings extends ISettings {
enable: boolean; enable: boolean;
enableWatchlistImport: boolean; enableWatchlistImport: boolean;
monitorAll: boolean;
servers: IPlexServer[]; servers: IPlexServer[];
} }
@ -145,6 +146,9 @@ export interface ISonarrSettings extends IExternalSettings {
languageProfile: number; languageProfile: number;
languageProfileAnime: number; languageProfileAnime: number;
scanForAvailability: boolean; scanForAvailability: boolean;
sendUserTags: boolean;
tag: number | null;
animeTag: number | null;
} }
export interface IRadarrSettings extends IExternalSettings { export interface IRadarrSettings extends IExternalSettings {
@ -252,6 +256,7 @@ export interface ICustomPage extends ISettings {
export interface IUserManagementSettings extends ISettings { export interface IUserManagementSettings extends ISettings {
importPlexUsers: boolean; importPlexUsers: boolean;
cleanupPlexUsers: boolean;
importPlexAdmin: boolean; importPlexAdmin: boolean;
importEmbyUsers: boolean; importEmbyUsers: boolean;
importJellyfinUsers: boolean; importJellyfinUsers: boolean;

View file

@ -12,3 +12,8 @@ export interface ILanguageProfiles {
name: string; name: string;
id: number; id: number;
} }
export interface ITag {
label: string;
id: number;
}

View file

@ -1,4 +1,5 @@
export interface ITesterResult { export interface ITesterResult {
isValid: boolean; isValid: boolean;
version?: string;
expectedSubDir?: string; expectedSubDir?: string;
} }

View file

@ -57,11 +57,11 @@
<i class="far fa-play-circle fa-2x"></i> <i class="far fa-play-circle fa-2x"></i>
</a> </a>
</ng-container> </ng-container>
<button *ngIf="!tv.fullyAvailable && !allEpisodesRequested()" mat-raised-button id="requestBtn" class="btn-spacing" color="primary" <button *ngIf="(!tv.fullyAvailable || (tv.fullyAvailable && tv.partlyAvailable)) && !allEpisodesRequestedOrAvailable()" mat-raised-button id="requestBtn" class="btn-spacing" color="primary"
(click)="request()"><i class="fas fa-plus"></i> (click)="request()"><i class="fas fa-plus"></i>
{{ 'Common.Request' | translate }}</button> {{ 'Common.Request' | translate }}</button>
<button *ngIf="!tv.denied && allEpisodesRequested()" mat-raised-button class="btn-spacing" color="warn" [disabled]> <button *ngIf="!tv.denied && allEpisodesRequestedOrAvailable()" mat-raised-button class="btn-spacing" color="warn" [disabled]>
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
{{ 'Common.Requested' | translate }}</button> {{ 'Common.Requested' | translate }}</button>

View file

@ -126,9 +126,10 @@ export class TvDetailsComponent implements OnInit {
} }
} }
public allEpisodesRequested(): boolean { public allEpisodesRequestedOrAvailable(): boolean {
return this.tv.seasonRequests.every(e => e.episodes.every(x => x.approved || x.requested)); return this.tv.seasonRequests.every(e => e.episodes.every(x => x.available || x.approved || x.requested));
} }
private checkPoster() { private checkPoster() {
if (this.tv.images.original == null) { if (this.tv.images.original == null) {
this.tv.images.original = "../../../images/default_movie_poster.png"; this.tv.images.original = "../../../images/default_movie_poster.png";

View file

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { ISonarrSettings } from "../../interfaces"; import { ISonarrSettings, ITag } from "../../interfaces";
import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder } from "../../interfaces"; import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
@ -36,6 +36,10 @@ export class SonarrService extends ServiceHelpers {
return this.http.get<ILanguageProfiles[]>(`${this.url}/v3/languageprofiles/`, {headers: this.headers}); return this.http.get<ILanguageProfiles[]>(`${this.url}/v3/languageprofiles/`, {headers: this.headers});
} }
public getTags(settings: ISonarrSettings): Observable<ITag[]> {
return this.http.post<ITag[]>(`${this.url}/tags/`, JSON.stringify(settings), {headers: this.headers});
}
public isEnabled(): Promise<boolean> { public isEnabled(): Promise<boolean> {
return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers }).toPromise(); return this.http.get<boolean>(`${this.url}/enabled/`, { headers: this.headers }).toPromise();
} }

View file

@ -10,7 +10,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-6 col-sm-6"> <div class="col-md-6 col-6 col-sm-6">
<div class="md-form-field"> <div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable <mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable
</mat-slide-toggle> </mat-slide-toggle>
</div> </div> </div> </div>
</div> </div>

View file

@ -72,6 +72,7 @@ export class EmbyComponent implements OnInit {
if (index > -1) { if (index > -1) {
this.settings.servers.splice(index, 1); this.settings.servers.splice(index, 1);
this.selected.setValue(this.settings.servers.length - 1); this.selected.setValue(this.settings.servers.length - 1);
this.toggle();
} }
} }

View file

@ -10,7 +10,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-6 col-sm-6"> <div class="col-md-6 col-6 col-sm-6">
<div class="md-form-field"> <div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable <mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>

View file

@ -73,6 +73,7 @@ export class JellyfinComponent implements OnInit {
if (index > -1) { if (index > -1) {
this.settings.servers.splice(index, 1); this.settings.servers.splice(index, 1);
this.selected.setValue(this.settings.servers.length - 1); this.selected.setValue(this.settings.servers.length - 1);
this.toggle();
} }
} }

View file

@ -2,6 +2,11 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
@Component({ @Component({
selector: "settings-plex-form-field", selector: "settings-plex-form-field",
styles: [`
.margin {
margin: 10px;
}
`],
template: ` template: `
<div class="row"> <div class="row">
<div class="col-2 align-self-center"> <div class="col-2 align-self-center">
@ -16,7 +21,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
<input matInput placeholder={{placeholder}} [attr.type]="type" id="{{id}}" name="{{id}}" [ngModel]="value" (ngModelChange)="change($event)" value="{{value}}"> <input matInput placeholder={{placeholder}} [attr.type]="type" id="{{id}}" name="{{id}}" [ngModel]="value" (ngModelChange)="change($event)" value="{{value}}">
</mat-form-field> </mat-form-field>
<mat-slide-toggle *ngIf="type === 'checkbox'" id="{{id}}" [ngModel]="value" (ngModelChange)="change($event)" [checked]="value"></mat-slide-toggle> <mat-slide-toggle class="margin" *ngIf="type === 'checkbox'" id="{{id}}" [ngModel]="value" (ngModelChange)="change($event)" [checked]="value"></mat-slide-toggle>
<ng-content select="[below]"></ng-content> <ng-content select="[below]"></ng-content>
</div> </div>

View file

@ -10,7 +10,7 @@
</div> </div>
<settings-plex-form-field [label]="'Enable'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enable"></settings-plex-form-field> <settings-plex-form-field [label]="'Enable'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enable"></settings-plex-form-field>
<settings-plex-form-field [label]="'Enable User Watchlist Requests'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enableWatchlistImport"> <settings-plex-form-field [label]="'Enable User Watchlist Requests'" [type]="'checkbox'" [id]="'enableWatchlistImport'" [(value)]="settings.enableWatchlistImport">
<small bottom>When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled. This <b>only</b> applies to users that are logging in with their Plex Account <small bottom>When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled. This <b>only</b> applies to users that are logging in with their Plex Account
<br>Request limits if set are all still applied <br>Request limits if set are all still applied
</small> </small>

View file

@ -16,6 +16,10 @@
<div class="md-form-field"> <div class="md-form-field">
<mat-slide-toggle formControlName="scanForAvailability">Scan for Availability</mat-slide-toggle> <mat-slide-toggle formControlName="scanForAvailability">Scan for Availability</mat-slide-toggle>
</div> </div>
<div class="md-form-field">
<mat-slide-toggle formControlName="sendUserTags" id="sendUserTags">Add the user as a tag</mat-slide-toggle>
<small><br>This will add the username of the requesting user as a tag in Sonarr. If the tag doesn't exist, Ombi will create it.</small>
</div>
<div class="md-form-field" style="margin-top:1em;"></div> <div class="md-form-field" style="margin-top:1em;"></div>
</div> </div>
</div> </div>
@ -53,14 +57,17 @@
</div> </div>
</div> </div>
<div class="col-md-5 col-4 col-sm-12"> <div class="col-md-5 col-4 col-sm-12">
<label for="username" class="control-label"><h3>Sonarr Interface</h3></label>
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<div id="profiles"> <div class="row">
<div class="md-form-field" style="display:inline;"> <div class="col-md-12">
<button mat-raised-button id="profiles" (click)="getProfiles(form)" class="mat-stroked-button"> <button mat-raised-button id="profiles" type="button" (click)="getProfiles(form)" class="mat-stroked-button">
Load Qualities <span *ngIf="profilesRunning" class="fas fa-spinner fa-spin"></span></button> Load Qualities <span *ngIf="profilesRunning" class="fas fa-spinner fa-spin"></span></button>
<div class="md-form-field" style="margin-top:1em;"></div> <div class="md-form-field" style="margin-top:1em;"></div>
</div> </div>
</div>
<div class="row">
<div id="profiles" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Quality Profiles</mat-label> <mat-label>Quality Profiles</mat-label>
@ -72,7 +79,7 @@
</div> </div>
</div> </div>
<div id="qualityProfileAnime"> <div id="qualityProfileAnime" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Quality Profiles (Anime)</mat-label> <mat-label>Quality Profiles (Anime)</mat-label>
@ -84,14 +91,18 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<div id="rootFolders"> <div class="row">
<div class="md-form-field" style="display:inline"> <div class="col-md-12">
<button mat-raised-button id="rootFolder" (click)="getRootFolders(form)" class="mat-stroked-button"> <button mat-raised-button id="rootFolder" type="button" (click)="getRootFolders(form)" class="mat-stroked-button">
Load Folders <span *ngIf="rootFoldersRunning" class="fas fa-spinner fa-spin"></span></button><div class="md-form-field" style="margin-top:1em;"></div> Load Folders <span *ngIf="rootFoldersRunning" class="fas fa-spinner fa-spin"></span></button><div class="md-form-field" style="margin-top:1em;"></div>
</div> </div>
</div>
<div class="row">
<div id="rootFolders" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Default Root Folders</mat-label> <mat-label>Default Root Folders</mat-label>
@ -103,7 +114,7 @@
</div> </div>
</div> </div>
<div id="rootFoldersAnime"> <div id="rootFoldersAnime" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Default Root Folders (Anime)</mat-label> <mat-label>Default Root Folders (Anime)</mat-label>
@ -114,21 +125,58 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
</div></div>
</div>
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<div class="row">
<div class="col-md-12">
<button mat-raised-button id="rootFolder" type="button" (click)="getTags(form)" class="mat-stroked-button">
Load Tags <span *ngIf="tagsRunning" class="fas fa-spinner fa-spin"></span></button><div class="md-form-field" style="margin-top:1em;"></div>
</div>
</div>
<div class="row">
<div id="tag" class="col-md-6">
<div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline">
<mat-label>Default Tag</mat-label>
<mat-select formControlName="tag">
<mat-option *ngFor="let tag of tags" [value]="tag.id">{{tag.label}} </mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div id="animeTag" class="col-md-6">
<div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline">
<mat-label>Anime Tags</mat-label>
<mat-select formControlName="animeTag">
<mat-option *ngFor="let tag of animeTags" [value]="tag.id">{{tag.label}} </mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div></div>
<div class="form-group col-md-12" *ngIf="sonarrVersion === '3'">
<label for="select" class="control-label">Language Profiles <label for="select" class="control-label">Language Profiles
<i *ngIf="form.get('languageProfile').hasError('required')" class="fas fa-exclamation-circle error-text" pTooltip="A Language Profile is required"></i> <i *ngIf="form.get('languageProfile').hasError('required')" class="fas fa-exclamation-circle error-text" pTooltip="A Language Profile is required"></i>
</label> </label>
<div id="langaugeProfile">
<div class="md-form-field" style="display:inline"> <div class="md-form-field" style="display:inline">
<div class="row">
<div class="col-md-12">
<button type="button" mat-raised-button (click)="getLanguageProfiles(form)" class="mat-stroked-button">Load <button type="button" mat-raised-button (click)="getLanguageProfiles(form)" class="mat-stroked-button">Load
Languages <span *ngIf="langRunning" class="fas fa-spinner fa-spin"> </span></button><div class="md-form-field" style="margin-top:1em;"></div> Languages <span *ngIf="langRunning" class="fas fa-spinner fa-spin"> </span></button><div class="md-form-field" style="margin-top:1em;"></div>
</div> </div>
</div>
</div>
<div class="row">
<div id="langaugeProfile" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Language Profiles </mat-label> <mat-label>Language Profiles</mat-label>
<mat-select formControlName="languageProfile"> <mat-select formControlName="languageProfile">
<mat-option *ngFor="let lang of languageProfiles" [value]="lang.id">{{lang.name}}</mat-option> <mat-option *ngFor="let lang of languageProfiles" [value]="lang.id">{{lang.name}}</mat-option>
</mat-select> </mat-select>
@ -137,16 +185,17 @@
</div> </div>
</div> </div>
<div id="langaugeProfileAnime"> <div id="langaugeProfileAnime" class="col-md-6">
<div class="md-form-field" style="display:contents;"> <div class="md-form-field" style="display:contents;">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Language Profiles Anime</mat-label> <mat-label>Anime</mat-label>
<mat-select formControlName="languageProfileAnime"> <mat-select formControlName="languageProfileAnime">
<mat-option *ngFor="let lang of languageProfiles" [value]="lang.id">{{lang.name}}</mat-option> <mat-option *ngFor="let lang of languageProfiles" [value]="lang.id">{{lang.name}}</mat-option>
</mat-select> </mat-select>
<mat-error>A Language Profile Anime is required</mat-error> <mat-error>A Language Profile Anime is required</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
</div>
</div> </div>
</div> </div>
@ -166,11 +215,6 @@
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group col-md-7">
<div>
<button mat-raised-button type="submit" class="mat-stroked-button accent mat-accent">Submit</button>
</div>
</div>
<div class="form-group col-md-7"> <div class="form-group col-md-7">
<div> <div>
@ -178,6 +222,12 @@
<span id="spinner"> </span></button> <span id="spinner"> </span></button>
</div> </div>
</div> </div>
<div class="form-group col-md-7">
<div>
<button mat-raised-button type="submit" class="mat-stroked-button accent mat-accent">Submit</button>
</div>
</div>
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,7 +1,8 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { UntypedFormBuilder, FormControl, UntypedFormGroup, Validators } from "@angular/forms"; import { UntypedFormBuilder, FormControl, UntypedFormGroup, Validators } from "@angular/forms";
import { finalize, map } from "rxjs";
import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder } from "../../interfaces"; import { ILanguageProfiles, ISonarrProfile, ISonarrRootFolder, ITag } from "../../interfaces";
import { ISonarrSettings } from "../../interfaces"; import { ISonarrSettings } from "../../interfaces";
import { SonarrService } from "../../services"; import { SonarrService } from "../../services";
@ -21,14 +22,20 @@ export class SonarrComponent implements OnInit {
public rootFoldersAnime: ISonarrRootFolder[]; public rootFoldersAnime: ISonarrRootFolder[];
public languageProfiles: ILanguageProfiles[]; public languageProfiles: ILanguageProfiles[];
public languageProfilesAnime: ILanguageProfiles[]; public languageProfilesAnime: ILanguageProfiles[];
public tags: ITag[];
public animeTags: ITag[];
public selectedRootFolder: ISonarrRootFolder; public selectedRootFolder: ISonarrRootFolder;
public selectedQuality: ISonarrProfile; public selectedQuality: ISonarrProfile;
public selectedLanguageProfiles: ILanguageProfiles; public selectedLanguageProfiles: ILanguageProfiles;
public profilesRunning: boolean; public profilesRunning: boolean;
public rootFoldersRunning: boolean; public rootFoldersRunning: boolean;
public tagsRunning: boolean;
public langRunning: boolean; public langRunning: boolean;
public form: UntypedFormGroup; public form: UntypedFormGroup;
public advanced = false; public advanced = false;
public sonarrVersion: string;
formErrors: any; formErrors: any;
constructor(private settingsService: SettingsService, constructor(private settingsService: SettingsService,
@ -72,11 +79,29 @@ export class SonarrComponent implements OnInit {
port: [x.port, [Validators.required]], port: [x.port, [Validators.required]],
addOnly: [x.addOnly], addOnly: [x.addOnly],
seasonFolders: [x.seasonFolders], seasonFolders: [x.seasonFolders],
languageProfile: [x.languageProfile, [Validators.required, validateProfile]], languageProfile: [x.languageProfile],
languageProfileAnime: [x.languageProfileAnime], languageProfileAnime: [x.languageProfileAnime],
scanForAvailability: [x.scanForAvailability], scanForAvailability: [x.scanForAvailability],
sendUserTags: [x.sendUserTags],
tag: [x.tag],
animeTag: [x.animeTag]
}); });
this.rootFolders = [];
this.qualities = [];
this.languageProfiles = [];
this.tags = [];
this.animeTags = [];
if (x.enabled && this.form.valid) {
this.testerService.sonarrTest(x).subscribe(result => {
this.sonarrVersion = result.version[0];
if (this.sonarrVersion === '3') {
this.form.controls.languageProfile.addValidators([Validators.required, validateProfile]);
}
});
}
if (x.qualityProfile) { if (x.qualityProfile) {
this.getProfiles(this.form); this.getProfiles(this.form);
} }
@ -86,6 +111,9 @@ export class SonarrComponent implements OnInit {
if (x.languageProfile) { if (x.languageProfile) {
this.getLanguageProfiles(this.form); this.getLanguageProfiles(this.form);
} }
if (x.tag || x.animeTag) {
this.getTags(this.form);
}
this.formErrors ={ this.formErrors ={
apiKey: {}, apiKey: {},
@ -96,12 +124,12 @@ export class SonarrComponent implements OnInit {
}; };
this.onFormValuesChanged(); this.onFormValuesChanged();
}); });
this.rootFolders = [];
this.qualities = [];
this.languageProfiles = [];
this.rootFolders.push({ path: "Please Select", id: -1 }); this.rootFolders.push({ path: "Please Select", id: -1 });
this.qualities.push({ name: "Please Select", id: -1 }); this.qualities.push({ name: "Please Select", id: -1 });
this.languageProfiles.push({ name: "Please Select", id: -1 }); this.languageProfiles.push({ name: "Please Select", id: -1 });
this.animeTags.push({label: "None", id: -1});
this.tags.push({label: "None", id: -1});
} }
public getProfiles(form: UntypedFormGroup) { public getProfiles(form: UntypedFormGroup) {
@ -141,14 +169,27 @@ export class SonarrComponent implements OnInit {
}); });
} }
public test(form: UntypedFormGroup) { public getTags(form: UntypedFormGroup) {
if (form.invalid) { this.tagsRunning = true;
this.notificationService.error("Please check your entered values"); this.sonarrService.getTags(form.value).pipe(
return; finalize(() => {
this.tagsRunning = false;
this.animeTags.unshift({ label: "None", id: -1 });
this.tags.unshift({ label: "None", id: -1 });
this.notificationService.success("Successfully retrieved the Tags");
}),
map(result => {
this.tags = result;
this.tags.forEach(val => this.animeTags.push(Object.assign({}, val)));
})
).subscribe()
} }
public test(form: UntypedFormGroup) {
const settings = <ISonarrSettings> form.value; const settings = <ISonarrSettings> form.value;
this.testerService.sonarrTest(settings).subscribe(result => { this.testerService.sonarrTest(settings).subscribe(result => {
if (result.isValid) { if (result.isValid) {
this.sonarrVersion = result.version[0];
this.notificationService.success("Successfully connected to Sonarr!"); this.notificationService.success("Successfully connected to Sonarr!");
} else if (result.expectedSubDir) { } else if (result.expectedSubDir) {
this.notificationService.error("Your Sonarr Base URL must be set to " + result.expectedSubDir); this.notificationService.error("Your Sonarr Base URL must be set to " + result.expectedSubDir);
@ -178,6 +219,12 @@ export class SonarrComponent implements OnInit {
this.notificationService.error("Please check your entered values"); this.notificationService.error("Please check your entered values");
} }
} }
if (form.controls.animeTag.value == -1) {
form.controls.animeTag.setValue(null);
}
if (form.controls.tag.value == -1) {
form.controls.tag.setValue(null);
}
this.settingsService.saveSonarr(form.value) this.settingsService.saveSonarr(form.value)
.subscribe(x => { .subscribe(x => {
@ -189,6 +236,7 @@ export class SonarrComponent implements OnInit {
}); });
} }
} }
function validateProfile(qualityProfile): { [key: string]:boolean } | null { function validateProfile(qualityProfile): { [key: string]:boolean } | null {
if (qualityProfile.value !== undefined && (isNaN(qualityProfile.value) || qualityProfile.value == -1)) { if (qualityProfile.value !== undefined && (isNaN(qualityProfile.value) || qualityProfile.value == -1)) {

View file

@ -17,6 +17,11 @@
<div class="form-group"> <div class="form-group">
<mat-slide-toggle id="importAdmin" [(ngModel)]="settings.importPlexAdmin">Import Plex Admin</mat-slide-toggle> <mat-slide-toggle id="importAdmin" [(ngModel)]="settings.importPlexAdmin">Import Plex Admin</mat-slide-toggle>
</div> </div>
<div *ngIf="settings.importPlexUsers || settings.importPlexAdmin">
<mat-slide-toggle id="cleanupPlexUsers" [(ngModel)]="settings.cleanupPlexUsers">
Cleanup Plex Users</mat-slide-toggle>
</div>
<div *ngIf="plexUsers"> <div *ngIf="plexUsers">
<p>Plex Users excluded from Import</p> <p>Plex Users excluded from Import</p>

View file

@ -65,11 +65,9 @@ export class AdminRequestDialogComponent implements OnInit {
if (this.data.type === RequestType.tvShow) { if (this.data.type === RequestType.tvShow) {
this.sonarrEnabled = await this.sonarrService.isEnabled(); this.sonarrEnabled = await this.sonarrService.isEnabled();
if (this.sonarrEnabled) { if (this.sonarrEnabled) {
this.settingsService.getSonarr().subscribe((settings: ISonarrSettings) => { this.sonarrService.getV3LanguageProfilesWithoutSettings().subscribe((profiles: ILanguageProfiles[]) => {
this.sonarrService.getV3LanguageProfiles(settings).subscribe((profiles: ILanguageProfiles[]) => {
this.sonarrLanguageProfiles = profiles; this.sonarrLanguageProfiles = profiles;
}) })
});
this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => { this.sonarrService.getQualityProfilesWithoutSettings().subscribe(c => {
this.sonarrProfiles = c; this.sonarrProfiles = c;
}); });

View file

@ -46,7 +46,7 @@ namespace Ombi.Controllers.V1.External
/// </summary> /// </summary>
public TesterController(INotificationService service, IDiscordNotification notification, IEmailNotification emailN, public TesterController(INotificationService service, IDiscordNotification notification, IEmailNotification emailN,
IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm,
IPlexApi plex, IEmbyApiFactory emby, IRadarrV3Api radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider, IPlexApi plex, IEmbyApiFactory emby, IRadarrV3Api radarr, ISonarrV3Api sonarr, ILogger<TesterController> log, IEmailProvider provider,
ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, ILegacyMobileNotification mobileNotification, ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, ILegacyMobileNotification mobileNotification,
ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um, IWebhookNotification webhookNotification, ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um, IWebhookNotification webhookNotification,
IJellyfinApi jellyfinApi, IPrincipal user) IJellyfinApi jellyfinApi, IPrincipal user)
@ -90,7 +90,7 @@ namespace Ombi.Controllers.V1.External
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private IRadarrV3Api RadarrApi { get; } private IRadarrV3Api RadarrApi { get; }
private IEmbyApiFactory EmbyApi { get; } private IEmbyApiFactory EmbyApi { get; }
private ISonarrApi SonarrApi { get; } private ISonarrV3Api SonarrApi { get; }
private ICouchPotatoApi CouchPotatoApi { get; } private ICouchPotatoApi CouchPotatoApi { get; }
private ILogger<TesterController> Log { get; } private ILogger<TesterController> Log { get; }
private IEmailProvider EmailProvider { get; } private IEmailProvider EmailProvider { get; }
@ -415,6 +415,7 @@ namespace Ombi.Controllers.V1.External
return new TesterResultModel return new TesterResultModel
{ {
IsValid = result.urlBase == settings.SubDir || string.IsNullOrEmpty(result.urlBase) && string.IsNullOrEmpty(settings.SubDir), IsValid = result.urlBase == settings.SubDir || string.IsNullOrEmpty(result.urlBase) && string.IsNullOrEmpty(settings.SubDir),
Version = result.version,
ExpectedSubDir = result.urlBase ExpectedSubDir = result.urlBase
}; };
} }

View file

@ -4,7 +4,7 @@
"LogLevel": { "LogLevel": {
"Default": "Warning", "Default": "Warning",
"System": "Warning", "System": "Warning",
"Microsoft": "Warning", "Microsoft": "Error",
"Hangfire": "None", "Hangfire": "None",
"System.Net.Http.HttpClient.health-checks": "Warning", "System.Net.Http.HttpClient.health-checks": "Warning",
"HealthChecks": "Warning" "HealthChecks": "Warning"

View file

@ -0,0 +1,459 @@
{
"Login": {
"SignInButton": "Inicia sessió",
"UsernamePlaceholder": "Nom d'usuari",
"PasswordPlaceholder": "Contrasenya",
"RememberMe": "Recorda'm",
"SignInWith": "Inicia la sessió amb {{appName}}",
"SignInWithPlex": "Inicia la sessió amb Plex",
"ForgottenPassword": "Has oblidat la contrasenya?",
"Errors": {
"IncorrectCredentials": "Nom d'usuari o contrasenya incorrecta"
}
},
"Common": {
"ContinueButton": "Continua",
"Available": "Disponible",
"Available4K": "Disponible en 4K",
"Approved": "Aprovat",
"Approve4K": "Aprovat en 4K",
"Pending": "Pendent",
"PartiallyAvailable": "Parcialment disponible",
"Monitored": "En seguiment",
"NotAvailable": "No disponible",
"ProcessingRequest": "Processant sol·licitud",
"ProcessingRequest4K": "Processant sol·licitud en 4K",
"PendingApproval": "Pendent d'aprovació",
"PendingApproval4K": "Pendent d'aprovació en 4K",
"RequestDenied": "Sol·licitud denegada",
"RequestDenied4K": "Sol·licitud en 4K denegada",
"NotRequested": "No sol·licitat",
"NotRequested4K": "No sol·licitat en 4K",
"Requested": "Sol·licitada",
"Requested4K": "Sol·licitada en 4K",
"Search": "Cerca",
"Request": "Sol·licitud",
"Request4K": "Sol·licitud en 4K",
"Denied": "Denegat",
"Approve": "Aprova",
"PartlyAvailable": "Parcialment disponible",
"ViewDetails": "Visualitza els detalls",
"Errors": {
"Validation": "Comproveu les dades inserides"
},
"Cancel": "Cancel·la",
"Submit": "Envia",
"Update": "Actualitza",
"tvShow": "Sèries de TV",
"movie": "Pel·lícula",
"album": "Àlbum"
},
"PasswordReset": {
"EmailAddressPlaceholder": "Adreça electrònica",
"ResetPasswordButton": "Restableix la contrasenya"
},
"LandingPage": {
"OnlineHeading": "En línia",
"OnlineParagraph": "El servidor està fora de línia",
"PartiallyOnlineHeading": "Parcialment en línia",
"PartiallyOnlineParagraph": "El servidor de mitjans està parcialment en línia.",
"MultipleServersUnavailable": "Hi ha {{serversUnavailable}} de {{totalServers}} servidors fora de línia.",
"SingleServerUnavailable": "Hi ha {{serversUnavailable}} de {{totalServers}} servidors fora de línia.",
"OfflineHeading": "Fora de línia",
"OfflineParagraph": "El servidor de mitjans està fora de línia.",
"CheckPageForUpdates": "Consulteu aquesta pàgina per obtenir actualitzacions contínues del lloc."
},
"ErrorPages": {
"NotFound": "No s'ha trobat la pàgina",
"SomethingWentWrong": "Alguna cosa no ha anat bé!"
},
"NavigationBar": {
"Discover": "Descobriu",
"Search": "Cerca",
"Requests": "Sol·licituds",
"UserManagement": "Usuaris",
"Issues": "Incidències",
"Vote": "Vota",
"Donate": "Feu un donatiu!",
"DonateLibraryMaintainer": "Feu un donatiu al desenvolupador de la biblioteca",
"DonateTooltip": "Així és com convenço a la meva dona para a què em deixi passar el meu temps lliure desenvolupant Ombi ;)",
"UpdateAvailableTooltip": "Actualització disponible!",
"Settings": "Configuració",
"Welcome": "Benvingut/da, {{username}}",
"UpdateDetails": "Edita les dades d'usuari",
"Logout": "Tanca sessió",
"OpenMobileApp": "Obre l'aplicació mòbil",
"RecentlyAdded": "Afegit recentment",
"ChangeTheme": "Canvia el tema",
"Calendar": "Calendari",
"UserPreferences": "Configuració",
"FeatureSuggestion": "Suggeriu noves característiques",
"FeatureSuggestionTooltip": "Tens una gran idea nova? Suggereix-ho aquí!",
"Filter": {
"Movies": "Pel·lícules",
"TvShows": "Sèries de TV",
"Music": "Música",
"People": "Persones"
},
"MorningWelcome": "Bon dia!",
"AfternoonWelcome": "Bon vespre!",
"EveningWelcome": "Bona nit!"
},
"Search": {
"Title": "Cerca",
"Paragraph": "Voleu veure alguna cosa que no està disponible actualment? Cap problema, només cal que ho cerqueu i ho sol·liciteu!",
"MoviesTab": "Pel·lícules",
"TvTab": "Sèries de TV",
"MusicTab": "Música",
"AdvancedSearch": "Podeu emplenar qualsevol dels següents apartats per descobrir nous mitjans. Tots els resultats estan ordenats per popularitat",
"AdvancedSearchHeader": "Cerca avançada",
"Suggestions": "Suggeriments",
"NoResults": "Ho sent, no s'han trobat resultats!",
"DigitalDate": "Versió digital: {{date}}",
"TheatricalRelease": "En cines: {{date}}",
"ViewOnPlex": "Reproduïu a Plex",
"ViewOnEmby": "Reproduïu a Emby",
"ViewOnJellyfin": "Reproduïu a Jellyfin",
"RequestAdded": "La sol·licitud per {{title}} s'ha afegit amb èxit",
"Similar": "Semblant",
"Refine": "Filtres",
"SearchBarPlaceholder": "Premeu aquí per cercar",
"Movies": {
"PopularMovies": "Pel·lícules populars",
"UpcomingMovies": "Properes pel·lícules",
"TopRatedMovies": "Pel·lícules millor valorades",
"NowPlayingMovies": "Pel·lícules en cartellera",
"HomePage": "Pàgina d'inici",
"Trailer": "Tràiler"
},
"TvShows": {
"Popular": "Popular",
"Trending": "Tendències",
"MostWatched": "Més vistes",
"MostAnticipated": "Més esperades",
"Results": "Resultats",
"AirDate": "Data d'emissió:",
"AllSeasons": "Totes les temporades",
"FirstSeason": "Primera temporada",
"LatestSeason": "Última temporada",
"Select": "Selecciona...",
"SubmitRequest": "Envieu una sol·licitud",
"Season": "Temporada {{seasonNumber}}",
"SelectAllInSeason": "Selecciona-ho tot en la temporada {{seasonNumber}}"
},
"AdvancedSearchInstructions": "Si us plau, escolliu el tipus de mitjà que esteu cercant:",
"YearOfRelease": "Any de llançament",
"SearchGenre": "Cerca per gènere",
"SearchKeyword": "Cerca per paraules clau",
"SearchProvider": "Proveïdor de cerca",
"KeywordSearchingDisclaimer": "Tingueu en compte que la cerca de paraules clau és poc confiable a causa de les dades inconsistents a TheMovieDb"
},
"Requests": {
"Title": "Sol·licituds",
"Paragraph": "A continuació podeu veure la vostra i totes les altres sol·licituds, així com el seu estat de descàrrega i aprovació.",
"MoviesTab": "Pel·lícules",
"ArtistName": "Artista",
"AlbumName": "Nom de l'àlbum",
"TvTab": "Sèries de TV",
"MusicTab": "Música",
"RequestedBy": "Sol·licitat per",
"Status": "Estat",
"RequestStatus": "Estat de la sol·licitud",
"Denied": " Denegat:",
"TheatricalRelease": "En cines: {{date}}",
"ReleaseDate": "Llançament: {{date}}",
"TheatricalReleaseSort": "En cines",
"DigitalRelease": "Versió digital: {{date}}",
"RequestDate": "Data de la sol·licitud",
"QualityOverride": "Sobreescriure qualitat:",
"RootFolderOverride": "Sobreescriure carpeta arrel:",
"ChangeRootFolder": "Carpeta arrel",
"ChangeQualityProfile": "Perfil de qualitat",
"MarkUnavailable": "Marca com a disponible",
"MarkUnavailable4K": "Marca com a no disponible en 4K",
"MarkAvailable": "Marca com a disponible",
"MarkAvailable4K": "Marca com a disponible en 4K",
"Remove": "Suprimeix",
"Deny": "Denega-ho",
"Deny4K": "Denega-ho en 4K",
"Has4KRequest": "Té sol·licitud en 4K",
"DenyReason": "Raó de denegació",
"DeniedReason": "Motiu de denegació",
"Season": "Temporada",
"GridTitle": "Títol",
"AirDate": "Data d'emissió",
"GridStatus": "Estat",
"ReportIssue": "Notifiqueu una incidència",
"Filter": "Filtra",
"Sort": "Ordena",
"SeasonNumberHeading": "Temporada: {seasonNumber}",
"SortTitleAsc": "Títol ▲",
"SortTitleDesc": "Títol ▼",
"SortRequestDateAsc": "Data de sol·licitud ▲",
"SortRequestDateDesc": "Data de sol·licitud ▼",
"SortStatusAsc": "Estat ▲",
"SortStatusDesc": "Estat ▼",
"Remaining": {
"Quota": "{{remaining}}/{{total}} sol·licituds restants",
"NextDays": "S'afegirà una altra sol·licitud d'aquí a {{time}} dies",
"NextHours": "S'afegirà una altra sol·licitud d'aquí a {{time}} hores",
"NextMinutes": "S'afegirà una altra sol·licitud d'aquí a {{time}} minuts",
"NextMinute": "S'afegirà una altra sol·licitud d'aquí a {{time}} minut"
},
"AllRequests": "Totes les sol·licituds",
"PendingRequests": "Sol·licituds pendents",
"ProcessingRequests": "Processant sol·licituds",
"AvailableRequests": "Sol·licituds disponibles",
"DeniedRequests": "Sol·licituds denegades",
"RequestsToDisplay": "Sol·licituds a mostrar",
"RequestsTitle": "Títol",
"Details": "Detalls",
"Options": "Opcions",
"RequestPanel": {
"Delete": "Suprimeix la sol·licitud",
"Approve": "Aprova la sol·licitud",
"Deny": "Denega la sol·licitud",
"Approve4K": "Aprova la sol·licitud en 4K",
"Deny4K": "Denega la sol·licitud en 4K",
"ChangeAvailability": "Marca com a disponible",
"Deleted": "Els elements seleccionats s'han suprimit correctament",
"Approved": "Els elements seleccionats s'han aprovat correctament",
"Denied": "Els elements seleccionats s'han denegat correctament"
},
"SuccessfullyApproved": "Aprovat correctament",
"SuccessfullyDeleted": "La sol·licitud s'ha suprimit correctament",
"NowAvailable": "La sol·licitud ja està disponible",
"NowUnavailable": "La sol·licitud ja no està disponible",
"SuccessfullyReprocessed": "La sol·licitud s'ha tornat a processar correctament",
"DeniedRequest": "Sol·licitud denegada",
"RequestCollection": "Sol·licita col·lecció",
"CollectionSuccesfullyAdded": "La col·lecció {{name}} s'ha afegit correctament!",
"NeedToSelectEpisodes": "Heu de seleccionar algun episodi!",
"RequestAddedSuccessfully": "La sol·licitud de {{title}} s'ha afegit correctament",
"ErrorCodes": {
"AlreadyRequested": "Ja s'ha sol·licitat",
"EpisodesAlreadyRequested": "Ja hi ha capítols sol·licitats d'aquesta sèrie",
"NoPermissionsOnBehalf": "No teniu els permisos correctes per fer sol·licituds en nom d'altres usuaris!",
"NoPermissions": "No teniu els permisos correctes!",
"RequestDoesNotExist": "La sol·licitud no existeix",
"ChildRequestDoesNotExist": "La sol·licitud no existeix",
"NoPermissionsRequestMovie": "No teniu permisos per sol·licitar una pel·lícula",
"NoPermissionsRequestTV": "No teniu permisos per sol·licitar una sèrie de televisió",
"NoPermissionsRequestAlbum": "No teniu permisos per sol·licitar un àlbum",
"MovieRequestQuotaExceeded": "Heu superat la vostra quota de sol·licitud de pel·lícules!",
"TvRequestQuotaExceeded": "Heu superat la vostra quota de sol·licitud d'episodis!",
"AlbumRequestQuotaExceeded": "Heu superat la vostra quota de sol·licitud d'àlbums!"
},
"Notify": "Notifica'm",
"RemoveNotification": "Elimina les notificacions",
"SuccessfulNotify": "Ara se us notificarà pel títol {{title}}",
"SuccessfulUnNotify": "Ja no se us notificarà pel títol {{title}}",
"CouldntNotify": "No s'ha pogut notificar pel títol {{title}}"
},
"Issues": {
"Title": "Incidències",
"IssuesForTitle": "Incidència per {{title}}",
"PendingTitle": "Incidències pendents",
"InProgressTitle": "Incidències en procés",
"ResolvedTitle": "Incidències resoltes",
"ColumnTitle": "Títol",
"Count": "Recompte",
"Category": "Categoria",
"Status": "Estat",
"Details": "Detalls",
"Description": "Descripció",
"NoComments": "Sense comentaris!",
"MarkInProgress": "Marca en progrés",
"MarkResolved": "Marca com resolt",
"SendMessageButton": "Envia",
"Subject": "Assumpte",
"Comments": "Comentaris",
"WriteMessagePlaceholder": "Escriviu aquí el missatge...",
"ReportedBy": "Notificat per",
"IssueDialog": {
"Title": "Notifiqueu una incidència",
"DescriptionPlaceholder": "Si us plau, descriviu la incidència",
"TitlePlaceholder": "Títol curt de la incidència",
"SelectCategory": "Selecciona categoria",
"IssueCreated": "S'ha creat la incidència"
},
"Outstanding": "Hi ha incidències pendents",
"ResolvedDate": "Data de resolució",
"CreatedDate": "Creat",
"MarkedAsResolved": "Aquesta incidència ha sigut marcada com resolta!",
"MarkedAsInProgress": "Aquesta incidència ha sigut marcada com en procés!",
"Delete": "Esborra la incidència",
"DeletedIssue": "S'ha esborrat la incidència",
"Chat": "Xat",
"EnterYourMessage": "Introduïu el vostre missatge",
"Requested": "Sol·licitat",
"UserOnDate": "{{user}} a {{date}}"
},
"Filter": {
"ClearFilter": "Neteja el filtre",
"FilterHeaderAvailability": "Disponibilitat",
"FilterHeaderRequestStatus": "Estat",
"Approved": "Aprovat",
"PendingApproval": "Pendent d'aprovació",
"WatchProviders": "Proveïdors de contingut",
"Keywords": "Etiquetes"
},
"UserManagment": {
"TvRemaining": "TV: {{remaining}}/{{total}} restants",
"MovieRemaining": "Pel·lícules: {{remaining}}/{{total}} restants",
"MusicRemaining": "Música: {{remaining}}/{{total}} restants",
"TvDue": "TV: {{date}}",
"MovieDue": "Pel·lícula: {{date}}",
"MusicDue": "Música: {{date}}"
},
"Votes": {
"CompletedVotesTab": "Votat",
"VotesTab": "Vots necessaris"
},
"MediaDetails": {
"Denied": "Denegat",
"Denied4K": "Denegat en 4K",
"Trailers": "Tràilers",
"RecommendationsTitle": "Recomanacions",
"SimilarTitle": "Semblant",
"VideosTitle": "Vídeos",
"AlbumsTitle": "Àlbums",
"RequestAllAlbums": "Sol·licita tots els àlbums",
"ClearSelection": "Esborra selecció",
"RequestSelectedAlbums": "Sol·licita àlbums seleccionats",
"ViewCollection": "Mostra col·lecció",
"NotEnoughInfo": "Malauradament, encara no hi ha prou informació sobre aquesta sèrie!",
"AdvancedOptions": "Opcions avançades",
"AutoApproveOptions": "Podeu configurar la sol·licitud aquí, un cop sol·licitada, s'enviarà a la vostra aplicació DVR i s'aprovarà automàticament! Tingueu en compte que això és opcional, només cal que premeu \"Sol·licita\" per ometre'l!",
"AutoApproveOptionsTv": "Podeu configurar la sol·licitud aquí, un cop sol·licitada, s'enviarà a la vostra aplicació DVR i s'aprovarà automàticament! Si la sol·licitud ja està a Sonarr, no canviarà la carpeta arrel ni el perfil de qualitat configurat! Tingueu en compte que això és opcional, només cal que premeu \"Sol·licita\" per ometre'l!",
"AutoApproveOptionsTvShort": "Podeu configurar la sol·licitud aquí, un cop sol·licitada, s'enviarà a la vostra aplicació DVR! Si la sol·licitud ja està a Sonarr, no canviarà la carpeta arrel ni el perfil de qualitat configurat! Tingueu en compte que això és opcional, només cal que premeu \"Sol·licita\" per ometre'l!",
"QualityProfilesSelect": "Seleccioneu un perfil de qualitat",
"RootFolderSelect": "Seleccioneu una carpeta arrel",
"LanguageProfileSelect": "Seleccioneu un perfil d'idioma",
"Status": "Estat:",
"StatusValues": {
"Rumored": "Es rumoreja",
"Planned": "Planificat",
"In Production": "En producció",
"Post Production": "En postproducció",
"Released": "En cines",
"Running": "En emissió",
"Returning Series": "Sèrie que torna a emetre's",
"Ended": "Finalitzada",
"Canceled": "Cancel·lada"
},
"Seasons": "Temporades:",
"Episodes": "Episodis:",
"Availability": "Disponibilitat:",
"RequestStatus": "Estat de la sol·licitud:",
"Quality": "Qualitat:",
"RootFolderOverride": "Sobreescriure carpeta arrel:",
"QualityOverride": "Sobreescriure qualitat:",
"Network": "Plataforma:",
"GenresLabel": "Gèneres:",
"Genres": "Gèneres",
"FirstAired": "Primera emissió:",
"TheatricalRelease": "Llançament:",
"DigitalRelease": "Estrena digital:",
"Votes": "Vots:",
"Runtime": "Durada:",
"Minutes": "{{runtime}} minuts",
"Revenue": "Ingressos:",
"Budget": "Pressupost:",
"Keywords": "Paraules clau/Etiquetes:",
"Casts": {
"CastTitle": "Repartiment"
},
"Crews": {
"CrewTitle": "Equip"
},
"EpisodeSelector": {
"AllSeasonsTooltip": "Això demanarà cada temporada per a aquesta sèrie",
"FirstSeasonTooltip": "Això sols demanarà la primera temporada per a aquesta sèrie",
"LatestSeasonTooltip": "Això sols demanarà l'última temporada per a aquesta sèrie",
"NoEpisodes": "Malauradament, encara no hi ha dades d'episodi per a aquesta sèrie!",
"SeasonNumber": "Temporada {{number}}"
},
"SonarrConfiguration": "Configuració de Sonarr",
"RadarrConfiguration": "Configuració de Radarr",
"RequestOnBehalf": "Sol·liciteu en nom de",
"PleaseSelectUser": "Si us plau, seleccioneu un usuari",
"StreamingOn": "Emès en:",
"RequestedBy": "Sol·licitat per:",
"OnDate": "A:",
"RequestedByOn": "Sol·licitat per {{user}} el {{date}}",
"RequestDate": "Data de sol·licitud:",
"DeniedReason": "Motiu de denegació:",
"ReProcessRequest": "Torna a processar la sol·licitud",
"ReProcessRequest4K": "Torna a processar la sol·licitud en 4K",
"Music": {
"Type": "Tipus:",
"Country": "País:",
"StartDate": "Data d'inici:",
"EndDate": "Data de finalització:"
},
"RequestSource": "Font:"
},
"Discovery": {
"PopularTab": "Popular",
"TrendingTab": "Tendències",
"UpcomingTab": "Pròximament",
"SeasonalTab": "Per temporada",
"RecentlyRequestedTab": "Sol·licituds recents",
"Movies": "Pel·lícules",
"Combined": "Combinat",
"Tv": "TV",
"CardDetails": {
"Availability": "Disponibilitat",
"Studio": "Estudi",
"Network": "Plataforma",
"UnknownNetwork": "Desconegut",
"RequestStatus": "Estat de la sol·licitud",
"Director": "Direcció",
"InCinemas": "En cines",
"FirstAired": "Emès per primera vegada",
"Writer": "Guionistes",
"ExecProducer": "Producció executiva"
},
"NoSearch": "Ho sentim, no hi ha coincidències a la cerca!"
},
"UserPreferences": {
"Welcome": "Benvingut/da {{username}}!",
"OmbiLanguage": "Idioma",
"DarkMode": "Mode fosc",
"Updated": "Sha actualitzat correctament",
"StreamingCountry": "País d'emissió",
"StreamingCountryDescription": "Aquest és el codi de país per al qual mostrarem la informació de transmissió. Si sou a Espanya, seleccioneu ES i obtindreu informació de l'emissió relacionada amb Espanya.",
"LanguageDescription": "Aquest és l'idioma en que es mostrarà la interfície d'Ombi.",
"MobileQRCode": "Codi QR mòbil",
"LegacyApp": "Inicia l'aplicació antiga",
"NoQrCode": "Poseu-vos en contacte amb el vostre administrador per activar els codis QR",
"UserType": "Tipus d'usuari:",
"ChangeDetails": "Modifica les teves dades",
"NeedCurrentPassword": "És necessari la teva contrasenya actual per a confirmar els canvis",
"CurrentPassword": "Contrasenya actual",
"EmailAddress": "Adreça de correu",
"NewPassword": "Nova contrasenya",
"NewPasswordConfirm": "Confirmeu la nova contrasenya",
"Security": "Seguretat",
"Profile": "Perfil",
"UpdatedYourInformation": "S'ha actualitzat la vostra informació",
"Unsubscribed": "La subscripció s'ha cancel·lat!"
},
"UserTypeLabel": {
"1": "Usuari local",
"2": "Usuari de Plex",
"3": "Usuari d'Emby",
"4": "Usuari connectat d'Emby",
"5": "Usuari de Jellyfin"
},
"Paginator": {
"itemsPerPageLabel": "Elements per pàgina:",
"nextPageLabel": "Pàgina següent",
"previousPageLabel": "Pàgina anterior",
"firstPageLabel": "Primera pàgina",
"lastPageLabel": "Última pàgina",
"rangePageLabel1": "0 de {{length}}",
"rangePageLabel2": "{{startIndex}} {{endIndex}} de {{length}}"
}
}

View file

@ -14,21 +14,21 @@
"Common": { "Common": {
"ContinueButton": "Doorgaan", "ContinueButton": "Doorgaan",
"Available": "Beschikbaar", "Available": "Beschikbaar",
"Available4K": "Available 4K", "Available4K": "Beschikbaar",
"Approved": "Goedgekeurd", "Approved": "Goedgekeurd",
"Approve4K": "Approve 4K", "Approve4K": "Goedgekeurd 4K",
"Pending": "In afwachting", "Pending": "In afwachting",
"PartiallyAvailable": "Deels Beschikbaar", "PartiallyAvailable": "Deels Beschikbaar",
"Monitored": "Gemonitord", "Monitored": "Gemonitord",
"NotAvailable": "Niet Beschikbaar", "NotAvailable": "Niet Beschikbaar",
"ProcessingRequest": "Verzoek wordt verwerkt", "ProcessingRequest": "Verzoek wordt verwerkt",
"ProcessingRequest4K": "Processing Request 4K", "ProcessingRequest4K": "Verzoeken in behandeling 4K",
"PendingApproval": "Wacht op goedkeuring", "PendingApproval": "Wacht op goedkeuring",
"PendingApproval4K": "Pending Approval 4K", "PendingApproval4K": "Wacht op goedkeuring 4K",
"RequestDenied": "Verzoek geweigerd", "RequestDenied": "Verzoek geweigerd",
"RequestDenied4K": "Request Denied 4K", "RequestDenied4K": "Verzoek geweigerd 4K",
"NotRequested": "Niet verzocht", "NotRequested": "Niet verzocht",
"NotRequested4K": "Not Requested 4K", "NotRequested4K": "Niet aangevraagd 4K",
"Requested": "Aangevraagd", "Requested": "Aangevraagd",
"Requested4K": "Requested 4K", "Requested4K": "Requested 4K",
"Search": "Zoeken", "Search": "Zoeken",

View file

@ -365,7 +365,7 @@
"CastTitle": "演员" "CastTitle": "演员"
}, },
"Crews": { "Crews": {
"CrewTitle": "Crew" "CrewTitle": "工作人员"
}, },
"EpisodeSelector": { "EpisodeSelector": {
"AllSeasonsTooltip": "请求这个节目的每一季", "AllSeasonsTooltip": "请求这个节目的每一季",

View file

@ -1,3 +1,3 @@
{ {
"version": "4.27.8" "version": "4.31.0"
} }