diff --git a/.gitignore b/.gitignore index a57df90cb..143b2fc13 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ *.userosscache *.sln.docstates -# User-specific files (MonoDevelop/Xamarin Studio) +# User-specific files *.userprefs # Build results @@ -252,4 +252,4 @@ _Pvt_Extensions /src/Ombi/healthchecksdb /src/Ombi/ClientApp/package-lock.json /src/Ombi.Core/Properties/launchSettings.json -.yarn \ No newline at end of file +.yarn diff --git a/CHANGELOG.md b/CHANGELOG.md index ddec12b1c..cba57e3ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,309 @@ +## [4.41.1](https://github.com/Ombi-app/Ombi/compare/v4.41.0...v4.41.1) (2023-05-27) + + + +# [4.41.0](https://github.com/Ombi-app/Ombi/compare/v4.40.0...v4.41.0) (2023-05-27) + + +### Bug Fixes + +* Fix various styling issues ([#4935](https://github.com/Ombi-app/Ombi/issues/4935)) ([90b934a](https://github.com/Ombi-app/Ombi/commit/90b934a36996c0f489096f3641350a1c0d3b7c89)) + + +### Features + +* **emby:** Show end-user external IP address to Emby when logging in as an Emby user ([#4949](https://github.com/Ombi-app/Ombi/issues/4949)) ([79cef7e](https://github.com/Ombi-app/Ombi/commit/79cef7e0f8643e36536a9ea84dd1a07c232403a9)), closes [#4947](https://github.com/Ombi-app/Ombi/issues/4947) + + + +# [4.40.0](https://github.com/Ombi-app/Ombi/compare/v4.39.1...v4.40.0) (2023-05-18) + + + +# [4.39.0](https://github.com/Ombi-app/Ombi/compare/v4.35.11...v4.39.0) (2023-05-17) + + +### Bug Fixes + +* **emby:** Fix Emby played sync running a full sync during recently added sync ([#4932](https://github.com/Ombi-app/Ombi/issues/4932)) ([9424586](https://github.com/Ombi-app/Ombi/commit/9424586e9c1b622b6475aeb8ee3cf4a8f346da6e)) + + +### Features + +* Hide watched status when request is not available ([#4934](https://github.com/Ombi-app/Ombi/issues/4934)) ([82c7f1c](https://github.com/Ombi-app/Ombi/commit/82c7f1c44fd7c87d57cc2b0c34a10fcda7628f4e)) + + + +## [4.38.2](https://github.com/Ombi-app/Ombi/compare/v4.38.1...v4.38.2) (2023-05-17) + + + +## [4.38.1](https://github.com/Ombi-app/Ombi/compare/v4.38.0...v4.38.1) (2023-05-09) + + +### Bug Fixes + +* **API:** Allow RequestOnBehalf rights if requested from the API ([#4919](https://github.com/Ombi-app/Ombi/issues/4919)) ([bb6dedd](https://github.com/Ombi-app/Ombi/commit/bb6deddfaecb3d6c7c3c6970414444b619bb9106)) +* **notificaitons:** Add the RequestedByAlias field to the Notification Message ([7e9c8be](https://github.com/Ombi-app/Ombi/commit/7e9c8bec6b02bb4e11f8db50394e493d4dd07723)) + + + +# [4.38.0](https://github.com/Ombi-app/Ombi/compare/v4.37.3...v4.38.0) (2023-05-07) + + +### Bug Fixes + +* remove sort header ([969bc7b](https://github.com/Ombi-app/Ombi/commit/969bc7bb25ea900ab9199509b079b36843e5bd6f)) + + +### Features + +* **emby:** Show watched status for Movie requests ([9cfb10b](https://github.com/Ombi-app/Ombi/commit/9cfb10bb1ee69067a6f47bd2c8a72d4e6834350e)) + + + +## [4.37.3](https://github.com/Ombi-app/Ombi/compare/v4.37.2...v4.37.3) (2023-05-07) + + +### Bug Fixes + +* Show the ApiAlias in the requests-list ([9ff624c](https://github.com/Ombi-app/Ombi/commit/9ff624ce4646815b239fbb8327117947f0a90e4b)) + + + +## [4.37.2](https://github.com/Ombi-app/Ombi/compare/v4.37.1...v4.37.2) (2023-05-03) + + +### Bug Fixes + +* **jellyfin:** Fixed an issue where the sync could stop working. Removed unused properties so the deseralization no longer fails ([0e5e0ad](https://github.com/Ombi-app/Ombi/commit/0e5e0adf862701d0f672beff14ec0aa75e4b5220)) + + + +## [4.37.1](https://github.com/Ombi-app/Ombi/compare/v4.37.0...v4.37.1) (2023-05-02) + + +### Bug Fixes + +* Cron Validation ([#4842](https://github.com/Ombi-app/Ombi/issues/4842)) ([97cc42f](https://github.com/Ombi-app/Ombi/commit/97cc42ffa8672e7d0d0996b5fbda7f7fe699da2d)) +* **discover:** :children_crossing: Improved the new Genre buttons, it now includes TV results ([b087d60](https://github.com/Ombi-app/Ombi/commit/b087d606ff36565208e564f8856903f2a4098db5)) +* **lidarr:** Change monitor to Existing to properly add artist [#3597](https://github.com/Ombi-app/Ombi/issues/3597) ([506f607](https://github.com/Ombi-app/Ombi/commit/506f60773bf1031d0be51ccd34289b855a04ea40)), closes [/github.com/Lidarr/Lidarr/issues/3597#issuecomment-1530804055](https://github.com//github.com/Lidarr/Lidarr/issues/3597/issues/issuecomment-1530804055) + + + +# [4.37.0](https://github.com/Ombi-app/Ombi/compare/v4.36.1...v4.37.0) (2023-04-24) + + +### Features + +* Search by genre ([1837419](https://github.com/Ombi-app/Ombi/commit/18374198f9f2462ba85c5781b0fcc05892728b21)) + + + +## [4.36.1](https://github.com/Ombi-app/Ombi/compare/v4.36.0...v4.36.1) (2023-04-20) + + +### Bug Fixes + +* **healthchecks:** Removed redundant ping check ([1751305](https://github.com/Ombi-app/Ombi/commit/1751305064176d2c0135f867773ccc46b03915ec)) + + + +# [4.36.0](https://github.com/Ombi-app/Ombi/compare/v4.35.19...v4.36.0) (2023-04-20) + + +### Features + +* **discover:** Add deny option to recently requested ([#4907](https://github.com/Ombi-app/Ombi/issues/4907)) ([78f340e](https://github.com/Ombi-app/Ombi/commit/78f340ee5f309c55690497170897533801957668)) + + + +## [4.35.19](https://github.com/Ombi-app/Ombi/compare/v4.35.18...v4.35.19) (2023-04-20) + + +### Bug Fixes + +* **radarr:** Fixed an issue where the radarr sync would break ([de4baad](https://github.com/Ombi-app/Ombi/commit/de4baade9f87248d77106ff1a313a498870f4fb3)) + + + +## [4.35.18](https://github.com/Ombi-app/Ombi/compare/v4.35.17...v4.35.18) (2023-04-15) + + +### Bug Fixes + +* **#4906:** :bug: Fixed an issue with power users and permissions ([80884bc](https://github.com/Ombi-app/Ombi/commit/80884bcd725c329867c278ad235cd4096cd4fe7a)) + + + +## [4.35.17](https://github.com/Ombi-app/Ombi/compare/v4.35.16...v4.35.17) (2023-04-15) + + +### Bug Fixes + +* **discover:** Fix denied requests displayed as approved ([#4901](https://github.com/Ombi-app/Ombi/issues/4901)) ([1e87f20](https://github.com/Ombi-app/Ombi/commit/1e87f2010491b0f3fdda70d2b19d9afd94438df7)) +* Fix denied movie shown as 'processing request' in details view ([#4900](https://github.com/Ombi-app/Ombi/issues/4900)) ([0069bfd](https://github.com/Ombi-app/Ombi/commit/0069bfdf54e0785bad45c832ca052f19fd4b940b)) + + + +## [4.35.16](https://github.com/Ombi-app/Ombi/compare/v4.35.15...v4.35.16) (2023-04-13) + + +### Bug Fixes + +* Support duplicates in Emby/JF collections ([#4902](https://github.com/Ombi-app/Ombi/issues/4902)) ([141f96d](https://github.com/Ombi-app/Ombi/commit/141f96da5e45d5b3fa5f496806b102e473da6607)) + + + +## [4.35.15](https://github.com/Ombi-app/Ombi/compare/v4.35.14...v4.35.15) (2023-04-06) + + +### Bug Fixes + +* **sonarr:** :bug: Stop the sonarr version endpoint from breaking when Sonarr is down [#4895](https://github.com/Ombi-app/Ombi/issues/4895) ([7bb8bec](https://github.com/Ombi-app/Ombi/commit/7bb8becfb140ef6012356752a71d53b5b404e482)) + + + +## [4.35.14](https://github.com/Ombi-app/Ombi/compare/v4.35.13...v4.35.14) (2023-04-06) + + +### Bug Fixes + +* Some minor tweaks to the movie info panel ([#4883](https://github.com/Ombi-app/Ombi/issues/4883)) ([1244487](https://github.com/Ombi-app/Ombi/commit/12444871df2f7602200f73971fce962f06b4a80b)) + + + +## [4.35.13](https://github.com/Ombi-app/Ombi/compare/v4.35.12...v4.35.13) (2023-03-28) + + +### Bug Fixes + +* **sonarr:** :bug: Added some more error handling and information around testing sonarr ([bd2c2d3](https://github.com/Ombi-app/Ombi/commit/bd2c2d3901e239393010fd582b207f1571fb4b7e)), closes [#4877](https://github.com/Ombi-app/Ombi/issues/4877) + + + +## [4.35.12](https://github.com/Ombi-app/Ombi/compare/v4.35.10...v4.35.12) (2023-03-25) + + +### Bug Fixes + +* **sonarr:** :bug: Improved the error handling in the sonarr settings page in the UI ([fcd78fe](https://github.com/Ombi-app/Ombi/commit/fcd78fee619d10ec7d78e8c8ec6c3ac4b0a361a1)), closes [#4877](https://github.com/Ombi-app/Ombi/issues/4877) + + + +## [4.35.9](https://github.com/Ombi-app/Ombi/compare/v4.35.8...v4.35.9) (2023-02-24) + + + +## [4.35.8](https://github.com/Ombi-app/Ombi/compare/v4.35.7...v4.35.8) (2023-02-17) + + +### Bug Fixes + +* **plex-oauth:** 🐛 Fixed an issue where using OAuth you could log in as a Ombi Local user [#4835](https://github.com/Ombi-app/Ombi/issues/4835) ([4098da3](https://github.com/Ombi-app/Ombi/commit/4098da305aaea9dae9a552644268a4fed7204cfe)) + + + +## [4.35.7](https://github.com/Ombi-app/Ombi/compare/v4.35.6...v4.35.7) (2023-02-10) + + +### Bug Fixes + +* **wizard:** :bug: Stop access to the wizard when you have already setup ombi ([#4866](https://github.com/Ombi-app/Ombi/issues/4866)) ([353de98](https://github.com/Ombi-app/Ombi/commit/353de981a462e1753288d225ec4644a44a62d2bc)) + + + +## [4.35.6](https://github.com/Ombi-app/Ombi/compare/v4.35.5...v4.35.6) (2023-01-31) + + +### Bug Fixes + +* Fixed the issue where the login page is still present after logging in with oauth ([aca4ee3](https://github.com/Ombi-app/Ombi/commit/aca4ee37915a28200e5233be3dc711ccc4a5aee9)) + + + +## [4.35.5](https://github.com/Ombi-app/Ombi/compare/v4.35.4...v4.35.5) (2023-01-24) + + +### Bug Fixes + +* **radarr-settings:** 🐛 Fixed a typo ([4a50a00](https://github.com/Ombi-app/Ombi/commit/4a50a00d4729d99f4359874b9af4dbc58a0c220b)) + + + +## [4.35.4](https://github.com/Ombi-app/Ombi/compare/v4.35.3...v4.35.4) (2023-01-22) + + +### Bug Fixes + +* **discover:** :bug: Fixed the default poster not taking into account the base url in some scenarios [#4845](https://github.com/Ombi-app/Ombi/issues/4845) ([8eda250](https://github.com/Ombi-app/Ombi/commit/8eda250367953183daec03ccb5cdf9fe94275b27)) +* **Hide music from navbar and request list when not enabled:** :bug: ([5123a76](https://github.com/Ombi-app/Ombi/commit/5123a76954e9f81d58c05e31afc7a29aec19cb7a)) + + + +## [4.35.3](https://github.com/Ombi-app/Ombi/compare/v4.35.2...v4.35.3) (2023-01-13) + + +### Bug Fixes + +* **#4847:** Invalid Discord request fixed, also fixed an issue where App Only users would not show as logged in on the user management page ([#4848](https://github.com/Ombi-app/Ombi/issues/4848)) ([f229d88](https://github.com/Ombi-app/Ombi/commit/f229d88bd744bc5253b5d3db69ae5ef22d014230)) + + + +## [4.35.2](https://github.com/Ombi-app/Ombi/compare/v4.35.1...v4.35.2) (2023-01-08) + + +### Bug Fixes + +* **database:** Just some tweaks, shouldn't notice any difference, maybe a less error in the log ([67fb992](https://github.com/Ombi-app/Ombi/commit/67fb9921c0c025025286eb6c0a9d09fd01b18465)) + + + +## [4.35.1](https://github.com/Ombi-app/Ombi/compare/v4.35.0...v4.35.1) (2023-01-06) + + +### Bug Fixes + +* **plex-watchlist:** Index out of bounds error ([8cd556e](https://github.com/Ombi-app/Ombi/commit/8cd556e268931596b9c1d1ae0ce533bfaaf330f4)) + + + +# [4.35.0](https://github.com/Ombi-app/Ombi/compare/v4.34.1...v4.35.0) (2023-01-04) + + +### Features + +* Add the option for header authentication to create users ([#4841](https://github.com/Ombi-app/Ombi/issues/4841)) ([e6c9ce5](https://github.com/Ombi-app/Ombi/commit/e6c9ce5ad0056608ecda8273fb8124ed292e2942)) + + + +## [4.34.1](https://github.com/Ombi-app/Ombi/compare/v4.34.0...v4.34.1) (2023-01-04) + + +### Bug Fixes + +* **plex-watchlist:** Lookup the ID from different sources when Plex doesn't contain the metadata ([#4843](https://github.com/Ombi-app/Ombi/issues/4843)) ([a2cc23b](https://github.com/Ombi-app/Ombi/commit/a2cc23b351c4a568c44e6c855f94db9f71ad084a)) + + + +# [4.34.0](https://github.com/Ombi-app/Ombi/compare/v4.33.1...v4.34.0) (2023-01-04) + + +### Features + +* Radarr tags ([#4815](https://github.com/Ombi-app/Ombi/issues/4815)) ([6fa5064](https://github.com/Ombi-app/Ombi/commit/6fa506491fe867cdeef9df79991ae49319d71c3d)) + + + +## [4.33.1](https://github.com/Ombi-app/Ombi/compare/v4.33.0...v4.33.1) (2022-12-22) + + +### Bug Fixes + +* **plex:** Added the watchlist request whole show back into the settings ([10701c4](https://github.com/Ombi-app/Ombi/commit/10701c4a0b6190eebb75c5d8b18224f3d0bc8502)) + + + # [4.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01) @@ -259,7 +565,7 @@ -# [4.23.0](https://github.com/Ombi-app/Ombi/compare/v4.22.4...v4.23.0) (2022-08-09) +# [4.23.0](https://github.com/Ombi-app/Ombi/compare/v4.22.5...v4.23.0) (2022-08-09) ### Bug Fixes @@ -379,3 +685,1754 @@ +## [4.20.1](https://github.com/Ombi-app/Ombi/compare/v4.20.0...v4.20.1) (2022-05-27) + + +### Bug Fixes + +* added media type tag to media type text ([#4638](https://github.com/Ombi-app/Ombi/issues/4638)) ([fe501d3](https://github.com/Ombi-app/Ombi/commit/fe501d34a0c36ac9f000b107eca49dbc6694d006)) +* **API:** Fix pagination in some edge cases ([#4649](https://github.com/Ombi-app/Ombi/issues/4649)) ([a70bf8f](https://github.com/Ombi-app/Ombi/commit/a70bf8f46c76d74c9dfdf908c53bd9955ca0a35d)) +* **discover:** Carousel touch not working when scrolling page and recommendations and similar movie navigation ([#4633](https://github.com/Ombi-app/Ombi/issues/4633)) ([d5ef1d5](https://github.com/Ombi-app/Ombi/commit/d5ef1d53e5f77d19dba8b8059c80b538a3e14f2a)) +* Improve Swagger documentation ([#4652](https://github.com/Ombi-app/Ombi/issues/4652)) ([181892b](https://github.com/Ombi-app/Ombi/commit/181892bcfe88e6d76febf49ef57745d04552d08e)) +* Missing Poster broken link fix ([#4637](https://github.com/Ombi-app/Ombi/issues/4637)) ([4070f0d](https://github.com/Ombi-app/Ombi/commit/4070f0d093b1c92487a1c80cabad8283a9650f51)) +* **sickrage:** Fixed issue with incorrect handling of SiCKRAGE episode results returned during episode status changes, now expects array of objects from data path if present ([#4648](https://github.com/Ombi-app/Ombi/issues/4648)) ([6d16442](https://github.com/Ombi-app/Ombi/commit/6d16442d4d714920367df065a3ced42b729f4233)) +* **sync:** Emby+Jellyfin - sync multi-episode files of 3+ episodes ([bd8fd89](https://github.com/Ombi-app/Ombi/commit/bd8fd890554c9d85d6da4d2cee813e82ce698e52)) + + + +# [4.20.0](https://github.com/Ombi-app/Ombi/compare/v4.19.1...v4.20.0) (2022-04-28) + + +### Features + +* **discover:** Show more relevant shows in upcoming TV ([8357819](https://github.com/Ombi-app/Ombi/commit/8357819b53b8c675c0b246d7006b5a778bdba33f)) + + + +## [4.19.1](https://github.com/Ombi-app/Ombi/compare/v4.19.0...v4.19.1) (2022-04-27) + + + +# [4.19.0](https://github.com/Ombi-app/Ombi/compare/v4.18.0...v4.19.0) (2022-04-27) + + +### Features + +* **sync:** Detect reidentified movies in Emby and Jellyfin ([5938077](https://github.com/Ombi-app/Ombi/commit/5938077d82a5357f79c07b218b3986557a5816e8)) +* **sync:** Detect reidentified series in Emby and Jellyfin ([9096e91](https://github.com/Ombi-app/Ombi/commit/9096e91d55d268819bce22831f8a8b27f2a1776b)) + + + +# [4.18.0](https://github.com/Ombi-app/Ombi/compare/v4.17.0...v4.18.0) (2022-04-26) + + +### Bug Fixes + +* **discover:** Fix cache mix up ([03d9422](https://github.com/Ombi-app/Ombi/commit/03d94220c7eaafb50c6c80a6ed1150794b873ac3)) +* **discover:** Fix new trending feature detection ([6794b88](https://github.com/Ombi-app/Ombi/commit/6794b887f6544fb41528bdd9728b7824b65e47ee)) +* **settings:** Allow toggling features when there are more than one ([a373359](https://github.com/Ombi-app/Ombi/commit/a373359ae8e6bad42b558a6e01a8ff2840d3bbaa)) + + +### Features + +* **discover:** Add new trending source experimental feature ([1a0823c](https://github.com/Ombi-app/Ombi/commit/1a0823ca80559417c67323aaeaa1ef5243e98031)) +* **discover:** Default trending source to new logic ([4f12939](https://github.com/Ombi-app/Ombi/commit/4f12939e22020a67a5ee75e2907923faea136e8d)) + + + +# [4.17.0](https://github.com/Ombi-app/Ombi/compare/v4.16.17...v4.17.0) (2022-04-25) + + + +## [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) + + +### Features + +* **discover:** Add original language filter ([ef7ec86](https://github.com/Ombi-app/Ombi/commit/ef7ec861d8aede2a4817752c990617f583805391)) + + + +## [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.39.1](https://github.com/Ombi-app/Ombi/compare/v4.39.0...v4.39.1) (2023-05-18) + + + +## [4.35.11](https://github.com/Ombi-app/Ombi/compare/v4.38.2...v4.35.11) (2023-05-17) + + + +## [4.35.10](https://github.com/Ombi-app/Ombi/compare/v4.35.9...v4.35.10) (2023-02-25) + + + +# [4.39.0](https://github.com/Ombi-app/Ombi/compare/v4.35.11...v4.39.0) (2023-05-17) + + +### Bug Fixes + +* **emby:** Fix Emby played sync running a full sync during recently added sync ([#4932](https://github.com/Ombi-app/Ombi/issues/4932)) ([9424586](https://github.com/Ombi-app/Ombi/commit/9424586e9c1b622b6475aeb8ee3cf4a8f346da6e)) + + +### Features + +* Hide watched status when request is not available ([#4934](https://github.com/Ombi-app/Ombi/issues/4934)) ([82c7f1c](https://github.com/Ombi-app/Ombi/commit/82c7f1c44fd7c87d57cc2b0c34a10fcda7628f4e)) + + + +## [4.38.2](https://github.com/Ombi-app/Ombi/compare/v4.38.1...v4.38.2) (2023-05-17) + + + +## [4.38.1](https://github.com/Ombi-app/Ombi/compare/v4.38.0...v4.38.1) (2023-05-09) + + +### Bug Fixes + +* **API:** Allow RequestOnBehalf rights if requested from the API ([#4919](https://github.com/Ombi-app/Ombi/issues/4919)) ([bb6dedd](https://github.com/Ombi-app/Ombi/commit/bb6deddfaecb3d6c7c3c6970414444b619bb9106)) +* **notificaitons:** Add the RequestedByAlias field to the Notification Message ([7e9c8be](https://github.com/Ombi-app/Ombi/commit/7e9c8bec6b02bb4e11f8db50394e493d4dd07723)) + + + +# [4.38.0](https://github.com/Ombi-app/Ombi/compare/v4.37.3...v4.38.0) (2023-05-07) + + +### Bug Fixes + +* remove sort header ([969bc7b](https://github.com/Ombi-app/Ombi/commit/969bc7bb25ea900ab9199509b079b36843e5bd6f)) + + +### Features + +* **emby:** Show watched status for Movie requests ([9cfb10b](https://github.com/Ombi-app/Ombi/commit/9cfb10bb1ee69067a6f47bd2c8a72d4e6834350e)) + + + +## [4.37.3](https://github.com/Ombi-app/Ombi/compare/v4.37.2...v4.37.3) (2023-05-07) + + +### Bug Fixes + +* Show the ApiAlias in the requests-list ([9ff624c](https://github.com/Ombi-app/Ombi/commit/9ff624ce4646815b239fbb8327117947f0a90e4b)) + + + +## [4.37.2](https://github.com/Ombi-app/Ombi/compare/v4.37.1...v4.37.2) (2023-05-03) + + +### Bug Fixes + +* **jellyfin:** Fixed an issue where the sync could stop working. Removed unused properties so the deseralization no longer fails ([0e5e0ad](https://github.com/Ombi-app/Ombi/commit/0e5e0adf862701d0f672beff14ec0aa75e4b5220)) + + + +## [4.37.1](https://github.com/Ombi-app/Ombi/compare/v4.37.0...v4.37.1) (2023-05-02) + + +### Bug Fixes + +* Cron Validation ([#4842](https://github.com/Ombi-app/Ombi/issues/4842)) ([97cc42f](https://github.com/Ombi-app/Ombi/commit/97cc42ffa8672e7d0d0996b5fbda7f7fe699da2d)) +* **discover:** :children_crossing: Improved the new Genre buttons, it now includes TV results ([b087d60](https://github.com/Ombi-app/Ombi/commit/b087d606ff36565208e564f8856903f2a4098db5)) +* **lidarr:** Change monitor to Existing to properly add artist [#3597](https://github.com/Ombi-app/Ombi/issues/3597) ([506f607](https://github.com/Ombi-app/Ombi/commit/506f60773bf1031d0be51ccd34289b855a04ea40)), closes [/github.com/Lidarr/Lidarr/issues/3597#issuecomment-1530804055](https://github.com//github.com/Lidarr/Lidarr/issues/3597/issues/issuecomment-1530804055) + + + +# [4.37.0](https://github.com/Ombi-app/Ombi/compare/v4.36.1...v4.37.0) (2023-04-24) + + +### Features + +* Search by genre ([1837419](https://github.com/Ombi-app/Ombi/commit/18374198f9f2462ba85c5781b0fcc05892728b21)) + + + +## [4.36.1](https://github.com/Ombi-app/Ombi/compare/v4.36.0...v4.36.1) (2023-04-20) + + +### Bug Fixes + +* **healthchecks:** Removed redundant ping check ([1751305](https://github.com/Ombi-app/Ombi/commit/1751305064176d2c0135f867773ccc46b03915ec)) + + + +# [4.36.0](https://github.com/Ombi-app/Ombi/compare/v4.35.19...v4.36.0) (2023-04-20) + + +### Features + +* **discover:** Add deny option to recently requested ([#4907](https://github.com/Ombi-app/Ombi/issues/4907)) ([78f340e](https://github.com/Ombi-app/Ombi/commit/78f340ee5f309c55690497170897533801957668)) + + + +## [4.35.19](https://github.com/Ombi-app/Ombi/compare/v4.35.18...v4.35.19) (2023-04-20) + + +### Bug Fixes + +* **radarr:** Fixed an issue where the radarr sync would break ([de4baad](https://github.com/Ombi-app/Ombi/commit/de4baade9f87248d77106ff1a313a498870f4fb3)) + + + +## [4.35.18](https://github.com/Ombi-app/Ombi/compare/v4.35.17...v4.35.18) (2023-04-15) + + +### Bug Fixes + +* **#4906:** :bug: Fixed an issue with power users and permissions ([80884bc](https://github.com/Ombi-app/Ombi/commit/80884bcd725c329867c278ad235cd4096cd4fe7a)) + + + +## [4.35.17](https://github.com/Ombi-app/Ombi/compare/v4.35.16...v4.35.17) (2023-04-15) + + +### Bug Fixes + +* **discover:** Fix denied requests displayed as approved ([#4901](https://github.com/Ombi-app/Ombi/issues/4901)) ([1e87f20](https://github.com/Ombi-app/Ombi/commit/1e87f2010491b0f3fdda70d2b19d9afd94438df7)) +* Fix denied movie shown as 'processing request' in details view ([#4900](https://github.com/Ombi-app/Ombi/issues/4900)) ([0069bfd](https://github.com/Ombi-app/Ombi/commit/0069bfdf54e0785bad45c832ca052f19fd4b940b)) + + + +## [4.35.16](https://github.com/Ombi-app/Ombi/compare/v4.35.15...v4.35.16) (2023-04-13) + + +### Bug Fixes + +* Support duplicates in Emby/JF collections ([#4902](https://github.com/Ombi-app/Ombi/issues/4902)) ([141f96d](https://github.com/Ombi-app/Ombi/commit/141f96da5e45d5b3fa5f496806b102e473da6607)) + + + +## [4.35.15](https://github.com/Ombi-app/Ombi/compare/v4.35.14...v4.35.15) (2023-04-06) + + +### Bug Fixes + +* **sonarr:** :bug: Stop the sonarr version endpoint from breaking when Sonarr is down [#4895](https://github.com/Ombi-app/Ombi/issues/4895) ([7bb8bec](https://github.com/Ombi-app/Ombi/commit/7bb8becfb140ef6012356752a71d53b5b404e482)) + + + +## [4.35.14](https://github.com/Ombi-app/Ombi/compare/v4.35.13...v4.35.14) (2023-04-06) + + +### Bug Fixes + +* Some minor tweaks to the movie info panel ([#4883](https://github.com/Ombi-app/Ombi/issues/4883)) ([1244487](https://github.com/Ombi-app/Ombi/commit/12444871df2f7602200f73971fce962f06b4a80b)) + + + +## [4.35.13](https://github.com/Ombi-app/Ombi/compare/v4.35.12...v4.35.13) (2023-03-28) + + +### Bug Fixes + +* **sonarr:** :bug: Added some more error handling and information around testing sonarr ([bd2c2d3](https://github.com/Ombi-app/Ombi/commit/bd2c2d3901e239393010fd582b207f1571fb4b7e)), closes [#4877](https://github.com/Ombi-app/Ombi/issues/4877) + + + +## [4.35.12](https://github.com/Ombi-app/Ombi/compare/v4.35.10...v4.35.12) (2023-03-25) + + +### Bug Fixes + +* **sonarr:** :bug: Improved the error handling in the sonarr settings page in the UI ([fcd78fe](https://github.com/Ombi-app/Ombi/commit/fcd78fee619d10ec7d78e8c8ec6c3ac4b0a361a1)), closes [#4877](https://github.com/Ombi-app/Ombi/issues/4877) + + + +## [4.35.9](https://github.com/Ombi-app/Ombi/compare/v4.35.8...v4.35.9) (2023-02-24) + + + +## [4.35.8](https://github.com/Ombi-app/Ombi/compare/v4.35.7...v4.35.8) (2023-02-17) + + +### Bug Fixes + +* **plex-oauth:** 🐛 Fixed an issue where using OAuth you could log in as a Ombi Local user [#4835](https://github.com/Ombi-app/Ombi/issues/4835) ([4098da3](https://github.com/Ombi-app/Ombi/commit/4098da305aaea9dae9a552644268a4fed7204cfe)) + + + +## [4.35.7](https://github.com/Ombi-app/Ombi/compare/v4.35.6...v4.35.7) (2023-02-10) + + +### Bug Fixes + +* **wizard:** :bug: Stop access to the wizard when you have already setup ombi ([#4866](https://github.com/Ombi-app/Ombi/issues/4866)) ([353de98](https://github.com/Ombi-app/Ombi/commit/353de981a462e1753288d225ec4644a44a62d2bc)) + + + +## [4.35.6](https://github.com/Ombi-app/Ombi/compare/v4.35.5...v4.35.6) (2023-01-31) + + +### Bug Fixes + +* Fixed the issue where the login page is still present after logging in with oauth ([aca4ee3](https://github.com/Ombi-app/Ombi/commit/aca4ee37915a28200e5233be3dc711ccc4a5aee9)) + + + +## [4.35.5](https://github.com/Ombi-app/Ombi/compare/v4.35.4...v4.35.5) (2023-01-24) + + +### Bug Fixes + +* **radarr-settings:** 🐛 Fixed a typo ([4a50a00](https://github.com/Ombi-app/Ombi/commit/4a50a00d4729d99f4359874b9af4dbc58a0c220b)) + + + +## [4.35.4](https://github.com/Ombi-app/Ombi/compare/v4.35.3...v4.35.4) (2023-01-22) + + +### Bug Fixes + +* **discover:** :bug: Fixed the default poster not taking into account the base url in some scenarios [#4845](https://github.com/Ombi-app/Ombi/issues/4845) ([8eda250](https://github.com/Ombi-app/Ombi/commit/8eda250367953183daec03ccb5cdf9fe94275b27)) +* **Hide music from navbar and request list when not enabled:** :bug: ([5123a76](https://github.com/Ombi-app/Ombi/commit/5123a76954e9f81d58c05e31afc7a29aec19cb7a)) + + + +## [4.35.3](https://github.com/Ombi-app/Ombi/compare/v4.35.2...v4.35.3) (2023-01-13) + + +### Bug Fixes + +* **#4847:** Invalid Discord request fixed, also fixed an issue where App Only users would not show as logged in on the user management page ([#4848](https://github.com/Ombi-app/Ombi/issues/4848)) ([f229d88](https://github.com/Ombi-app/Ombi/commit/f229d88bd744bc5253b5d3db69ae5ef22d014230)) + + + +## [4.35.2](https://github.com/Ombi-app/Ombi/compare/v4.35.1...v4.35.2) (2023-01-08) + + +### Bug Fixes + +* **database:** Just some tweaks, shouldn't notice any difference, maybe a less error in the log ([67fb992](https://github.com/Ombi-app/Ombi/commit/67fb9921c0c025025286eb6c0a9d09fd01b18465)) + + + +## [4.35.1](https://github.com/Ombi-app/Ombi/compare/v4.35.0...v4.35.1) (2023-01-06) + + +### Bug Fixes + +* **plex-watchlist:** Index out of bounds error ([8cd556e](https://github.com/Ombi-app/Ombi/commit/8cd556e268931596b9c1d1ae0ce533bfaaf330f4)) + + + +# [4.35.0](https://github.com/Ombi-app/Ombi/compare/v4.34.1...v4.35.0) (2023-01-04) + + +### Features + +* Add the option for header authentication to create users ([#4841](https://github.com/Ombi-app/Ombi/issues/4841)) ([e6c9ce5](https://github.com/Ombi-app/Ombi/commit/e6c9ce5ad0056608ecda8273fb8124ed292e2942)) + + + +## [4.34.1](https://github.com/Ombi-app/Ombi/compare/v4.34.0...v4.34.1) (2023-01-04) + + +### Bug Fixes + +* **plex-watchlist:** Lookup the ID from different sources when Plex doesn't contain the metadata ([#4843](https://github.com/Ombi-app/Ombi/issues/4843)) ([a2cc23b](https://github.com/Ombi-app/Ombi/commit/a2cc23b351c4a568c44e6c855f94db9f71ad084a)) + + + +# [4.34.0](https://github.com/Ombi-app/Ombi/compare/v4.33.1...v4.34.0) (2023-01-04) + + +### Features + +* Radarr tags ([#4815](https://github.com/Ombi-app/Ombi/issues/4815)) ([6fa5064](https://github.com/Ombi-app/Ombi/commit/6fa506491fe867cdeef9df79991ae49319d71c3d)) + + + +## [4.33.1](https://github.com/Ombi-app/Ombi/compare/v4.33.0...v4.33.1) (2022-12-22) + + +### Bug Fixes + +* **plex:** Added the watchlist request whole show back into the settings ([10701c4](https://github.com/Ombi-app/Ombi/commit/10701c4a0b6190eebb75c5d8b18224f3d0bc8502)) + + + +# [4.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01) + + +### Features + +* Angular 15 and Dependency upgrades ([#4818](https://github.com/Ombi-app/Ombi/issues/4818)) ([4816acf](https://github.com/Ombi-app/Ombi/commit/4816acf6f94443d23ebef6091d4cfcbca580f9ca)) + + + +## [4.32.3](https://github.com/Ombi-app/Ombi/compare/v4.32.2...v4.32.3) (2022-11-24) + + +### Bug Fixes + +* **sonarr:** V4 actually works this time around ([f62e70f](https://github.com/Ombi-app/Ombi/commit/f62e70fc493c7971da5e4508ce10522f5df0bbf7)) + + + +## [4.32.2](https://github.com/Ombi-app/Ombi/compare/v4.32.1...v4.32.2) (2022-11-23) + + +### Bug Fixes + +* **sonarr:** :bug: Sonarr V4 should work now ([#4810](https://github.com/Ombi-app/Ombi/issues/4810)) ([37655af](https://github.com/Ombi-app/Ombi/commit/37655aff9d3d133b42f5664bc9445d6571e966d6)) + + + +## [4.32.1](https://github.com/Ombi-app/Ombi/compare/v4.32.0...v4.32.1) (2022-11-21) + + +### Bug Fixes + +* **plex:** :bug: Fixed the issue where you couldn't add a new server on a fresh setup after the settings page rework ([187b18d](https://github.com/Ombi-app/Ombi/commit/187b18d5c01f6a13831e4a410b5d7c349e27d847)) + + + +# [4.32.0](https://github.com/Ombi-app/Ombi/compare/v4.31.0...v4.32.0) (2022-11-18) + + +### Bug Fixes + +* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4801](https://github.com/Ombi-app/Ombi/issues/4801)) ([4692003](https://github.com/Ombi-app/Ombi/commit/46920032baed04675b2ffbe1700afdc0740a4ac4)) + + +### Features + +* **plex:** Rework the Plex Settings page ([#4805](https://github.com/Ombi-app/Ombi/issues/4805)) ([1b8c47f](https://github.com/Ombi-app/Ombi/commit/1b8c47f3163f618851d4904732cb07015e1e93ff)) + + + +# [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.7](https://github.com/Ombi-app/Ombi/compare/v4.27.6...v4.27.7) (2022-10-07) + + +### Bug Fixes + +* Fixes default image for recently requested items. ([#4767](https://github.com/Ombi-app/Ombi/issues/4767)) ([2e6f35f](https://github.com/Ombi-app/Ombi/commit/2e6f35f89abb3dd3685ec8289f8620c7ef7072cd)) + + + +## [4.27.6](https://github.com/Ombi-app/Ombi/compare/v4.27.5...v4.27.6) (2022-10-01) + + +### Bug Fixes + +* **notifications:** Fixed the error when sending multiple test notifications. Added more logging when Discord complains the message is invalid ([fc14780](https://github.com/Ombi-app/Ombi/commit/fc14780bd354483119ddcbb55a8c382e1890a783)) + + + +## [4.27.5](https://github.com/Ombi-app/Ombi/compare/v4.27.4...v4.27.5) (2022-09-30) + + +### Bug Fixes + +* **importer:** 🐛 Allow you to only import Plex Admins without the Plex Users ([8c9ad9b](https://github.com/Ombi-app/Ombi/commit/8c9ad9b414fdc6c88bdb911d6057ae5d38783b98)) + + + +## [4.27.4](https://github.com/Ombi-app/Ombi/compare/v4.27.3...v4.27.4) (2022-09-30) + + + +## [4.27.3](https://github.com/Ombi-app/Ombi/compare/v4.27.2...v4.27.3) (2022-09-30) + + +### Bug Fixes + +* **availability:** 🐛 Fixed a issue with the availability checker after the previous update. Added full test coverage around that area ([28e2480](https://github.com/Ombi-app/Ombi/commit/28e248046ad56390595f84172bbd5f5961325b4d)) + + + +## [4.27.2](https://github.com/Ombi-app/Ombi/compare/v4.27.1...v4.27.2) (2022-09-29) + + +### Bug Fixes + +* **sonarr:** :bug: Cleaned up and removed Sonarr v3 option, sonarr v3 is now the default. This allows us to get ready for the upcoming Sonarr v4 ([#4764](https://github.com/Ombi-app/Ombi/issues/4764)) ([2cddec7](https://github.com/Ombi-app/Ombi/commit/2cddec759004b6490f686ff74cb092238e3dc946)) + + + +## [4.27.1](https://github.com/Ombi-app/Ombi/compare/v4.27.0...v4.27.1) (2022-09-20) + + +### Bug Fixes + +* **plex:** stop the plex sync from deleting episodes when we can't find the plex key ([66b05e5](https://github.com/Ombi-app/Ombi/commit/66b05e5a85dbfe1fec5f9366e80987f2cfa1f4fe)) + + + +# [4.27.0](https://github.com/Ombi-app/Ombi/compare/v4.26.0...v4.27.0) (2022-09-14) + + +### Features + +* Recently requested improvements ([#4755](https://github.com/Ombi-app/Ombi/issues/4755)) ([ff04d87](https://github.com/Ombi-app/Ombi/commit/ff04d875343604c77c391bf55d0968977e480281)) + + + +# [4.26.0](https://github.com/Ombi-app/Ombi/compare/v4.25.1...v4.26.0) (2022-09-07) + + +### Features + +* **notifications:** Add more curly variables for partially available notification ([66aa101](https://github.com/Ombi-app/Ombi/commit/66aa101019c4c4b34e186db9d303049d02b9c781)) + + + +## [4.25.1](https://github.com/Ombi-app/Ombi/compare/v4.25.0...v4.25.1) (2022-09-07) + + +### Bug Fixes + +* **webhook:** Remove added trailing slash from webhook URL [#4710](https://github.com/Ombi-app/Ombi/issues/4710) ([369eb33](https://github.com/Ombi-app/Ombi/commit/369eb339171671101be219486e2aab27a20f3d74)) + + + +# [4.25.0](https://github.com/Ombi-app/Ombi/compare/v4.24.0...v4.25.0) (2022-08-23) + + +### Bug Fixes + +* fixed stats controller ([#4742](https://github.com/Ombi-app/Ombi/issues/4742)) ([47ea64b](https://github.com/Ombi-app/Ombi/commit/47ea64b5a401770f1943b575ca40f84d515e96b3)) + + +### Features + +* Watchlist history errors([#4741](https://github.com/Ombi-app/Ombi/issues/4741)) ([c222f1a](https://github.com/Ombi-app/Ombi/commit/c222f1a945e944ef34e68cad2b61f40e57cab823)) + + + +# [4.24.0](https://github.com/Ombi-app/Ombi/compare/v4.23.2...v4.24.0) (2022-08-22) + + +### Features + +* add crew on movie page ([#4722](https://github.com/Ombi-app/Ombi/issues/4722)) ([1d53261](https://github.com/Ombi-app/Ombi/commit/1d532613823804b25984bd1d223d081a54ad143d)) + + + +## [4.23.2](https://github.com/Ombi-app/Ombi/compare/v4.23.1...v4.23.2) (2022-08-22) + + +### Bug Fixes + +* Fix conflicting property name for Swagger ([#4733](https://github.com/Ombi-app/Ombi/issues/4733)) ([d661f32](https://github.com/Ombi-app/Ombi/commit/d661f32e8a9e105faab6380b4b7b642896b98163)) + + + +## [4.23.1](https://github.com/Ombi-app/Ombi/compare/v4.23.0...v4.23.1) (2022-08-12) + + +### Bug Fixes + +* Localize recently requested on discover page ([#4729](https://github.com/Ombi-app/Ombi/issues/4729)) ([bf65c76](https://github.com/Ombi-app/Ombi/commit/bf65c76ff9ce38f65a9e5feb872734e8d8e35eb6)) + + + +# [4.23.0](https://github.com/Ombi-app/Ombi/compare/v4.22.5...v4.23.0) (2022-08-09) + + +### Bug Fixes + +* Log Microsoft warnings to log file ([#4723](https://github.com/Ombi-app/Ombi/issues/4723)) ([26ac75f](https://github.com/Ombi-app/Ombi/commit/26ac75f0c223c2a91e3471797ae46ede3fde89cc)) + + +### Features + +* ✨ Recently Requested on Discover Page ([#4387](https://github.com/Ombi-app/Ombi/issues/4387)) ([44d38fb](https://github.com/Ombi-app/Ombi/commit/44d38fbaae521dbb467b61c7471b2384015ac52e)) + + + +## [4.22.4](https://github.com/Ombi-app/Ombi/compare/v4.22.3...v4.22.4) (2022-08-04) + + +### Bug Fixes + +* :bug: Fixed missing externals ([#4712](https://github.com/Ombi-app/Ombi/issues/4712)) ([fcc1eaa](https://github.com/Ombi-app/Ombi/commit/fcc1eaaa377683dcdc81d62a2a688fb0c4490c7b)) +* fixed trakt image not loading when base url present ([#4711](https://github.com/Ombi-app/Ombi/issues/4711)) ([f102dcf](https://github.com/Ombi-app/Ombi/commit/f102dcf751c2eb62ebfe30f9f8e4b2ad863c3b0d)) +* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4713](https://github.com/Ombi-app/Ombi/issues/4713)) ([ff142b0](https://github.com/Ombi-app/Ombi/commit/ff142b09abbb2f9540387284222552e6e12639fe)) + + + +## [4.22.3](https://github.com/Ombi-app/Ombi/compare/v4.22.2...v4.22.3) (2022-07-28) + + +### Bug Fixes + +* Override Sonarr V3 Profiles endpoint ([#4678](https://github.com/Ombi-app/Ombi/issues/4678)) ([875da95](https://github.com/Ombi-app/Ombi/commit/875da959f353119b05138d68ee6d32a49e14b91e)) + + + +## [4.22.2](https://github.com/Ombi-app/Ombi/compare/v4.22.1...v4.22.2) (2022-07-25) + + +### Bug Fixes + +* fixed an issue where I broke images for some users ([81ddc85](https://github.com/Ombi-app/Ombi/commit/81ddc8553b9094c3f6843b036daebb2eb9262e00)) + + + +## [4.22.1](https://github.com/Ombi-app/Ombi/compare/v4.22.0...v4.22.1) (2022-07-25) + + +### Bug Fixes + +* **discover:** :bug: Created new Image component to handle 429's from TMDB ([#4698](https://github.com/Ombi-app/Ombi/issues/4698)) and fixed [#4635](https://github.com/Ombi-app/Ombi/issues/4635) ([#4699](https://github.com/Ombi-app/Ombi/issues/4699)) ([f22d3da](https://github.com/Ombi-app/Ombi/commit/f22d3da765799365455b919027f7563e52b347c3)) + + + +# [4.22.0](https://github.com/Ombi-app/Ombi/compare/v4.21.2...v4.22.0) (2022-07-22) + + +### Features + +* **discover:** ✨ Added infinite scroll on advanced search results ([898bc89](https://github.com/Ombi-app/Ombi/commit/898bc89fa78245c1f3de9481f6c724f087a16e39)) + + + +## [4.21.2](https://github.com/Ombi-app/Ombi/compare/v4.21.1...v4.21.2) (2022-07-22) + + +### Bug Fixes + +* Landing and Login page improvements ([#4690](https://github.com/Ombi-app/Ombi/issues/4690)) ([6d423b5](https://github.com/Ombi-app/Ombi/commit/6d423b5447c52c5e59d8d2bd92a23b47468eb736)) + + + +## [4.21.1](https://github.com/Ombi-app/Ombi/compare/v4.21.0...v4.21.1) (2022-07-11) + + +### Bug Fixes + +* **images:** Retry images with a backoff when we get a Too Many requests from TheMovieDb [#4685](https://github.com/Ombi-app/Ombi/issues/4685) ([3f1f35d](https://github.com/Ombi-app/Ombi/commit/3f1f35df3164db6739691cdda8f925c296239791)) + + + +# [4.21.0](https://github.com/Ombi-app/Ombi/compare/v4.20.4...v4.21.0) (2022-06-22) + + +### Features + +* Upgrade to Angular14 ([#4668](https://github.com/Ombi-app/Ombi/issues/4668)) ([b9d55a4](https://github.com/Ombi-app/Ombi/commit/b9d55a469b412558cbf67c1e25db7fdda5964cd8)) + + +### Performance Improvements + +* stop populating obsolete subscribe fields ([#4625](https://github.com/Ombi-app/Ombi/issues/4625)) ([9a73463](https://github.com/Ombi-app/Ombi/commit/9a734637665f671b17c2bb440d93b35a891c142b)) + + + +## [4.20.4](https://github.com/Ombi-app/Ombi/compare/v4.20.3...v4.20.4) (2022-06-15) + + +### Bug Fixes + +* fixed build ([f877921](https://github.com/Ombi-app/Ombi/commit/f8779219146051ea74f8b6408658ff7975afb88b)) + + + +## [4.20.3](https://github.com/Ombi-app/Ombi/compare/v4.20.2...v4.20.3) (2022-06-05) + + +### Bug Fixes + +* **plex:** 🐛 Fixed an issue with the Plex Sync ([ab1a11a](https://github.com/Ombi-app/Ombi/commit/ab1a11af78efbe9d37bd55aa80a640796c138a98)) + + + +## [4.20.2](https://github.com/Ombi-app/Ombi/compare/v4.20.1...v4.20.2) (2022-06-03) + + +### Bug Fixes + +* :bug: Fixed the Request on Behalf of having blanks ([#4667](https://github.com/Ombi-app/Ombi/issues/4667)) ([7dd9b1c](https://github.com/Ombi-app/Ombi/commit/7dd9b1cac07f571dd35b362544e4fe0226c4b817)) + + + +## [4.20.1](https://github.com/Ombi-app/Ombi/compare/v4.20.0...v4.20.1) (2022-05-27) + + +### Bug Fixes + +* added media type tag to media type text ([#4638](https://github.com/Ombi-app/Ombi/issues/4638)) ([fe501d3](https://github.com/Ombi-app/Ombi/commit/fe501d34a0c36ac9f000b107eca49dbc6694d006)) +* **API:** Fix pagination in some edge cases ([#4649](https://github.com/Ombi-app/Ombi/issues/4649)) ([a70bf8f](https://github.com/Ombi-app/Ombi/commit/a70bf8f46c76d74c9dfdf908c53bd9955ca0a35d)) +* **discover:** Carousel touch not working when scrolling page and recommendations and similar movie navigation ([#4633](https://github.com/Ombi-app/Ombi/issues/4633)) ([d5ef1d5](https://github.com/Ombi-app/Ombi/commit/d5ef1d53e5f77d19dba8b8059c80b538a3e14f2a)) +* Improve Swagger documentation ([#4652](https://github.com/Ombi-app/Ombi/issues/4652)) ([181892b](https://github.com/Ombi-app/Ombi/commit/181892bcfe88e6d76febf49ef57745d04552d08e)) +* Missing Poster broken link fix ([#4637](https://github.com/Ombi-app/Ombi/issues/4637)) ([4070f0d](https://github.com/Ombi-app/Ombi/commit/4070f0d093b1c92487a1c80cabad8283a9650f51)) +* **sickrage:** Fixed issue with incorrect handling of SiCKRAGE episode results returned during episode status changes, now expects array of objects from data path if present ([#4648](https://github.com/Ombi-app/Ombi/issues/4648)) ([6d16442](https://github.com/Ombi-app/Ombi/commit/6d16442d4d714920367df065a3ced42b729f4233)) +* **sync:** Emby+Jellyfin - sync multi-episode files of 3+ episodes ([bd8fd89](https://github.com/Ombi-app/Ombi/commit/bd8fd890554c9d85d6da4d2cee813e82ce698e52)) + + + +# [4.20.0](https://github.com/Ombi-app/Ombi/compare/v4.19.1...v4.20.0) (2022-04-28) + + +### Features + +* **discover:** Show more relevant shows in upcoming TV ([8357819](https://github.com/Ombi-app/Ombi/commit/8357819b53b8c675c0b246d7006b5a778bdba33f)) + + + +## [4.19.1](https://github.com/Ombi-app/Ombi/compare/v4.19.0...v4.19.1) (2022-04-27) + + + +# [4.19.0](https://github.com/Ombi-app/Ombi/compare/v4.18.0...v4.19.0) (2022-04-27) + + +### Features + +* **sync:** Detect reidentified movies in Emby and Jellyfin ([5938077](https://github.com/Ombi-app/Ombi/commit/5938077d82a5357f79c07b218b3986557a5816e8)) +* **sync:** Detect reidentified series in Emby and Jellyfin ([9096e91](https://github.com/Ombi-app/Ombi/commit/9096e91d55d268819bce22831f8a8b27f2a1776b)) + + + +# [4.18.0](https://github.com/Ombi-app/Ombi/compare/v4.17.0...v4.18.0) (2022-04-26) + + +### Bug Fixes + +* **discover:** Fix cache mix up ([03d9422](https://github.com/Ombi-app/Ombi/commit/03d94220c7eaafb50c6c80a6ed1150794b873ac3)) +* **discover:** Fix new trending feature detection ([6794b88](https://github.com/Ombi-app/Ombi/commit/6794b887f6544fb41528bdd9728b7824b65e47ee)) +* **settings:** Allow toggling features when there are more than one ([a373359](https://github.com/Ombi-app/Ombi/commit/a373359ae8e6bad42b558a6e01a8ff2840d3bbaa)) + + +### Features + +* **discover:** Add new trending source experimental feature ([1a0823c](https://github.com/Ombi-app/Ombi/commit/1a0823ca80559417c67323aaeaa1ef5243e98031)) +* **discover:** Default trending source to new logic ([4f12939](https://github.com/Ombi-app/Ombi/commit/4f12939e22020a67a5ee75e2907923faea136e8d)) + + + +# [4.17.0](https://github.com/Ombi-app/Ombi/compare/v4.16.17...v4.17.0) (2022-04-25) + + + +## [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) + + +### Features + +* **discover:** Add original language filter ([ef7ec86](https://github.com/Ombi-app/Ombi/commit/ef7ec861d8aede2a4817752c990617f583805391)) + + + +## [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.35.11](https://github.com/Ombi-app/Ombi/compare/v4.38.2...v4.35.11) (2023-05-17) + + + +## [4.35.10](https://github.com/Ombi-app/Ombi/compare/v4.35.9...v4.35.10) (2023-02-25) + + + +## [4.38.2](https://github.com/Ombi-app/Ombi/compare/v4.38.1...v4.38.2) (2023-05-17) + + + +## [4.38.1](https://github.com/Ombi-app/Ombi/compare/v4.38.0...v4.38.1) (2023-05-09) + + +### Bug Fixes + +* **API:** Allow RequestOnBehalf rights if requested from the API ([#4919](https://github.com/Ombi-app/Ombi/issues/4919)) ([bb6dedd](https://github.com/Ombi-app/Ombi/commit/bb6deddfaecb3d6c7c3c6970414444b619bb9106)) +* **notificaitons:** Add the RequestedByAlias field to the Notification Message ([7e9c8be](https://github.com/Ombi-app/Ombi/commit/7e9c8bec6b02bb4e11f8db50394e493d4dd07723)) + + + +# [4.38.0](https://github.com/Ombi-app/Ombi/compare/v4.37.3...v4.38.0) (2023-05-07) + + +### Bug Fixes + +* remove sort header ([969bc7b](https://github.com/Ombi-app/Ombi/commit/969bc7bb25ea900ab9199509b079b36843e5bd6f)) + + +### Features + +* **emby:** Show watched status for Movie requests ([9cfb10b](https://github.com/Ombi-app/Ombi/commit/9cfb10bb1ee69067a6f47bd2c8a72d4e6834350e)) + + + +## [4.37.3](https://github.com/Ombi-app/Ombi/compare/v4.37.2...v4.37.3) (2023-05-07) + + +### Bug Fixes + +* Show the ApiAlias in the requests-list ([9ff624c](https://github.com/Ombi-app/Ombi/commit/9ff624ce4646815b239fbb8327117947f0a90e4b)) + + + +## [4.37.2](https://github.com/Ombi-app/Ombi/compare/v4.37.1...v4.37.2) (2023-05-03) + + +### Bug Fixes + +* **jellyfin:** Fixed an issue where the sync could stop working. Removed unused properties so the deseralization no longer fails ([0e5e0ad](https://github.com/Ombi-app/Ombi/commit/0e5e0adf862701d0f672beff14ec0aa75e4b5220)) + + + +## [4.37.1](https://github.com/Ombi-app/Ombi/compare/v4.37.0...v4.37.1) (2023-05-02) + + +### Bug Fixes + +* Cron Validation ([#4842](https://github.com/Ombi-app/Ombi/issues/4842)) ([97cc42f](https://github.com/Ombi-app/Ombi/commit/97cc42ffa8672e7d0d0996b5fbda7f7fe699da2d)) +* **discover:** :children_crossing: Improved the new Genre buttons, it now includes TV results ([b087d60](https://github.com/Ombi-app/Ombi/commit/b087d606ff36565208e564f8856903f2a4098db5)) +* **lidarr:** Change monitor to Existing to properly add artist [#3597](https://github.com/Ombi-app/Ombi/issues/3597) ([506f607](https://github.com/Ombi-app/Ombi/commit/506f60773bf1031d0be51ccd34289b855a04ea40)), closes [/github.com/Lidarr/Lidarr/issues/3597#issuecomment-1530804055](https://github.com//github.com/Lidarr/Lidarr/issues/3597/issues/issuecomment-1530804055) + + + +# [4.37.0](https://github.com/Ombi-app/Ombi/compare/v4.36.1...v4.37.0) (2023-04-24) + + +### Features + +* Search by genre ([1837419](https://github.com/Ombi-app/Ombi/commit/18374198f9f2462ba85c5781b0fcc05892728b21)) + + + +## [4.36.1](https://github.com/Ombi-app/Ombi/compare/v4.36.0...v4.36.1) (2023-04-20) + + +### Bug Fixes + +* **healthchecks:** Removed redundant ping check ([1751305](https://github.com/Ombi-app/Ombi/commit/1751305064176d2c0135f867773ccc46b03915ec)) + + + +# [4.36.0](https://github.com/Ombi-app/Ombi/compare/v4.35.19...v4.36.0) (2023-04-20) + + +### Features + +* **discover:** Add deny option to recently requested ([#4907](https://github.com/Ombi-app/Ombi/issues/4907)) ([78f340e](https://github.com/Ombi-app/Ombi/commit/78f340ee5f309c55690497170897533801957668)) + + + +## [4.35.19](https://github.com/Ombi-app/Ombi/compare/v4.35.18...v4.35.19) (2023-04-20) + + +### Bug Fixes + +* **radarr:** Fixed an issue where the radarr sync would break ([de4baad](https://github.com/Ombi-app/Ombi/commit/de4baade9f87248d77106ff1a313a498870f4fb3)) + + + +## [4.35.18](https://github.com/Ombi-app/Ombi/compare/v4.35.17...v4.35.18) (2023-04-15) + + +### Bug Fixes + +* **#4906:** :bug: Fixed an issue with power users and permissions ([80884bc](https://github.com/Ombi-app/Ombi/commit/80884bcd725c329867c278ad235cd4096cd4fe7a)) + + + +## [4.35.17](https://github.com/Ombi-app/Ombi/compare/v4.35.16...v4.35.17) (2023-04-15) + + +### Bug Fixes + +* **discover:** Fix denied requests displayed as approved ([#4901](https://github.com/Ombi-app/Ombi/issues/4901)) ([1e87f20](https://github.com/Ombi-app/Ombi/commit/1e87f2010491b0f3fdda70d2b19d9afd94438df7)) +* Fix denied movie shown as 'processing request' in details view ([#4900](https://github.com/Ombi-app/Ombi/issues/4900)) ([0069bfd](https://github.com/Ombi-app/Ombi/commit/0069bfdf54e0785bad45c832ca052f19fd4b940b)) + + + +## [4.35.16](https://github.com/Ombi-app/Ombi/compare/v4.35.15...v4.35.16) (2023-04-13) + + +### Bug Fixes + +* Support duplicates in Emby/JF collections ([#4902](https://github.com/Ombi-app/Ombi/issues/4902)) ([141f96d](https://github.com/Ombi-app/Ombi/commit/141f96da5e45d5b3fa5f496806b102e473da6607)) + + + +## [4.35.15](https://github.com/Ombi-app/Ombi/compare/v4.35.14...v4.35.15) (2023-04-06) + + +### Bug Fixes + +* **sonarr:** :bug: Stop the sonarr version endpoint from breaking when Sonarr is down [#4895](https://github.com/Ombi-app/Ombi/issues/4895) ([7bb8bec](https://github.com/Ombi-app/Ombi/commit/7bb8becfb140ef6012356752a71d53b5b404e482)) + + + +## [4.35.14](https://github.com/Ombi-app/Ombi/compare/v4.35.13...v4.35.14) (2023-04-06) + + +### Bug Fixes + +* Some minor tweaks to the movie info panel ([#4883](https://github.com/Ombi-app/Ombi/issues/4883)) ([1244487](https://github.com/Ombi-app/Ombi/commit/12444871df2f7602200f73971fce962f06b4a80b)) + + + +## [4.35.13](https://github.com/Ombi-app/Ombi/compare/v4.35.12...v4.35.13) (2023-03-28) + + +### Bug Fixes + +* **sonarr:** :bug: Added some more error handling and information around testing sonarr ([bd2c2d3](https://github.com/Ombi-app/Ombi/commit/bd2c2d3901e239393010fd582b207f1571fb4b7e)), closes [#4877](https://github.com/Ombi-app/Ombi/issues/4877) + + + +## [4.35.12](https://github.com/Ombi-app/Ombi/compare/v4.35.10...v4.35.12) (2023-03-25) + + +### Bug Fixes + +* **sonarr:** :bug: Improved the error handling in the sonarr settings page in the UI ([fcd78fe](https://github.com/Ombi-app/Ombi/commit/fcd78fee619d10ec7d78e8c8ec6c3ac4b0a361a1)), closes [#4877](https://github.com/Ombi-app/Ombi/issues/4877) + + + +## [4.35.9](https://github.com/Ombi-app/Ombi/compare/v4.35.8...v4.35.9) (2023-02-24) + + + +## [4.35.8](https://github.com/Ombi-app/Ombi/compare/v4.35.7...v4.35.8) (2023-02-17) + + +### Bug Fixes + +* **plex-oauth:** 🐛 Fixed an issue where using OAuth you could log in as a Ombi Local user [#4835](https://github.com/Ombi-app/Ombi/issues/4835) ([4098da3](https://github.com/Ombi-app/Ombi/commit/4098da305aaea9dae9a552644268a4fed7204cfe)) + + + +## [4.35.7](https://github.com/Ombi-app/Ombi/compare/v4.35.6...v4.35.7) (2023-02-10) + + +### Bug Fixes + +* **wizard:** :bug: Stop access to the wizard when you have already setup ombi ([#4866](https://github.com/Ombi-app/Ombi/issues/4866)) ([353de98](https://github.com/Ombi-app/Ombi/commit/353de981a462e1753288d225ec4644a44a62d2bc)) + + + +## [4.35.6](https://github.com/Ombi-app/Ombi/compare/v4.35.5...v4.35.6) (2023-01-31) + + +### Bug Fixes + +* Fixed the issue where the login page is still present after logging in with oauth ([aca4ee3](https://github.com/Ombi-app/Ombi/commit/aca4ee37915a28200e5233be3dc711ccc4a5aee9)) + + + +## [4.35.5](https://github.com/Ombi-app/Ombi/compare/v4.35.4...v4.35.5) (2023-01-24) + + +### Bug Fixes + +* **radarr-settings:** 🐛 Fixed a typo ([4a50a00](https://github.com/Ombi-app/Ombi/commit/4a50a00d4729d99f4359874b9af4dbc58a0c220b)) + + + +## [4.35.4](https://github.com/Ombi-app/Ombi/compare/v4.35.3...v4.35.4) (2023-01-22) + + +### Bug Fixes + +* **discover:** :bug: Fixed the default poster not taking into account the base url in some scenarios [#4845](https://github.com/Ombi-app/Ombi/issues/4845) ([8eda250](https://github.com/Ombi-app/Ombi/commit/8eda250367953183daec03ccb5cdf9fe94275b27)) +* **Hide music from navbar and request list when not enabled:** :bug: ([5123a76](https://github.com/Ombi-app/Ombi/commit/5123a76954e9f81d58c05e31afc7a29aec19cb7a)) + + + +## [4.35.3](https://github.com/Ombi-app/Ombi/compare/v4.35.2...v4.35.3) (2023-01-13) + + +### Bug Fixes + +* **#4847:** Invalid Discord request fixed, also fixed an issue where App Only users would not show as logged in on the user management page ([#4848](https://github.com/Ombi-app/Ombi/issues/4848)) ([f229d88](https://github.com/Ombi-app/Ombi/commit/f229d88bd744bc5253b5d3db69ae5ef22d014230)) + + + +## [4.35.2](https://github.com/Ombi-app/Ombi/compare/v4.35.1...v4.35.2) (2023-01-08) + + +### Bug Fixes + +* **database:** Just some tweaks, shouldn't notice any difference, maybe a less error in the log ([67fb992](https://github.com/Ombi-app/Ombi/commit/67fb9921c0c025025286eb6c0a9d09fd01b18465)) + + + +## [4.35.1](https://github.com/Ombi-app/Ombi/compare/v4.35.0...v4.35.1) (2023-01-06) + + +### Bug Fixes + +* **plex-watchlist:** Index out of bounds error ([8cd556e](https://github.com/Ombi-app/Ombi/commit/8cd556e268931596b9c1d1ae0ce533bfaaf330f4)) + + + +# [4.35.0](https://github.com/Ombi-app/Ombi/compare/v4.34.1...v4.35.0) (2023-01-04) + + +### Features + +* Add the option for header authentication to create users ([#4841](https://github.com/Ombi-app/Ombi/issues/4841)) ([e6c9ce5](https://github.com/Ombi-app/Ombi/commit/e6c9ce5ad0056608ecda8273fb8124ed292e2942)) + + + +## [4.34.1](https://github.com/Ombi-app/Ombi/compare/v4.34.0...v4.34.1) (2023-01-04) + + +### Bug Fixes + +* **plex-watchlist:** Lookup the ID from different sources when Plex doesn't contain the metadata ([#4843](https://github.com/Ombi-app/Ombi/issues/4843)) ([a2cc23b](https://github.com/Ombi-app/Ombi/commit/a2cc23b351c4a568c44e6c855f94db9f71ad084a)) + + + +# [4.34.0](https://github.com/Ombi-app/Ombi/compare/v4.33.1...v4.34.0) (2023-01-04) + + +### Features + +* Radarr tags ([#4815](https://github.com/Ombi-app/Ombi/issues/4815)) ([6fa5064](https://github.com/Ombi-app/Ombi/commit/6fa506491fe867cdeef9df79991ae49319d71c3d)) + + + +## [4.33.1](https://github.com/Ombi-app/Ombi/compare/v4.33.0...v4.33.1) (2022-12-22) + + +### Bug Fixes + +* **plex:** Added the watchlist request whole show back into the settings ([10701c4](https://github.com/Ombi-app/Ombi/commit/10701c4a0b6190eebb75c5d8b18224f3d0bc8502)) + + + +# [4.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01) + + +### Features + +* Angular 15 and Dependency upgrades ([#4818](https://github.com/Ombi-app/Ombi/issues/4818)) ([4816acf](https://github.com/Ombi-app/Ombi/commit/4816acf6f94443d23ebef6091d4cfcbca580f9ca)) + + + +## [4.32.3](https://github.com/Ombi-app/Ombi/compare/v4.32.2...v4.32.3) (2022-11-24) + + +### Bug Fixes + +* **sonarr:** V4 actually works this time around ([f62e70f](https://github.com/Ombi-app/Ombi/commit/f62e70fc493c7971da5e4508ce10522f5df0bbf7)) + + + +## [4.32.2](https://github.com/Ombi-app/Ombi/compare/v4.32.1...v4.32.2) (2022-11-23) + + +### Bug Fixes + +* **sonarr:** :bug: Sonarr V4 should work now ([#4810](https://github.com/Ombi-app/Ombi/issues/4810)) ([37655af](https://github.com/Ombi-app/Ombi/commit/37655aff9d3d133b42f5664bc9445d6571e966d6)) + + + +## [4.32.1](https://github.com/Ombi-app/Ombi/compare/v4.32.0...v4.32.1) (2022-11-21) + + +### Bug Fixes + +* **plex:** :bug: Fixed the issue where you couldn't add a new server on a fresh setup after the settings page rework ([187b18d](https://github.com/Ombi-app/Ombi/commit/187b18d5c01f6a13831e4a410b5d7c349e27d847)) + + + +# [4.32.0](https://github.com/Ombi-app/Ombi/compare/v4.31.0...v4.32.0) (2022-11-18) + + +### Bug Fixes + +* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4801](https://github.com/Ombi-app/Ombi/issues/4801)) ([4692003](https://github.com/Ombi-app/Ombi/commit/46920032baed04675b2ffbe1700afdc0740a4ac4)) + + +### Features + +* **plex:** Rework the Plex Settings page ([#4805](https://github.com/Ombi-app/Ombi/issues/4805)) ([1b8c47f](https://github.com/Ombi-app/Ombi/commit/1b8c47f3163f618851d4904732cb07015e1e93ff)) + + + +# [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.7](https://github.com/Ombi-app/Ombi/compare/v4.27.6...v4.27.7) (2022-10-07) + + +### Bug Fixes + +* Fixes default image for recently requested items. ([#4767](https://github.com/Ombi-app/Ombi/issues/4767)) ([2e6f35f](https://github.com/Ombi-app/Ombi/commit/2e6f35f89abb3dd3685ec8289f8620c7ef7072cd)) + + + +## [4.27.6](https://github.com/Ombi-app/Ombi/compare/v4.27.5...v4.27.6) (2022-10-01) + + +### Bug Fixes + +* **notifications:** Fixed the error when sending multiple test notifications. Added more logging when Discord complains the message is invalid ([fc14780](https://github.com/Ombi-app/Ombi/commit/fc14780bd354483119ddcbb55a8c382e1890a783)) + + + +## [4.27.5](https://github.com/Ombi-app/Ombi/compare/v4.27.4...v4.27.5) (2022-09-30) + + +### Bug Fixes + +* **importer:** 🐛 Allow you to only import Plex Admins without the Plex Users ([8c9ad9b](https://github.com/Ombi-app/Ombi/commit/8c9ad9b414fdc6c88bdb911d6057ae5d38783b98)) + + + +## [4.27.4](https://github.com/Ombi-app/Ombi/compare/v4.27.3...v4.27.4) (2022-09-30) + + + +## [4.27.3](https://github.com/Ombi-app/Ombi/compare/v4.27.2...v4.27.3) (2022-09-30) + + +### Bug Fixes + +* **availability:** 🐛 Fixed a issue with the availability checker after the previous update. Added full test coverage around that area ([28e2480](https://github.com/Ombi-app/Ombi/commit/28e248046ad56390595f84172bbd5f5961325b4d)) + + + +## [4.27.2](https://github.com/Ombi-app/Ombi/compare/v4.27.1...v4.27.2) (2022-09-29) + + +### Bug Fixes + +* **sonarr:** :bug: Cleaned up and removed Sonarr v3 option, sonarr v3 is now the default. This allows us to get ready for the upcoming Sonarr v4 ([#4764](https://github.com/Ombi-app/Ombi/issues/4764)) ([2cddec7](https://github.com/Ombi-app/Ombi/commit/2cddec759004b6490f686ff74cb092238e3dc946)) + + + +## [4.27.1](https://github.com/Ombi-app/Ombi/compare/v4.27.0...v4.27.1) (2022-09-20) + + +### Bug Fixes + +* **plex:** stop the plex sync from deleting episodes when we can't find the plex key ([66b05e5](https://github.com/Ombi-app/Ombi/commit/66b05e5a85dbfe1fec5f9366e80987f2cfa1f4fe)) + + + +# [4.27.0](https://github.com/Ombi-app/Ombi/compare/v4.26.0...v4.27.0) (2022-09-14) + + +### Features + +* Recently requested improvements ([#4755](https://github.com/Ombi-app/Ombi/issues/4755)) ([ff04d87](https://github.com/Ombi-app/Ombi/commit/ff04d875343604c77c391bf55d0968977e480281)) + + + +# [4.26.0](https://github.com/Ombi-app/Ombi/compare/v4.25.1...v4.26.0) (2022-09-07) + + +### Features + +* **notifications:** Add more curly variables for partially available notification ([66aa101](https://github.com/Ombi-app/Ombi/commit/66aa101019c4c4b34e186db9d303049d02b9c781)) + + + +## [4.25.1](https://github.com/Ombi-app/Ombi/compare/v4.25.0...v4.25.1) (2022-09-07) + + +### Bug Fixes + +* **webhook:** Remove added trailing slash from webhook URL [#4710](https://github.com/Ombi-app/Ombi/issues/4710) ([369eb33](https://github.com/Ombi-app/Ombi/commit/369eb339171671101be219486e2aab27a20f3d74)) + + + +# [4.25.0](https://github.com/Ombi-app/Ombi/compare/v4.24.0...v4.25.0) (2022-08-23) + + +### Bug Fixes + +* fixed stats controller ([#4742](https://github.com/Ombi-app/Ombi/issues/4742)) ([47ea64b](https://github.com/Ombi-app/Ombi/commit/47ea64b5a401770f1943b575ca40f84d515e96b3)) + + +### Features + +* Watchlist history errors([#4741](https://github.com/Ombi-app/Ombi/issues/4741)) ([c222f1a](https://github.com/Ombi-app/Ombi/commit/c222f1a945e944ef34e68cad2b61f40e57cab823)) + + + +# [4.24.0](https://github.com/Ombi-app/Ombi/compare/v4.23.2...v4.24.0) (2022-08-22) + + +### Features + +* add crew on movie page ([#4722](https://github.com/Ombi-app/Ombi/issues/4722)) ([1d53261](https://github.com/Ombi-app/Ombi/commit/1d532613823804b25984bd1d223d081a54ad143d)) + + + +## [4.23.2](https://github.com/Ombi-app/Ombi/compare/v4.23.1...v4.23.2) (2022-08-22) + + +### Bug Fixes + +* Fix conflicting property name for Swagger ([#4733](https://github.com/Ombi-app/Ombi/issues/4733)) ([d661f32](https://github.com/Ombi-app/Ombi/commit/d661f32e8a9e105faab6380b4b7b642896b98163)) + + + +## [4.23.1](https://github.com/Ombi-app/Ombi/compare/v4.23.0...v4.23.1) (2022-08-12) + + +### Bug Fixes + +* Localize recently requested on discover page ([#4729](https://github.com/Ombi-app/Ombi/issues/4729)) ([bf65c76](https://github.com/Ombi-app/Ombi/commit/bf65c76ff9ce38f65a9e5feb872734e8d8e35eb6)) + + + +# [4.23.0](https://github.com/Ombi-app/Ombi/compare/v4.22.5...v4.23.0) (2022-08-09) + + +### Bug Fixes + +* Log Microsoft warnings to log file ([#4723](https://github.com/Ombi-app/Ombi/issues/4723)) ([26ac75f](https://github.com/Ombi-app/Ombi/commit/26ac75f0c223c2a91e3471797ae46ede3fde89cc)) + + +### Features + +* ✨ Recently Requested on Discover Page ([#4387](https://github.com/Ombi-app/Ombi/issues/4387)) ([44d38fb](https://github.com/Ombi-app/Ombi/commit/44d38fbaae521dbb467b61c7471b2384015ac52e)) + + + +## [4.22.4](https://github.com/Ombi-app/Ombi/compare/v4.22.3...v4.22.4) (2022-08-04) + + +### Bug Fixes + +* :bug: Fixed missing externals ([#4712](https://github.com/Ombi-app/Ombi/issues/4712)) ([fcc1eaa](https://github.com/Ombi-app/Ombi/commit/fcc1eaaa377683dcdc81d62a2a688fb0c4490c7b)) +* fixed trakt image not loading when base url present ([#4711](https://github.com/Ombi-app/Ombi/issues/4711)) ([f102dcf](https://github.com/Ombi-app/Ombi/commit/f102dcf751c2eb62ebfe30f9f8e4b2ad863c3b0d)) +* **translations:** 🌐 New translations from Crowdin [skip ci] ([#4713](https://github.com/Ombi-app/Ombi/issues/4713)) ([ff142b0](https://github.com/Ombi-app/Ombi/commit/ff142b09abbb2f9540387284222552e6e12639fe)) + + + +## [4.22.3](https://github.com/Ombi-app/Ombi/compare/v4.22.2...v4.22.3) (2022-07-28) + + +### Bug Fixes + +* Override Sonarr V3 Profiles endpoint ([#4678](https://github.com/Ombi-app/Ombi/issues/4678)) ([875da95](https://github.com/Ombi-app/Ombi/commit/875da959f353119b05138d68ee6d32a49e14b91e)) + + + +## [4.22.2](https://github.com/Ombi-app/Ombi/compare/v4.22.1...v4.22.2) (2022-07-25) + + +### Bug Fixes + +* fixed an issue where I broke images for some users ([81ddc85](https://github.com/Ombi-app/Ombi/commit/81ddc8553b9094c3f6843b036daebb2eb9262e00)) + + + +## [4.22.1](https://github.com/Ombi-app/Ombi/compare/v4.22.0...v4.22.1) (2022-07-25) + + +### Bug Fixes + +* **discover:** :bug: Created new Image component to handle 429's from TMDB ([#4698](https://github.com/Ombi-app/Ombi/issues/4698)) and fixed [#4635](https://github.com/Ombi-app/Ombi/issues/4635) ([#4699](https://github.com/Ombi-app/Ombi/issues/4699)) ([f22d3da](https://github.com/Ombi-app/Ombi/commit/f22d3da765799365455b919027f7563e52b347c3)) + + + +# [4.22.0](https://github.com/Ombi-app/Ombi/compare/v4.21.2...v4.22.0) (2022-07-22) + + +### Features + +* **discover:** ✨ Added infinite scroll on advanced search results ([898bc89](https://github.com/Ombi-app/Ombi/commit/898bc89fa78245c1f3de9481f6c724f087a16e39)) + + + +## [4.21.2](https://github.com/Ombi-app/Ombi/compare/v4.21.1...v4.21.2) (2022-07-22) + + +### Bug Fixes + +* Landing and Login page improvements ([#4690](https://github.com/Ombi-app/Ombi/issues/4690)) ([6d423b5](https://github.com/Ombi-app/Ombi/commit/6d423b5447c52c5e59d8d2bd92a23b47468eb736)) + + + +## [4.21.1](https://github.com/Ombi-app/Ombi/compare/v4.21.0...v4.21.1) (2022-07-11) + + +### Bug Fixes + +* **images:** Retry images with a backoff when we get a Too Many requests from TheMovieDb [#4685](https://github.com/Ombi-app/Ombi/issues/4685) ([3f1f35d](https://github.com/Ombi-app/Ombi/commit/3f1f35df3164db6739691cdda8f925c296239791)) + + + +# [4.21.0](https://github.com/Ombi-app/Ombi/compare/v4.20.4...v4.21.0) (2022-06-22) + + +### Features + +* Upgrade to Angular14 ([#4668](https://github.com/Ombi-app/Ombi/issues/4668)) ([b9d55a4](https://github.com/Ombi-app/Ombi/commit/b9d55a469b412558cbf67c1e25db7fdda5964cd8)) + + +### Performance Improvements + +* stop populating obsolete subscribe fields ([#4625](https://github.com/Ombi-app/Ombi/issues/4625)) ([9a73463](https://github.com/Ombi-app/Ombi/commit/9a734637665f671b17c2bb440d93b35a891c142b)) + + + +## [4.20.4](https://github.com/Ombi-app/Ombi/compare/v4.20.3...v4.20.4) (2022-06-15) + + +### Bug Fixes + +* fixed build ([f877921](https://github.com/Ombi-app/Ombi/commit/f8779219146051ea74f8b6408658ff7975afb88b)) + + + +## [4.20.3](https://github.com/Ombi-app/Ombi/compare/v4.20.2...v4.20.3) (2022-06-05) + + +### Bug Fixes + +* **plex:** 🐛 Fixed an issue with the Plex Sync ([ab1a11a](https://github.com/Ombi-app/Ombi/commit/ab1a11af78efbe9d37bd55aa80a640796c138a98)) + + + +## [4.20.2](https://github.com/Ombi-app/Ombi/compare/v4.20.1...v4.20.2) (2022-06-03) + + +### Bug Fixes + +* :bug: Fixed the Request on Behalf of having blanks ([#4667](https://github.com/Ombi-app/Ombi/issues/4667)) ([7dd9b1c](https://github.com/Ombi-app/Ombi/commit/7dd9b1cac07f571dd35b362544e4fe0226c4b817)) + + + +## [4.20.1](https://github.com/Ombi-app/Ombi/compare/v4.20.0...v4.20.1) (2022-05-27) + + +### Bug Fixes + +* added media type tag to media type text ([#4638](https://github.com/Ombi-app/Ombi/issues/4638)) ([fe501d3](https://github.com/Ombi-app/Ombi/commit/fe501d34a0c36ac9f000b107eca49dbc6694d006)) +* **API:** Fix pagination in some edge cases ([#4649](https://github.com/Ombi-app/Ombi/issues/4649)) ([a70bf8f](https://github.com/Ombi-app/Ombi/commit/a70bf8f46c76d74c9dfdf908c53bd9955ca0a35d)) +* **discover:** Carousel touch not working when scrolling page and recommendations and similar movie navigation ([#4633](https://github.com/Ombi-app/Ombi/issues/4633)) ([d5ef1d5](https://github.com/Ombi-app/Ombi/commit/d5ef1d53e5f77d19dba8b8059c80b538a3e14f2a)) +* Improve Swagger documentation ([#4652](https://github.com/Ombi-app/Ombi/issues/4652)) ([181892b](https://github.com/Ombi-app/Ombi/commit/181892bcfe88e6d76febf49ef57745d04552d08e)) +* Missing Poster broken link fix ([#4637](https://github.com/Ombi-app/Ombi/issues/4637)) ([4070f0d](https://github.com/Ombi-app/Ombi/commit/4070f0d093b1c92487a1c80cabad8283a9650f51)) +* **sickrage:** Fixed issue with incorrect handling of SiCKRAGE episode results returned during episode status changes, now expects array of objects from data path if present ([#4648](https://github.com/Ombi-app/Ombi/issues/4648)) ([6d16442](https://github.com/Ombi-app/Ombi/commit/6d16442d4d714920367df065a3ced42b729f4233)) +* **sync:** Emby+Jellyfin - sync multi-episode files of 3+ episodes ([bd8fd89](https://github.com/Ombi-app/Ombi/commit/bd8fd890554c9d85d6da4d2cee813e82ce698e52)) + + + +# [4.20.0](https://github.com/Ombi-app/Ombi/compare/v4.19.1...v4.20.0) (2022-04-28) + + +### Features + +* **discover:** Show more relevant shows in upcoming TV ([8357819](https://github.com/Ombi-app/Ombi/commit/8357819b53b8c675c0b246d7006b5a778bdba33f)) + + + +## [4.19.1](https://github.com/Ombi-app/Ombi/compare/v4.19.0...v4.19.1) (2022-04-27) + + + +# [4.19.0](https://github.com/Ombi-app/Ombi/compare/v4.18.0...v4.19.0) (2022-04-27) + + +### Features + +* **sync:** Detect reidentified movies in Emby and Jellyfin ([5938077](https://github.com/Ombi-app/Ombi/commit/5938077d82a5357f79c07b218b3986557a5816e8)) +* **sync:** Detect reidentified series in Emby and Jellyfin ([9096e91](https://github.com/Ombi-app/Ombi/commit/9096e91d55d268819bce22831f8a8b27f2a1776b)) + + + +# [4.18.0](https://github.com/Ombi-app/Ombi/compare/v4.17.0...v4.18.0) (2022-04-26) + + +### Bug Fixes + +* **discover:** Fix cache mix up ([03d9422](https://github.com/Ombi-app/Ombi/commit/03d94220c7eaafb50c6c80a6ed1150794b873ac3)) +* **discover:** Fix new trending feature detection ([6794b88](https://github.com/Ombi-app/Ombi/commit/6794b887f6544fb41528bdd9728b7824b65e47ee)) +* **settings:** Allow toggling features when there are more than one ([a373359](https://github.com/Ombi-app/Ombi/commit/a373359ae8e6bad42b558a6e01a8ff2840d3bbaa)) + + +### Features + +* **discover:** Add new trending source experimental feature ([1a0823c](https://github.com/Ombi-app/Ombi/commit/1a0823ca80559417c67323aaeaa1ef5243e98031)) +* **discover:** Default trending source to new logic ([4f12939](https://github.com/Ombi-app/Ombi/commit/4f12939e22020a67a5ee75e2907923faea136e8d)) + + + +# [4.17.0](https://github.com/Ombi-app/Ombi/compare/v4.16.17...v4.17.0) (2022-04-25) + + + +## [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) + + +### Features + +* **discover:** Add original language filter ([ef7ec86](https://github.com/Ombi-app/Ombi/commit/ef7ec861d8aede2a4817752c990617f583805391)) + + + +## [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.35.10](https://github.com/Ombi-app/Ombi/compare/v4.35.9...v4.35.10) (2023-02-25) + + + +## [4.35.9](https://github.com/Ombi-app/Ombi/compare/v4.35.8...v4.35.9) (2023-02-24) + + + +## [4.22.5](https://github.com/Ombi-app/Ombi/compare/v4.22.4...v4.22.5) (2022-08-05) + + + +## [4.35.8](https://github.com/Ombi-app/Ombi/compare/v4.35.7...v4.35.8) (2023-02-17) + + +### Bug Fixes + +* **plex-oauth:** 🐛 Fixed an issue where using OAuth you could log in as a Ombi Local user [#4835](https://github.com/Ombi-app/Ombi/issues/4835) ([4098da3](https://github.com/Ombi-app/Ombi/commit/4098da305aaea9dae9a552644268a4fed7204cfe)) + + + +## [4.35.7](https://github.com/Ombi-app/Ombi/compare/v4.35.6...v4.35.7) (2023-02-10) + + +### Bug Fixes + +* **wizard:** :bug: Stop access to the wizard when you have already setup ombi ([#4866](https://github.com/Ombi-app/Ombi/issues/4866)) ([353de98](https://github.com/Ombi-app/Ombi/commit/353de981a462e1753288d225ec4644a44a62d2bc)) + + + +## [4.35.6](https://github.com/Ombi-app/Ombi/compare/v4.35.5...v4.35.6) (2023-01-31) + + +### Bug Fixes + +* Fixed the issue where the login page is still present after logging in with oauth ([aca4ee3](https://github.com/Ombi-app/Ombi/commit/aca4ee37915a28200e5233be3dc711ccc4a5aee9)) + + + +## [4.35.5](https://github.com/Ombi-app/Ombi/compare/v4.35.4...v4.35.5) (2023-01-24) + + +### Bug Fixes + +* **radarr-settings:** 🐛 Fixed a typo ([4a50a00](https://github.com/Ombi-app/Ombi/commit/4a50a00d4729d99f4359874b9af4dbc58a0c220b)) + + + +## [4.35.4](https://github.com/Ombi-app/Ombi/compare/v4.35.3...v4.35.4) (2023-01-22) + + +### Bug Fixes + +* **discover:** :bug: Fixed the default poster not taking into account the base url in some scenarios [#4845](https://github.com/Ombi-app/Ombi/issues/4845) ([8eda250](https://github.com/Ombi-app/Ombi/commit/8eda250367953183daec03ccb5cdf9fe94275b27)) +* **Hide music from navbar and request list when not enabled:** :bug: ([5123a76](https://github.com/Ombi-app/Ombi/commit/5123a76954e9f81d58c05e31afc7a29aec19cb7a)) + + + +## [4.35.3](https://github.com/Ombi-app/Ombi/compare/v4.35.2...v4.35.3) (2023-01-13) + + +### Bug Fixes + +* **#4847:** Invalid Discord request fixed, also fixed an issue where App Only users would not show as logged in on the user management page ([#4848](https://github.com/Ombi-app/Ombi/issues/4848)) ([f229d88](https://github.com/Ombi-app/Ombi/commit/f229d88bd744bc5253b5d3db69ae5ef22d014230)) + + + +## [4.35.2](https://github.com/Ombi-app/Ombi/compare/v4.35.1...v4.35.2) (2023-01-08) + + +### Bug Fixes + +* **database:** Just some tweaks, shouldn't notice any difference, maybe a less error in the log ([67fb992](https://github.com/Ombi-app/Ombi/commit/67fb9921c0c025025286eb6c0a9d09fd01b18465)) + + + +## [4.35.1](https://github.com/Ombi-app/Ombi/compare/v4.35.0...v4.35.1) (2023-01-06) + + +### Bug Fixes + +* **plex-watchlist:** Index out of bounds error ([8cd556e](https://github.com/Ombi-app/Ombi/commit/8cd556e268931596b9c1d1ae0ce533bfaaf330f4)) + + + +# [4.35.0](https://github.com/Ombi-app/Ombi/compare/v4.34.1...v4.35.0) (2023-01-04) + + +### Features + +* Add the option for header authentication to create users ([#4841](https://github.com/Ombi-app/Ombi/issues/4841)) ([e6c9ce5](https://github.com/Ombi-app/Ombi/commit/e6c9ce5ad0056608ecda8273fb8124ed292e2942)) + + + +## [4.34.1](https://github.com/Ombi-app/Ombi/compare/v4.34.0...v4.34.1) (2023-01-04) + + +### Bug Fixes + +* **plex-watchlist:** Lookup the ID from different sources when Plex doesn't contain the metadata ([#4843](https://github.com/Ombi-app/Ombi/issues/4843)) ([a2cc23b](https://github.com/Ombi-app/Ombi/commit/a2cc23b351c4a568c44e6c855f94db9f71ad084a)) + + + +# [4.34.0](https://github.com/Ombi-app/Ombi/compare/v4.33.1...v4.34.0) (2023-01-04) + + +### Features + +* Radarr tags ([#4815](https://github.com/Ombi-app/Ombi/issues/4815)) ([6fa5064](https://github.com/Ombi-app/Ombi/commit/6fa506491fe867cdeef9df79991ae49319d71c3d)) + + + +## [4.33.1](https://github.com/Ombi-app/Ombi/compare/v4.33.0...v4.33.1) (2022-12-22) + + +### Bug Fixes + +* **plex:** Added the watchlist request whole show back into the settings ([10701c4](https://github.com/Ombi-app/Ombi/commit/10701c4a0b6190eebb75c5d8b18224f3d0bc8502)) + + + +# [4.33.0](https://github.com/Ombi-app/Ombi/compare/v4.32.3...v4.33.0) (2022-12-01) + + +### Features + +* Angular 15 and Dependency upgrades ([#4818](https://github.com/Ombi-app/Ombi/issues/4818)) ([4816acf](https://github.com/Ombi-app/Ombi/commit/4816acf6f94443d23ebef6091d4cfcbca580f9ca)) + + + +## [4.32.3](https://github.com/Ombi-app/Ombi/compare/v4.32.2...v4.32.3) (2022-11-24) + + +### Bug Fixes + +* **sonarr:** V4 actually works this time around ([f62e70f](https://github.com/Ombi-app/Ombi/commit/f62e70fc493c7971da5e4508ce10522f5df0bbf7)) + + + +## [4.32.2](https://github.com/Ombi-app/Ombi/compare/v4.32.1...v4.32.2) (2022-11-23) + + +### Bug Fixes + +* **sonarr:** :bug: Sonarr V4 should work now ([#4810](https://github.com/Ombi-app/Ombi/issues/4810)) ([37655af](https://github.com/Ombi-app/Ombi/commit/37655aff9d3d133b42f5664bc9445d6571e966d6)) + + + diff --git a/README.md b/README.md index 098c7f6d0..50c962495 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,6 @@ Here are some of the features Ombi has: Jamie - - - Roxedus -
- Roxedus -
- twanariens @@ -92,13 +85,6 @@ Here are some of the features Ombi has: Twan Ariens - - - Drewster727 -
- Drew -
- sephrat @@ -112,8 +98,7 @@ Here are some of the features Ombi has:
Anojh Thayaparan
- - + Magikarplvl4 @@ -127,7 +112,8 @@ Here are some of the features Ombi has:
James Carty
- + + smcpeck @@ -155,8 +141,7 @@ Here are some of the features Ombi has:
Dhruv Bhavsar
- - + joshuaboniface @@ -170,7 +155,8 @@ Here are some of the features Ombi has:
Bruvv
- + + louis-lau @@ -198,8 +184,7 @@ Here are some of the features Ombi has:
Julien Loir
- - + ProtoJazz @@ -213,7 +198,8 @@ Here are some of the features Ombi has:
Avi
- + + kitzin @@ -241,8 +227,7 @@ Here are some of the features Ombi has:
Taylor Buchanan
- - + fservida @@ -256,7 +241,8 @@ Here are some of the features Ombi has:
Patrick Collins
- + + xweskingx @@ -284,8 +270,7 @@ Here are some of the features Ombi has:
Stephen Panzer
- - + aptalca @@ -299,7 +284,8 @@ Here are some of the features Ombi has:
Dr3amer
- + + mhann @@ -314,6 +300,13 @@ Here are some of the features Ombi has: Ombi-bot + + + Fire-Swan +
+ Fire-Swan +
+ snyk-bot @@ -327,15 +320,15 @@ Here are some of the features Ombi has:
Andrew Metzger
- - + au5ton
Austin Jackson
- + + D34DC3N73R @@ -370,15 +363,15 @@ Here are some of the features Ombi has:
Jack Steel
- - + jpeters
Jeffrey Peters
- + + MariusSchiffer @@ -413,14 +406,21 @@ Here are some of the features Ombi has:
Javier Pastor
- - + AbeKline
Abe Kline
+ + + + + aj3x +
+ Alexander Russell +
@@ -522,6 +522,13 @@ Here are some of the features Ombi has: Fish2 + + + Grygon +
+ Grygon +
+ ketsapiwiq @@ -535,15 +542,15 @@ Here are some of the features Ombi has:
Haries Ramdhani
- + + comigor
Igor Borges
- - + ImgBotApp @@ -578,15 +585,15 @@ Here are some of the features Ombi has:
Joe Harvey
- + + jonbloom
Jon Bloom
- - + jonocairns @@ -608,13 +615,21 @@ Here are some of the features Ombi has: Kyle Lucy + + + janderedev +
+ Lea +
+ Lixumos
Lightkeeper
- + + Lucane @@ -628,8 +643,7 @@ Here are some of the features Ombi has:
Madeleine Schönemann
- - + marleypowell @@ -657,7 +671,8 @@ Here are some of the features Ombi has:
Micky
- + + mvicomoya @@ -671,8 +686,7 @@ Here are some of the features Ombi has:
Nathan Miller
- - + cqxmzz @@ -680,13 +694,6 @@ Here are some of the features Ombi has: Qiming Chen - - - randallbruder -
- Randall Bruder -
- rob1998 @@ -707,15 +714,15 @@ Here are some of the features Ombi has:
Sean Callinan
- + + shoghicp
Shoghi
- - + Teifun2 @@ -750,15 +757,15 @@ Here are some of the features Ombi has:
Torkil
- + + bybeet
Travis Bybee
- - + Xirg @@ -793,15 +800,15 @@ Here are some of the features Ombi has:
Michael DiStaula
- + + baikunz
Dorian ALKOUM
- - + echel0n @@ -836,6 +843,14 @@ Here are some of the features Ombi has:
Mkgeeky
+ + + + + ryan-c44 +
+ Ryan-c44 +
@@ -843,8 +858,7 @@ Here are some of the features Ombi has:
Sirmarv
- - + tdorsey diff --git a/assets/Ombi-icon.ai b/assets/Ombi-icon.ai new file mode 100644 index 000000000..da64fd25f --- /dev/null +++ b/assets/Ombi-icon.ai @@ -0,0 +1,1233 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[8 0 R 5 0 R 6 0 R 9 0 R 7 0 R 44 0 R 41 0 R 42 0 R 45 0 R 43 0 R 82 0 R 79 0 R 80 0 R 83 0 R 81 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + Ombi-icon copy + + + Adobe Illustrator CC 22.0 (Windows) + 2018-01-14T01:11:22+02:00 + 2018-03-23T15:05:02Z + 2018-03-23T15:05:02Z + + + + 256 + 232 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgA6AEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYqh9R1Kw02ylvtQuI7SzgXlNcTMERR7scVeEeeP+coreGSSz8n2YuSNv0neBljPvHCOLn5uR/q 4q8d1781PzC12Rm1DXbrgx/uIHNvF8vTh4Kae4xVi0kkkjl5GLuerMSSabdTiq3FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYqj9N1/XdLYPpmo3Viw6G2mkiPWv7BGKvRvKv/OR/5gaO6R6lJHrlmNmj uQEmA/yZowDX3cNir6B/L783fKPnWMRWMxtdVVeUumXBCy7dTGekijxXfxAxVm2KuxV2KuxV2Kux V2KuxV2KuxVLPMvmTSPLei3OsatN6FlarVz1ZmOyoi92Y7AYq+O/zM/NPXvPOps9w7W2jwuTY6Yr fAg6B3p9uQj9o9O1BirCsVdirsVdirsVdirsVRtlo2r31PqlnNOD+0kbFf8AgqUyrJnhD6pANkMM 5cgSnEH5d+bZgCbMRKe8kkY/AMT+GYku08A/i+wuTHs/MeiKX8rfM5FSbdT4GQ1/BTlZ7Xw+fyZj szL5LZPyw80pXikMn+rIP+NguEdrYT3/ACQezcvkl915H812wJfTpGA7xFZfwQscvh2hglykPuap aLKP4Unntri3f07iJ4ZB+xIpU/ccyozEhYNuNKJHMKWSQ7FXYq7FXYq7FVS3ubi2uI7i2laG4hYP FLGSrqymoZWG4IOKvqH8kPzv/wASen5c8xyKuuqtLO8NFW6VR9luwlA32+18+qr2fFXYq7FXYq7F XYq7FXYq7FXyL+fn5kSeafND6XZSk6Ho7tFAFPwzTj4ZZj47/CntuPtHFXluKuxV2KuxV2Kqtta3 N1MsFtE80z/ZjQFmP0DIzmIiyaDKMTI0BZZton5WX04WXVZhaod/Qjo8n0t9lfxzT6jtiMdoDi8+ js8PZcjvM0zbS/J3lzTQpgs0eVf93TD1Hr4gtUD6KZqM2uzZOctvLZ2eLR44cgnQoBQdMw3Jbrir q4q6uKuriqlcW1tcxmK4iSaM9UkUOv3GuSjMxNg0iUQRRFsX1b8tfLt6C1srWMx6NEapX3Rv+NSM 2OHtbLDn6h5uDl7Nxy5eksE13yFrukq03AXdqu5nhqaD/KT7Q/V75utN2liy7fTLzdVn0OTHvzHk xvNg4TsVdirsVdiqpbXFxbXEVzbyNDcQsJIpUJVldTVWUjcEHFX2l+UX5gx+dfKMN9KVXVbUi31O Jdv3qjaQDssg+Ie9R2xVm2KuxV2KuxV2KuxV2KsM/ODzU/ln8vtV1CByl5KgtbNgaESzngGU+KKS /wBGKvibFXYq7FXYq7FWS+VvI+o62VnkrbafXedhu9OojHf59M12s7Rhh2G8+79bnaXQyy78ovVd G0HStHg9GxgEdRR5TvI/+s3X+Gc1n1M8puRd9h08MYqITCuY7c6uKurirq4q6uKurirq4q6uKuri rq4qxXzN5A0vVg9xagWd+anmopG5/wAtR+sfjmz0nac8W0vVFwNT2fDJuNpPLNV0jUNKu2tb6IxS jdT1Vl/mU9xnS4c8MseKJsOgy4ZYzUggsua3Yq7FXYq9V/5xw81Po/5gR6bI9LPXIzbSKegmQGSF vnUFB/rYq+t8VdirsVdirsVdirsVeD/85Y6i8ei+X9NBPC5uZ7lh2rbxqg/6iDir5rxV2KuxV2Ks 58keRProTUtVQi02a3tzsZP8pv8AI/X8uul7R7S4PRD6up7v2u10Og4/XPl0He9ORURFRAFRQAqj YADYAAZzZNu+ApdXArq4q6uKurirq4q6uKurirq4q6uKurirq4q6uKpfrWiafrFmbW9j5L1jkGzo 38ynL9PqZ4pcUWnPgjkjUnjnmPy5faFe/V7ijxPUwTr9l1H6iO4zrtJq45o2OfUPNanTSxSopTmU 47sVdiqYeX9TfSte07U0NGsbqG4B3/3VIH7fLFX35irsVdirsVdirsVdir5x/wCcsz/uQ8tjt6V1 /wASixV4DirsVdirMfIXlAanP+kL1P8AQIW/doekrjt/qr3+7xzUdp6/wxwR+o/Y7Ps/R+IeKX0j 7XqwoBQCgHQZy70Lq4q6uKurirq4q6uKurirq4q6uKurirq4q6uKurirq4q6uKoDWtHstXsHs7ta q26OPtI3Zl98v0+olinxRas+COSPCXius6Rd6TqEtldLSSM/Cw+y6n7LL7HOy0+eOWAlF5XNhljk YlA5c1OxV2Kv0MxV2KuxV2KuxV2KuxV84/8AOWf/AB0PLf8Axhuv+JRYq8BxV2Kpl5e0WfWdViso 6qjHlPIP2Ix9pv4D3zG1epGHGZH4e9v02A5ZiIe3Wlrb2ltFbW6COGFQkaDsBnFTyGcjI8y9ZCAi AByCrXIsnVxV1cVTPS/LeuaovOxtHlj/AN+miJt4M5VT9GZeDQ5souMdu9xc2sxY9id0wk/L7zYi FvqYem5CyRk7e3LMmXY+cDkD8WiPamEnqPgkNzbXNtM0NzE8My/ajkUqwr7HNdkxygakKLnQyRmL ibClXIM3VxV1cVdXFXVxV1cVdXFXVxV1cVdXFWN+efLa6xpZkhWt/agvAR1ZerR/T2982PZus8Gd H6Zc/wBbg6/S+LCx9QePZ17zDsVdir9DMVdirsVdirsVdirsVfOP/OWf/HQ8t/8AGG6/4lFirwHF XYq9Y/LvQxYaOL2VaXN9R6nqIv2B9P2s5PtfU+Jk4R9Mfv6vSdmafgx8R5y+5llc1TsnVxV1cVZh +X/lOLVrh7+9TlYW7cVjPSSSlaH/ACVqCfH783XZWgGQ8c/pHLzdT2lrDD0R5l6dd3lhp1r6tzLH bW0YCgsQqig2VR8hsBnSznGAsmg6GMTI0BZS+w84+Wr+5W2tr5Wnc0RGV4+R8AXVQT7Zj4tbhyGo yFt2TSZYC5R2XeY/LVhrlmYpxwuFH+j3IHxIf4r4jDqtLDNGpfA9yNPqJYpWHjeraTfaVfPZ3kfC VNwRurKejKe4OcdqdNLDLhk9Tgzxyx4ooOuUNzq4q6uKurirq4q6uKurirq4q6uKuriryP8AMHQx p2tG4hXjbX1ZVA6B6/vF+81+nOt7J1PiYqP1R2/U812lp/DyWOUvwWL5tHXOxV+hmKuxV2KuxV2K uxV2KvnH/nLP/joeW/8AjDdf8SixV4DiqYaDpp1PWLWy/ZlcepTsi/E5/wCBBzH1Wbwscpdwb9Ni 8TII973FFVFCKAqqAFA6ADOGJt68Cm64Eurirq4q9r8jW6QeVdPVaVdDIx8S7Ft/vpnb6CAjggB3 ffu8jrJGWaV9/wBzAPzK1G5n8yy2jt+4s1RYU7VdFdmp4nlT6M5/tnPKWXg6Rd32VhAx8XWTE65q LdnT0vyL56+senpWqyf6RstrdMf7zwRz/N4Hv8+vT9m9pcfon9XQ9/7XntfoOD1w+n7v2Mo8yeW7 HXbEwTjhMlTb3AFWRj+tT3GbLVaWOaPDL4HucDT6iWKVh4zq2k32k3z2d6nCVNwRurKejKe4Ocdq dPPDLhk9Vgzxyx4oq/lnTrfUtds7K45ejM5D8TQ0Cluv0YdHiGTLGJ5Fjqshx4zIcws8w2MGn61e WUBYwwSFU5GrU9yAMGqxDHllEcgU6bIZ4xI8yl1cx291cVdXFXVxV1cVdXFXVxVj3nvSxqHl2cqK zWv+kR/7AfEP+Brmx7Lz+HmHdLZwe0cPHiPeN3j2di8s7FX6GYq7FXYq7FXYq7FXYq+cf+cs/wDj oeW/+MN1/wASixV4DirOPytsA9/d3zCogjEaH/KkNSR9C/jmi7cy1CMO838ncdj47kZdz0mucy9A 6uKurirq4q9c/LPVIrry8tnUevYuyOvfg7F1b8SPozsOyc4nhA6x2/U8v2lhMMpPSW6B/MTyfPeM dYsF5zogF1AB8TqvR18SBsR4ZR2roDk9cPqHTv8A2t3ZutGP0S+k/Y8xrnLPRLkjlkJEaM5HUIpa lfGlcvwabJk+gE005s8Mf1mreleRfPRnKaRqz0uR8FtcvsXI29OSv7fge/z69H2f2gSfCy7TH2/t +90Wt0QA8THvA/Z+xk/mXy3Za7YGCYBJ0BNtcD7SN/FT3H8c2Gq0sc0OGXwPc4Wm1EsUrDy/ydbT WvnWztp14zQzSRyL1oyowOcvocZhqhE8wS9DrJienMhyICzzPaXN55yvrW1jMs8s5VEXqTQZDWY5 T1MoxFkllpJiOCJOwAZDc22i+TtGaC4jjvtdvYyGVhyVFO3foo+9j+GwnDFpMdECWWQ+X7PvcGEs mqyWLjjj+Px3PPq5oHdurirq4q6uKurirq4q0yq6lWFVYUYHuDhBpBFvCdUszZajdWh/3RK8YJ7h TQH6Rne4MnHAS7w8Zmx8EzHuKFy1rfoZirsVdirsVdirsVdir5x/5yz/AOOh5b/4w3X/ABKLFXgO KvU/y0thF5faanxTzu1fZQFH4g5yfbeS81dwel7JhWK+8ssrmndo6uKurirq4qj9G1q+0e/S9s34 yLs6ndXQ9UYdwcydLqpYZ8Uf7XH1Gnjljwl7R5d8xWGu2AubY8ZFoLi3Jq0bHsfEHse/3jOz02ph mhxR/seV1GnlilwyYj588ic/U1bSY/j3a6tVH2vF0Hj4jNV2n2Zx3kxj1dR3/t+/389j2f2hw+if 09D3fsYj5fuIQkluTxlZi6/5Q4gfeKZPsXPA4+D+Ifi2HauGQycf8JRGqaWLkGaHa4H0BwOx9/A5 la/QDOLG0xyP6/xs0aPWHCaO8DzD1DydeXV55asZ7ok3HBkctXkfTdowWrvUhd8yNKZnGOP6urj6 gRGQ8P09Hmf5gVtvON1JbkxSfupAyHiQ/pr8QIpQ13zmu2Bw6ixsaBeg7L9WGjysoLy35jv9Iv5Z 7aFbq6uVMS+oCzcmIIK03JJ7d8xNHq54pkxHFI7ORqtLHLEAmgFa78s+c725kurmxnlnlPJ3YCpP 37fLLMmj1M5GUoyJLGGr08BwiQACBv8Ay7rthB695ZSwwggGRl+EE9KkdMoy6TLjFyiQG7HqcczU ZAlLa5jN7q4q6uKurirq4q6uKvJfzCtvR8zzuNhOkco/4HifxXOx7InxYB5WHlu1IVmPnTGs2br3 6GYq7FXYq7FXYq7FXYq+cf8AnLP/AI6Hlv8A4w3X/EosVeA4q9h8joE8rWIHdXY/TIxziu1Deol8 PuD1nZwrBH8dU9rmA5zq4q6uKurirq4qjtF1q/0e/S9sn4yLs6HdXU9VYdwcyNLqp4Z8Uf7XH1Om jljwye1eXPMdhrtgLq1PGRaC4tyatGx7HxB7Hv8AeM7TTamGaHFH+x5TUaeWKXDJi/nD8vJLu4Oo 6IFjuWPKa2rwDNX7cZ6K3jmv1nZplPxMR4Z/j8ebm6XXgR8PIOKH4/HklB0rXbK2RtVtTA7HiHDI 6tQV/YLAH2zY6eU5QHGOGTgZ4wEvQbimWg69Npk3FqvaOf3kfcH+Zff9eXtTHNU07XfNOvXt9p9l JJA0hSORgI04xgIPicqOVBUiucrrNLn1GeRETQ2325PR6XU4cGGIMhZ323a/wB5ytWW4itP3kRDq 0csfNSu4Io1a/LKz2RqI7ir8i2DtTBLY3XmFO485+dbaZobm8mhmT7UckaKw+YK5Xk1+qgalIg+7 9jOGi08xcQCPegNR806/qVuba9vXmgJBMZCqCRuK8QK5j5tblyCpSsN+LSYsZuMaKV1zFcl1cVdX FXVxV1cVdXFXmn5oR01e0kp9q341/wBV2P8AxtnUdhS/dSH9L9DznbI/eA+TDM3jqH6GYq7FXYq7 FXYq7FXYq+cf+cs/+Oh5b/4w3X/EosVeA4q9k8nOG8s6eR2jp9zEZxHaQrUT971+gN4Y+5OK5guY 6uKurirq4q6uKuriqP0XW7/R79L2yfjIuzod0dT1Vx3BzJ0uqnhlxR/tcfUaaOWPDJ6hpX5n+Xbu IfXC9hPT4kdWdCf8l0B/EDOowdr4Zjc8J8/1vO5uy8sDsOIeX6kZqPnTyY1q8VxepPG67xoruT4U 4jY+G+ZE+0MERZmPhv8Ac0Q0OaRoRP3fewmGeK7SW6tI5fqKymKKaVQCSAGoeJIrQ5PS6qOeJlG6 umGo08sUuGXOrTjQdem0ybi1XtHP7yPuD/Mvv+vMloZTZecPLV5dfVYL5DcE8RG4eMlq04j1AtT7 ZQNTjM+CxxdzcdPMR4q9Pe35l8safr1kYbhQlwo/0e6A+ND/ABU91/jkNXpIZ48Mvge5lptTLDK4 /LveK6xpF/pF9JZXsfCVNwRurqejKe4OcZqdNPDPhl/a9Zp9RHLHiigq5jt7q4q6uKurirq4q6uK vOfzRYfpCyFdxCxI+bZ0/YP0S97zvbP1x9zCc3zpn6GYq7FXYq7FXYq7FXYq+cf+cs/+Oh5b/wCM N1/xKLFXgOKvWvIUwk8r2q9TE0iH/kYWH4NnGdrxrUS86+56zsuV4B5X97Ia5rHYOrirq4q6uKur irq4q6uKurirq4q9p8jWFpL5Ks4JIw8c6yNKD3Jkbf5igoc7ns2AjggB3fe8dr5GWaV97E9VW2s9 cutLSQs9vxK8tiVdA4+dOVDl8c8TMwB9UWmWGQiJ16SlGraSt0pmhotyo+QcDsffwOYnaGgGcWNp jkf0H8bOTotacJo7wPMMq8iefDOyaPrDlbtTwtrl9i5G3pyV/b8D3+fXH7P7QJPhZdsg+39v3t+t 0QA8XFvA/Z+PsZT5m8s2GvWBt7gcJkqbe4Aq0bH9anuMz9XpIZ4cMvge5wtNqZYZcUXiesaRfaRf yWV7HwlTcEbq6noynuDnF6nTTwz4Zf2vW6fURyx4ooKuY7e6uKurirq4q6uKvMfzLm569FGOkVuo PzLMf1Uzrew41hJ75fqeZ7YleUDuixLNy6l+hmKuxV2KuxV2KuxV2KvnH/nLP/joeW/+MN1/xKLF XgOKvRfyxuw2n3loTvFKJAPaRaf8aZy/b2Opxl3ivl/a9F2LO4Sj3H7/AOxmtc0LunVxV1cVdXFX VxV1cVdXFXVxV1cVewfldq0V35eFlyH1iwdlZe5R2Lq34kfRnY9j5xPCB1jt+p5XtTCYZSekt/1o L8xvJ9zdv+m9NBa6iUC5hX7TKnR0p+0o2I8Mh2lo5yIy4vrj9v4+1l2fqoAHFk+iX4/HcxLSdXW6 UQzELcj6A4Hce/iMu7P7QjnjR2mOY/SPxs1a7QnCbG8DyP60v8xmAXcfp09bifWp9HCvvSv4ZrO3 uDijX1/o/HJ2PYvFUr+n9LOvIPn76z6ek6tJ/pOy2t0x/vPBHP8AP4Hv8+uT2X2p4n7vIfV0Pf8A t+9x+0ezuD1w+nqO79jKfM/lmx17TzbzgJOgJtrgfaRz+tT+0P45stXpIZ4cMvge51+m1MsMuKLw u7tp7S6mtZ14zQO0ci+DKaHOHy4zjkYnmHscWQTiJDkVKuVs3VxV1cVdXFXj3m+7F15kvpB9lZPS H/PIBP1rncdm4+DBEeV/Pd4/tDJxZpHzr5bJNmc4b9DMVdirsVdirsVdirsVfOP/ADln/wAdDy3/ AMYbr/iUWKvAcVZL5A1D6rr6RMaR3aGI+HL7S/iKfTmp7Zw8eAnrHd2fZWXgzV/O2ep5xz1TsVdi rsVdirsVdirsVdiqP0TW7/RtQS+sn4yLs6HdXQ9UYdwcydLqpYJ8Uf7XH1Omjmjwye3+W/Mlhr1g Lq1PGRaC4tyavG57HxB7Hv8AeM7XS6qGaHFH+x5HUaeWKXDJh3n/AMgeqJdW0mKr7td2ij7Xcug8 fEd81XafZnF+8x/V1Hf5jz+/389l2f2hw/u8n09D3fs+55kNhQbDOXJJ5vRgAcnVwWmnu/km/ub/ AMrafc3LF5mRkZz1b03aMEnxIXfO70GU5MMZHnX7HjNbiEMsojlbzL8y4Ui83XJSn71InYDseAG/ 3Vzm+24gZ7HUD9Tv+yJE4fcSxXNQ7R2KuxVDalepY6fcXb/ZgjZ6HuQNh9J2y3BiOSYiOpa82QQg ZHoHiTu8js7nk7ksx8SdznoIAAoPDk2bK3Ch+hmKuxV2KuxV2KuxV2KvnH/nLP8A46Hlv/jDdf8A EosVeA4qvhlkhmSaM8ZI2Do3gymoORlESBB5FMZEGxzD2nSdRj1HTre9jpSZASB2boy/Qds4DU4D iyGB6Pb6fKMkBIdUXXKG51cVdXFXVxV1cVdXFXVxV1cVdXFUw0TXNQ0a/S9sn4yLs6H7DoeqOO4O ZWk1c8E+KPy73H1Omjmjwy/se3eW/Mlhr1gLq1PGRaC4tyavG57HxB7Hv94ztdLqoZocUf7HkNRp 5YpcMmK+dPy3N7M+o6KFS5c1ntCQquT1ZCdlbxB2P69Z2j2R4p48e0uo7/2ux0HafhjgnvH7mM6b +WXmi5uQl1CtlBWjzSOj0AO/FUZiT4dvfNXg7FzSl6/SPn9zsc3a+KI9PqP473rmn2NrpmnQ2kPw W9tGFBNBso3Zj4nqc63HjEIiI5B5mczORkeZeG+btXj1bzFe3sW8DuEhO+6RgIrb/wA3GucR2jqB lzSkOXIfB67QYDjxAHnzKT1zBc11cVdXFWGfmRq4jtIdMjPxzkSzD/IU/CPpYfhnQdhaa5HIeQ2H v/s+90nbOoqIxjrufc87zqHnHYq/QzFXYq7FXYq7FXYq7FXzj/zln/x0PLf/ABhuv+JRYq8BxV2K sz/LvXRBcPpU7UjnPO3J7SU3X/ZAff8APNB25o+KPijnHn7nd9j6rhl4Z5Hl73odc5Z6N1cVdXFX VxV1cVdXFXVxV1cVdXFXVxVMNE1y/wBF1BL2yfjIuzod0dD1Rx3BzJ0mrngnxR+I73H1Omjmjwy/ seq6R+aHlu8iH1x2sLj9pJAXQn/JdQdv9YDOr0/bGHINzwnz/W83n7LzQOw4h5fqTG48++UYE5Nq UbeAjDSH7lBzIn2hgiLM4/f9zjx0OaRoRP3fewDzj+ZE+rQvYaarW1i4pNI1BLIO67E8V8RXf8M0 HaHbHiAwx7R6nvd3oeyuA8eTeXQMIrmidy6uKuriqldXUNrbyXE7cIYlLu3gBk8eMzkIx5ljkmIR MjyDxvWNTm1PUZr2XYyN8C/yoNlX6Bne6XTjDjEB0eK1Oc5ZmR6oLMhodir9DMVdirsVdirsVdir sVfOP/OWf/HQ8t/8Ybr/AIlFirwHFXYquR3jdXRiroQysNiCNwRgIBFFIJBsPWfK3mGPWNPDMQLy EBbhPfs49mziO0dCcE9voPL9T2Gg1gzQ/pDmnVc1znOrirq4q6uKurirq4q6uKurirq4q6uKurir q4q6uKurirq4q6uKvPPPvmQXMv6KtWrBE1blx0Zx+z8l/X8s6rsbQcA8WX1Hl7v2vN9ra3iPhx5D n7/2MNzfukdirsVfoZirsVdirsVdirsVdir52/5y0t2E3lm53Kst5GdtgQYSN/ev4Yq+fMVdirsV RmlapdaZepd2zUdNmU9GU9Vb2OUanTxzQMJN2nzyxTEovWNF1qz1azFzbtQjaWI/aRvA/wADnD6v STwT4ZfA972Wl1Mc0eKP9iPrmK5Dq4q6uKurirq4q6uKurirq4q6uKurirq4q6uKurirq4qxHzj5 vW0R9OsHrdsKTTKf7sHsP8r9XzzfdldmeIRkmPT0Hf8As+90vaXaPADCH1dT3ftedZ1bzLsVdiq6 ON5JFjQVdyFUdKkmg64q/QrFXYq7FXYq7FXYq7FXkP8Azk5oL3/kCLUolrJpN3HLIf8AimYGFv8A h2TFXyjirsVdirsVRmlate6XdLc2j8WGzId1df5WHcZj6nTQzR4Zj9jfp9RPFLiiXp2geZrDWIqR n0rpRWS3Y7j3X+YZxut7Pyac77x73rNHroZxttLuTiuYDnOrirq4q6uKurirq4q6uKurirq4q6uK urirRYAEk0A3JOKsL8z+eUQPZ6S/J+kl2Og9o/H/AFvuzouzuxyanlG3839f6nQa/tYD0Yvn+pgZ JYkk1J3JPUnOnAedJaxV2KuxVlf5V6DJrv5haFp6qWT60k8//GK3PrSV8KqhGKvuHFXYq7FXYq7F XYq7FUFrmj2etaPe6Terytb6F7eYDrxkUrUeBHUe+KvhTzN5fv8Ay7r99ol+vG5sZWiY0oGUbpIt f2XUhh7HFUrxV2KuxV2Kr4pZIpFkicpIhqrqSCCO4IyMoiQo7hMZEGxzZlof5gugWDVlLr0F0g+L /ZqOvzH3Zz2s7DB9WL5H9Bd9pO2SNsvz/WzSzv7O9iEtrMk0Z7qa0+Y6j6c53LhnjNTFF32LLDIL ibCvXKmx1cVdXFXVxV1cVdXFXVxV1cVSrV/M+k6WCs8vOcdII/if6ey/Tmdpezsub6RUe88nD1Ov xYeZ37gwHXfN2parWIH6vaH/AHQh+0P8tv2v1Z1Oi7Lx4N/qn3/qeb1faWTNt9Me79aRZs3XOxV2 KuxV2Kvo3/nF3yPJDb3nnC8jobkGz0yveNWrNIPm6hB8mxV7/irsVdirsVdirsVdirsVeRfn3+Ur +adOGvaNDXX7BCJIV63Vuu/D3kTqnjuPDFXyiysrFWBDA0IOxBGKtYq7FXYq7FXYqrW13c2soltp XhkH7SEqfwyGTFGYqQBDPHklA3E0WR6f+YOrQUW6RLtB3PwP967f8Lmnz9hYpbwJj9o/Hxdrh7ay x+oCX2H8fBPrT8wtFlAE6S27dyV5r967/hmqy9hZo/SRL7HZY+2sJ+q4pnD5p8vygFb6IV/nJT/i YXMKfZuojzgfv+5zI9oYJcpj7vvRK6zpDfZvrdqeEqH+OVHSZhzhL5FtGqxHlKPzC19e0RK8r+3B HUeqhP3A4Rosx5Ql8ig6vCP44/MIKfzl5chG92HPhGrN+IFPxzIh2TqJfw176cefamCP8V/NKLz8 x7RQRZ2jyHs0pCD7l5VzYYvZ+Z+uQHu3/U4WXtyI+iJPvY7qPnDXb6qmf0Ij/uuD4P8Ahvtfjm30 /ZODFvXEfPf9jqs/aebJ1oeX4tJeubJ17WKuxV2KuxV2Ks1/Kz8s9T8868luivDo9sytqd8Bside CE7GR+i+HXoMVfZ+m6dZabYW+n2MSwWdpGsNvCvRUQUUYqiMVdirsVdirsVdirsVdirsVeRfm1+Q mneaXm1nQSlhr5+KaMjjb3R/y6fYkP8AOOvcd8VfMnmDyzr/AJdv2sNbsZbG5WtFlWisBtyjcVV1 91JGKpXirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVepflv+QfmjzTJFe6oj6PoZIYz yrSeZfCGJt9/5227jl0xV9T+W/LWi+W9Ih0nR7ZbWyg6IN2Zj9p3Y7sx7k4qmeKuxV2KuxV2KuxV 2KuxV2KuxV2KoLWND0fWrNrLVrKG+tW3MNwiyLXxHIbEeI3xV5dr3/OMfkC/dpdNlu9JkPSOKQTQ /wDAzBn/AOHxVitx/wA4lyhq23mdWUk7SWZBA7biY1+4Yqo/9Cmah/1MkP8A0it/1VxV3/Qpmof9 TJD/ANIrf9VcVd/0KZqH/UyQ/wDSK3/VXFXf9Cmah/1MkP8A0it/1VxV3/Qpmof9TJD/ANIrf9Vc Vd/0KZqH/UyQ/wDSK3/VXFXf9Cmah/1MkP8A0it/1VxV3/Qpmof9TJD/ANIrf9VcVd/0KZqH/UyQ /wDSK3/VXFXf9Cmah/1MkP8A0it/1VxV3/Qpmof9TJD/ANIrf9VcVd/0KZqH/UyQ/wDSK3/VXFXf 9Cmah/1MkP8A0it/1VxVw/5xM1Dv5kip/wAwrf8AVXFU207/AJxO0WNwdS8wXNyld1toI7c0+btc fqxV6L5V/J/8vvLLpPp+lJLeIQVvLomeUEdGUvVUP+oBirM8VdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVf/2Q== + + + + uuid:C1BCCE1871B8DB11993190FCD52B4E9F + xmp.did:aa72db2f-bf46-7944-95c3-71159217fe7e + uuid:fc6c0d57-9426-4ab6-908a-8915ee26be0b + proof:pdf + + xmp.iid:ac54d225-b929-1647-8a63-9cb077fbd17f + xmp.did:ac54d225-b929-1647-8a63-9cb077fbd17f + uuid:C1BCCE1871B8DB11993190FCD52B4E9F + proof:pdf + + + + + saved + xmp.iid:53f32f1a-0299-417f-8eab-8c744979db72 + 2017-09-27T22:41:49-04:00 + Adobe Illustrator CC 2017 (Macintosh) + / + + + saved + xmp.iid:aa72db2f-bf46-7944-95c3-71159217fe7e + 2018-01-14T01:11:20+01:00 + Adobe Illustrator CC 22.0 (Windows) + / + + + + Document + Mobile + 1 + False + False + + 1024.000000 + 1024.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + R=23 G=145 B=255 + RGB + PROCESS + 23 + 145 + 255 + + + R=35 G=35 B=35 + RGB + PROCESS + 35 + 35 + 35 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + + + + + Adobe PDF library 15.00 + 21.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 11 0 obj <>/Resources<>/ExtGState<>/Properties<>/Shading<>/XObject<>>>/Thumb 93 0 R/TrimBox[0.0 0.0 1024.0 1024.0]/Type/Page>> endobj 85 0 obj <>stream +Hn$GDrmv.@Zl Z`YdA09L`eFd,称}]]C*g?_~~\~>;>z-s-4^ޏ5wFh`v GiޑRfLݞ/o]˽k2渽-,T/9m[s{̽-|3P^q& 0 ۿyװ}?맵|߲ΑR<ߪϗ-he#`\%Awb!KDS׊XZG35x*庬:Y,PN;gef3fQ҄Ze[QAlW`,[VL: (|5" <, +LbYk WܪNb#ۈ6`I+HI}TSGp1 5:IWoF)̏Ug]A!+uEb \+h|Q +\$^A%J #t B-CX=3Y=8T&S y1 RfmcS"=p4O!kFJšT@Y4ZPOɪAdtHN>u`Eɑ%@4C7) CP2x*a`Gq3R}ѡ.^Ҡ"!(kB72U?6`\F4F pJ C ' .P [Al"[rB9 ѝ..)d"Iˊ lq 7DrP5&<T4^.Uvg4IY.f;SK) f-sمr)ċ-2 BוL-;Y3.GAH!L#1#*<+;%() PFCe7H fsj"oR<܋OTV]1#,Τaъҙs):U2RNn}p*['pS(Rt"eShE:m i%D*f܆sUS%W7|LIt*y)Ni9wRDT+I( L^\P --5A1ᥪYr1 UY +uW\x5p*v t!"#Qw:pM"NyL:,Qn^S5J 2lgcWuMR]dQв7Jip[Ite dgPu%%pWX*)YSL3M4o0Lڶ JSo/XgcKУeS&Kx%u׵09((Q8%K8I[FiLltsA^1Uec.#`yXK>Ei(VP11!,o&F黨쭐ůd+u?牸}\I]M'ƪ&t'y&38_ҳ+u]։Ʞd( N`O [d,`ji 6)Kঃ](slpe+L~Aٵ0@.tlLBrN. &#)2 frؠDd @R +endstream endobj 93 0 obj <>stream +8;Xu\acV61$mZphS33)R?h.S\+J%W$FW]8BG,]5,6#s$V[b*K#`bo:m`]3U]C7P62 +D\E4q`cFo^d'q#]),f/a!^jcoLo-Tc/?27LJ*1s?/]3jcVKJIXb?S"4*UPBOVrK"P +Li]=c+-i8P&>"KsB$+;6c&Q1DrTW!0iOPl*%Oc6G)WU7_($<>^%dV1m@m2RRdU641J.VA"G"%1Ti +4`t23R)P8Po53fd._=A/e7o6-T6[="8]WgV$JUqg=MgVHSh5XDSnZol[kNMZE2j6D +TodijW$e2$;@4/pH_piq@E.T=Uo"\rA3IH^8-MKQELqh7?_/-NfCf+f\,5u)in?Gs +U\`tr'!co\"o"[m%^ai;JkU`:M6H(P7@?5W?C8fTZ2+`%@[8sie-[\p]H>NpbQem3QeP@O29G/5rmkddp-?e&.M>g[JI1nC +B"92V`q3D+738O!G!E&UI74=iMCa>BOQng&_NA9O_Z>?2$d--:BGUAea +0dRe3$o=;8VlWJg%Q*b0;=n7Qq%ZB&\oW*W\XN4TX3CdN1n/!\932[ET-C-7e^)`Q +U/,?Bk"+O39ime(9uuOcm*okmh6q/MWo4[cWjb-S0fh%2NrlHBf;LSgrRtWG^HKbN!pMdcpZ`kOFjFf:Xjm* +<&G*4;u[N*Vq>K^:Do5Rlg!"7"+>_P/P]rmW9:nr!*#.PMu~> +endstream endobj 94 0 obj [/Indexed/DeviceRGB 255 95 0 R] endobj 95 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> +endstream endobj 91 0 obj <>/ExtGState<>>>/Subtype/Form>>stream +/CS0 CS 1 1 1 SCN +1 w 10 M 0 j 2 J []0 d +/GS0 gs +q 1 0 0 1 1024 1024 cm +0 0 m +-1024 -1024 l +S +Q +q 1 0 0 1 0 1024 cm +0 0 m +1024 -1024 l +S +Q +q 1 0 0 1 958 1024 cm +0 0 m +0 -1024 l +S +Q +q 1 0 0 1 706 1024 cm +0 0 m +0 -1024 l +S +Q +q 1 0 0 1 512 1024 cm +0 0 m +0 -1024 l +S +Q +q 1 0 0 1 318 1024 cm +0 0 m +0 -1024 l +S +Q +q 1 0 0 1 66 1024 cm +0 0 m +0 -1024 l +S +Q +q 1 0 0 1 0 958 cm +0 0 m +1024 0 l +S +Q +q 1 0 0 1 0 706 cm +0 0 m +1024 0 l +S +Q +q 1 0 0 1 0 512 cm +0 0 m +1024 0 l +S +Q +q 1 0 0 1 0 318 cm +0 0 m +1024 0 l +S +Q +q 1 0 0 1 0 66 cm +0 0 m +1024 0 l +S +Q +0 J +q 1 0 0 1 512 318 cm +0 0 m +107.144 0 194 86.856 194 194 c +194 301.143 107.144 388 0 388 c +-107.143 388 -194 301.143 -194 194 c +-194 86.856 -107.143 0 0 0 c +h +S +Q +q 1 0 0 1 512 238 cm +0 0 m +151.326 0 274 122.674 274 274 c +274 425.326 151.326 548 0 548 c +-151.326 548 -274 425.326 -274 274 c +-274 122.674 -151.326 0 0 0 c +h +S +Q +q 1 0 0 1 512 66 cm +0 0 m +246.319 0 446 199.681 446 446 c +446 692.319 246.319 892 0 892 c +-246.319 892 -446 692.319 -446 446 c +-446 199.681 -246.319 0 0 0 c +h +S +Q + +endstream endobj 92 0 obj <>/ExtGState<>>>/Subtype/Form>>stream +/CS0 CS 1 1 1 SCN +1 w 10 M 0 j 2 J []0 d +/GS0 gs +q 1 0 0 1 798.9805 0 cm +0 0 m +-573.961 0 l +-698.236 0 -798.98 100.745 -798.98 225.02 c +-798.98 798.98 l +-798.98 923.255 -698.236 1024 -573.961 1024 c +0 1024 l +124.274 1024 225.02 923.255 225.02 798.98 c +225.02 225.02 l +225.02 100.745 124.274 0 0 0 c +h +S +Q + +endstream endobj 97 0 obj <> endobj 88 0 obj <> endobj 87 0 obj [/ICCBased 98 0 R] endobj 98 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km +endstream endobj 96 0 obj <> endobj 90 0 obj <> endobj 99 0 obj <> endobj 100 0 obj <> endobj 82 0 obj <> endobj 79 0 obj <> endobj 80 0 obj <> endobj 83 0 obj <> endobj 81 0 obj <> endobj 109 0 obj [/View/Design] endobj 110 0 obj <>>> endobj 107 0 obj [/View/Design] endobj 108 0 obj <>>> endobj 105 0 obj [/View/Design] endobj 106 0 obj <>>> endobj 103 0 obj [/View/Design] endobj 104 0 obj <>>> endobj 101 0 obj [/View/Design] endobj 102 0 obj <>>> endobj 89 0 obj <> endobj 86 0 obj <> endobj 111 0 obj <> endobj 112 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 17.0 +%%AI8_CreatorVersion: 22.0.0 +%%For: (Jamie Rees) () +%%Title: (Ombi-icon copy.ai) +%%CreationDate: 3/23/2018 3:05 PM +%%Canvassize: 16383 +%%BoundingBox: -314 -1214 1024 0 +%%HiResBoundingBox: -313.00000512612 -1213.00001221649 1024 0 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 13.0 +%AI12_BuildNumber: 243 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Registration]) +%AI3_Cropmarks: 0 -1024 1024 0 +%AI3_TemplateBox: 512.5 -512.5 512.5 -512.5 +%AI3_TileBox: 224 -868 800 -134 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 5 +%AI17_Begin_Content_if_version_gt:17 1 +%AI9_OpenToView: -563.355782210889 320.36793160342 0.6667 1584 958 18 0 0 78 87 0 0 0 1 1 0 1 1 0 0 +%AI17_Alternate_Content +%AI9_OpenToView: -563.355782210889 320.36793160342 0.6667 1584 958 18 0 0 78 87 0 0 0 1 1 0 1 1 0 0 +%AI17_End_Versioned_Content +%AI5_OpenViewLayers: 27627 +%%PageOrigin:424 -616 +%AI7_GridSettings: 10.0799999237061 1 10.0799999237061 1 1 0 0.788235306739807 0.7843137383461 0.792156875133514 0.894117653369904 0.892156839370728 0.89607846736908 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 113 0 obj <>stream +%%BoundingBox: -314 -1214 1024 0 +%%HiResBoundingBox: -313.00000512612 -1213.00001221649 1024 0 +%AI7_Thumbnail: 128 116 8 +%%BeginData: 17220 Hex Bytes +%0000330000660000990000CC0033000033330033660033990033CC0033FF +%0066000066330066660066990066CC0066FF009900009933009966009999 +%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 +%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 +%3333663333993333CC3333FF3366003366333366663366993366CC3366FF +%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 +%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 +%6600666600996600CC6600FF6633006633336633666633996633CC6633FF +%6666006666336666666666996666CC6666FF669900669933669966669999 +%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 +%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF +%9933009933339933669933999933CC9933FF996600996633996666996699 +%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 +%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF +%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 +%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 +%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF +%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC +%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 +%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 +%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 +%000011111111220000002200000022222222440000004400000044444444 +%550000005500000055555555770000007700000077777777880000008800 +%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB +%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF +%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF +%524C45FD2DFFA87D52522727275227272752272727522727275227272752 +%272727522727275227272752272727522727275227272752272727522727 +%27522727275227272752277D7DA8FD39FFA87D2727F8FD4127F85252A8FD +%35FF52FD4A277DA8FD31FF7DFD4C27F8277DFD2FFF522727275227272752 +%272727522727275227272752272727522727275227272752272727522727 +%275227272752272727522727275227272752272727522727275227272752 +%2727275227272752FD042752FD2CFFA827F8FD27270527272705FD2627FD +%2AFFA852FD2527514B515175517651512751FD2527FD28FFA827F8FD1D27 +%052727515175759F999F99C19FC19FC19F9F999F75754B4B272700FD1E27 +%FD27FF522752272727522727275227272752272727522727275227272752 +%FD05277575C29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FA07552 +%272727282727275227272752272727522727275227272752272727522727 +%52FD25FF7DF8FD1C2751759F9FC19FC19F9F99C19F9F99C19F9F99C19F9F +%99C19F9F99C19FC19F9F754B05FD1B277DFD24FFFD1B274B51A09FC29FC2 +%9FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29F9F +%4BFD1B27A8FD22FF52FD1A277599C1999F999F999F999F999F999F999F99 +%9F999F999F999F999F999F999F999F999F999F9FC17551FD1827F852FD21 +%FFA82727522727275227272752272727522727275227272752272751A0C1 +%C29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29F +%C29FC29FC29FC29FC29FA0FD052752272727522727275227272752272727 +%52272727FD21FF7DF8FD1527005175C19FC19F9F99C19F9F99C19F9F99C1 +%9F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F +%51FD172752FD20FFFD1727759FC29FC19FC29FC19FC29FC19FC29FC19FC2 +%9FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC1 +%7551FD152752FD1FFF7DFD1627759F9F999F999F999F999F999F999F999F +%999F999F999F999F999F999F999F999F999F999F999F999F999F999F999F +%999F99C1995105FD1427A8FD1EFF7D275227272752272727522727275227 +%272752272727A0C1C29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29F +%C29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29FC29F +%7527282727275227272752272727522727275227277DFD1EFF52FD14279F +%9FC19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C1 +%9F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F75FD132752 +%FD1EFF52FD12274BC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29F +%C19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19F +%C29FC19FC2C19FFD1327FD1EFFFD13279F999F999F999F999F999F999F99 +%9F999F999F999F999F999F999F999F999F999F999F999F999F999F999F99 +%9F999F999F999F999F999F999F999F9F7505FD1127FD1DFFA85227272752 +%2727275227272752272727284BC19FC29FC19FC29FC19FC29FC19FC29FC1 +%9FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC2 +%9FC19FC29FC19FC29FC19FC29FC19FA02727275227272752272727522727 +%275227FD1DFFA8FD11279F9F9F99C19F9F99C19F9F99C19F9F99C19F9F99 +%C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F9F99C19F +%9F99C19F9F99C19F9F99C19F9F9F75FD1027FD1DFFA8FD10279F9FC19FC1 +%9FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC1 +%9FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC1 +%9F75FD0F27FD1EFFFD0E2700759F9F999F999F999F999F999F999F999F99 +%9F999F999F999F999F999F999F999F999F999F999F999F999F999F999F99 +%9F999F999F999F999F999F999F999F999F999F995105FD0D27FD1DFFA827 +%2752272727522727275227272776C1C19FC29FC19FC29FC19FC29FC19FC2 +%9FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29FC1 +%9FC29FC19FC29FC19FC29FC19FC29FC19FC29FC19FC29F27275227272752 +%27272752272727FD1EFFFD0D274B99C1999F99C1999F99C1999F99C1999F +%99C1999F98C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 +%999F99C1999F99C1999F99C1999F99C1999F99C1999F99C175FD0D27FD1D +%FFA8FD0D2775C19FC19FC19FC19FC19FC19FC19FC19FC198C1A0A0A0C29F +%9F99C19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19F +%C19FC19FC19FC19FC19FC19FC19FC19FC14BFD0C27FD1DFFA8FD0C274BC1 +%999F999F999F999F999F999F999F999F99A7A8FFFFFFAFFFA79F989F999F +%999F999F999F999F999F999F999F999F999F999F999F999F999F999F999F +%999F999F999F999F999F999FFD0C27FD1DFFA85227272752272727522727 +%279F9FC19FC19FC19FC19FC19FC19FC19FC1A0FD0AFFC999C19FC19FC19F +%C19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19F +%C19FC19FC19FC1C1752752272727522727275227FD1DFFA8FD0A27057599 +%C1999F99C1999F99C1999F99C1999FA0FD0CFFA7989F99C1999F99C1999F +%99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 +%999F99C199FD0B27FD1DFFA8FD0A274B99C199C19FC199C19FC199C19FC1 +%99C19FFD0EFFC999C199C19FC199C19FC199C19FC199C19FC199C19FC199 +%C19FC199C19FC199C19FC199C19FC199C19FC199C175FD0A27FD1EFFFD0A +%274BC1999F999F999F999F999F999F999F98C9FFFFCAFFA8A1999FA0CFFD +%04FFAFA0989F999F999F999F999F999F999F999F999F999F999F999F999F +%999F999F999F999F999F999F999F999F999FFD0A27FD1DFFA82727522727 +%27522727279F9FC19FC19FC19FC19FC19FC19FC19FC2FD04FFCFA098C19F +%C199CFFD05FFC998C19FC19FC19FC199C19FC19FC19FC19FC19FC19FC19F +%C19FC19FC19FC19FC19FC19FC19FC19FC19FC1C175275227272752272727 +%FD1EFFFD09275199C1999F99C1999F99C1999F99C1999F9FFD04FFA1989F +%99C1999F99CFFD05FFA098C1999F99C199C9A0C1999F99C1999F99C1999F +%99C1999F99C1999F99C1999F99C1999F99C1999F99C175FD0927FD1DFFA8 +%FD092775C199C19FC199C19FC199C19FC199C199C9FD04FF9FC19FC199C1 +%9FC199CFFD05FFA798C19FC199C1FFFF99C19FC199C19FC199C19FC199C1 +%9FC199C19FC199C19FC199C19FC199C19FC199C14BFD0827FD1DFFA8FD09 +%279F999F989F999F989F999F989F999F989FA0FFFFFFCA9F989F999F989F +%999F98CAFFFFCAFFFFA0989F999F99FFA89F989F999F989F999F989F999F +%989F999F989F999F989F999F989F999F989F999F987505FD0727FD1DFFA8 +%5227272752272727759FC19FC19FC19FC19FC19FC19FC19FC19FA0FD04FF +%A0C19FC19FC19FC19FC19FCFFD05FFC999C199C1FFCF99C19FC199CFC9C1 +%99C19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC19FC199272752 +%2727275227FD1DFFA8FD082775C1999F99C1999F99C1999F99C1999F99C1 +%9FFD04FFCA98C1999F99C1999F99C199CAFD05FFA0989F99FFCA9F98C198 +%CAFFCA989F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99 +%C1FD0827FD1DFFA8FD08279F99C19FC199C19FC199C19FC199C19FC199C1 +%A8FD04FFCA99C19FC199C19FC199C19FCFFD05FFC299C1CFCF9FC198CFFF +%CA99C19FC199C19FC199C19FC199C19FC199C19FC199C19FC199C19FC19F +%75FD0727FD1EFFFD0627054B999F989F999F989F999F989F999F989F999F +%98A0FFFFCAFFFFA8989F999F989F7C7D769F98CAFD04FFA19F99FFA89F98 +%CAFFCA989F989F999F989F999F989F999F989F999F989F999F989F999F98 +%9F99C174FD0727FD1DFFA82727522727272851C19FC19FC199C19FC199C1 +%9FC199C19FC199C199C9FD05FFCF99C199C17DA8A8A8A0C199FD05FF9FC1 +%A0C899CAFFCF9FC199C19FC199C19FC199C19FC199C19FC199C19FC199C1 +%9FC199C19FC19F9F27272752272727FD1EFFFD062705759F9F98C1999F98 +%C1999F98C1999F98C1999F98C198A7FD05FFCA989F98A784A87DA87C98A0 +%FD04FFA0989F98C1A1C9989F999F98C1999F98C1999F98C1999F98C1999F +%98C1999F98C1999F98C1999F984BFD0627FD1DFFA8FD072775C19FC199C1 +%9FC199C19FC199C19FC199C19FC199C198A7FD05FFCA99C17CA8A8A87DA8 +%7C9FFD04FFA0C19FC199C198C199C199C198C19FC199C19FC199C19FC199 +%C19FC199C19FC199C19FC199C19FC14BFD0627FD1DFFA8FD072799989F98 +%9F989F989F989F989F989F989F989F989F989F98A7FD05FFCA989F7CA87D +%A87DA87CFD04FFA0989F989F98C2A8CFA8CFA8CFA19F989F989F989F989F +%989F989F989F989F989F989F989F989F985100FD0527FD1DFFA852272727 +%52272799C19FC199C19FC199C19FC199C19FC199C19FC199C19FC198C9FD +%05FFCA98C1A0FD06A8FFFFFFA0C19FC199C1A0FFCFFD04FFCF9FC199C19F +%C199C19FC199C19FC199C19FC199C19FC199C19FC175282727275227FD1D +%FFA8FD07279F98C1999F98C1999F98C1999F98C1999F98C1999F98C1999F +%98A7FD05FFCA98C07CA87DA87DA8A8FFA89F98C1989F98C1999F98C1999F +%98C1999F98C1999F98C1999F98C1999F98C1999F98C1999F98C1997505FD +%0527FD1DFFA8FD0627519FC199C19FC199C19FC199C19FC199C19FC199C1 +%9FC199C19FC198C9FD05FFCF9FC1A1A87DFD04A8C9989F9FA0A0A099C198 +%C199C199C199C19FC199C19FC199C19FC199C19FC199C19FC199C19FC199 +%C175FD0627FD1EFFFD07279F989F989F989F989F989F989F989F989F989F +%989F989F989F989F98A7FFFFCAFFFFFFCAFFA8A87DA87DA87CC9A8FFAFFF +%FFFFA19F989F989F989F989F989F989F989F989F989F989F989F989F989F +%989F989F9899FD0627FD1DFFA8272752272727519FC199C19FC199C19FC1 +%99C19FC199C19FC199C19FC199C19FC199C198C9FD08FFFD04A8A7A8A8FD +%08FFC898C19FC199C19FC199C19FC199C19FC199C19FC199C19FC199C19F +%C199C175272752272727FD1EFFFD06274BC1999F98C1999F98C1999F98C1 +%999F98C1999F98C1999F98C1999F98C198A1AFFD07FFA8A87DA87DA8A8FD +%08FFA098C1999F98C1999F98C1999F98C1999F98C1999F98C1999F98C199 +%9F9899FD0627FD1DFFA8FD06275199C199C199C199C199C199C199C199C1 +%99C199C199C199C199C199C199C198A0A7FD06FFC97CA8A8A87DA8A8FD08 +%FFA098C199C199C199C199C199C199C199C199C199C199C199C199C199C1 +%75FD0627FD1DFFA8FD07279F989F989F989F989F989F989F989F989F989F +%989F989F989F989F989F989F989999A0A0A0999F98A7A8A87DA87DA87D9F +%A0CFFFFFCAFFFFA0989F989F989F989F989F989F989F989F989F989F989F +%989F989F987505FD0527FD1DFFA85227272752275199C19FC199C19FC199 +%C19FC199C19FC199C19FC199C19FC199C19FC199C19FC199C198C198C199 +%C1CAFFFD06A87CC199CFFD05FFC998C199C19FC199C19FC199C19FC199C1 +%9FC199C19FC199C19FC175282727275227FD1DFFA8FD07279F98C1989F98 +%C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C198 +%9F98C19FFFFFFFA8A87DA87DA87CC198CAFD05FFA0989F98C1989F98C198 +%9F98C1989F98C1989F98C1989F98C1987505FD0527FD1DFFA8FD072799C1 +%98C199C198C199C198C199C198C199C198C199C198C199C198C199C198C1 +%99C198C199C198A0FD04FF7DA87DA8A8A87CC199CFFD05FFC998C198C199 +%C198C199C198C199C198C199C198C199C198C151FD0627FD1EFFFD062705 +%99989F989F989F989F989F989F989F989F989F989F989F989F989F989F98 +%9F989F989F989F98989FFFFFFFA89F7CA87DA87DA87C9898CAFD04FFAFA0 +%989F989F989F989F989F989F989F989F989F989F989F985105FD0527FD1D +%FFA82727522727272875C199C199C199C199C199C199C199C199C199C199 +%C199C199C199C199C199C199C199C199C198C2FD04FFA0C17CFD05A8A0C1 +%99CFFD05FFC898C199C199C199C199C199C199C199C199C199C199C12727 +%2752272727FD1EFFFD06270575989F98C1989F98C1989F98C1989F98C198 +%9F98C1989F98C1989F98C1989F98C1989F98C1989F99FD04FFCF98987CA8 +%7DA87DA8999898CFFD05FFA098C1989F98C1989F98C1989F98C1989F98C1 +%989F98FD0727FD1DFFA8FD0827C199C198C199C198C199C198C199C198C1 +%99C198C199C198C199C198C199C198C199C198C1999FA7FD04FFCA98C17C +%A8A8A87D9F99C198CFFD05FFA098C199C198C199C198C199C198C199C198 +%C19F99FD0727FD1DFFA8FD082774C1989F989F989F989F989F989F989F98 +%9F989F989F989F989F989F989F989F989F989F989F989FA9FD04FFCA9898 +%76A87DA0989F989898CAFFFFCFFFA198989F989F989F989F989F989F989F +%989F98C14BFD0727FD1DFFA8522727275227272799C1C199C199C199C199 +%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C198 +%C9FD05FFCA98C19F9F98C199C199C199FD05FF99C199C199C199C199C199 +%C199C199C199C19F5127522727275227FD1DFFA8FD08274BC1989F98C198 +%9F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98 +%C1989F98A7FD05FFCA98C1989F98C1989F98C0A0FD04FFA098C1989F98C1 +%989F98C1989F98C1989F989FFD0827FD1DFFA8FD08274B98C199C198C199 +%C198C199C198C199C198C199C198C199C198C199C198C199C198C199C198 +%C199C198C9FD05FFCA98C199C198C199C198C1FD04FFA0C198C199C198C1 +%99C198C199C198C199C175FD0827FD1EFFFD092775989F989F989F989F98 +%9F989F989F989F989F989F989F989F989F989F989F989F989F989F989F98 +%9F98A7FFFFCAFFFFA8989F989F989F989998FFFFFFAFA0989F989F989F98 +%9F989F989F989F989F984B00FD0727FD1DFFA82727522727275227274BC1 +%99C198C199C198C199C198C199C198C199C198C199C198C199C198C199C1 +%98C199C198C199C198C198C9FD05FFCA98C199C199C198C9FD04FFA0C198 +%C199C198C199C198C199C198C19F9F272827272752272727FD1EFFFD0A27 +%98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1 +%989F98C1989F98C1989F98C198A1FD05FFCA989898C1989FA8FFFFFFA89F +%989F98C1989F98C1989F98C1989F98C14BFD0927FD1DFFA8FD0A277598C1 +%99C198C199C198C199C198C199C198C199C198C199C198C199C198C199C1 +%98C199C198C199C198C198A7FD06FF9FC199C2CAFD04FFC998C199C198C1 +%99C198C199C198C199C19851FD0927FD1DFFA8FD0B2799989F989F989F98 +%9F989F989F989F989F989F989F989F989F989F989F989F989F989F989F98 +%9F989F989F98A1FD06FFA8FFAFFD05FF9998989F989F989F989F989F989F +%989F987500FD0927FD1DFFA8522727275227272752272775C199C198C199 +%C198C199C198C199C198C199C198C199C198C199C198C199C198C199C198 +%C199C198C199C198C9FD0CFFA0C198C199C198C199C198C199C198C199C1 +%2728272727522727275227FD1DFFA8FD0C2798C1989F98C1989F98C1989F +%98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1 +%989F98A7FD0AFF9F98989F98C1989F98C1989F98C1989F98C175FD0B27FD +%1DFFA8FD0C277598C199C198C199C198C199C198C199C198C199C198C199 +%C198C199C198C199C198C199C198C199C198C199C198C2A7FD06FFCA9FC0 +%98C199C198C199C198C199C198C199C19851FD0B27FD1EFFFD0D2799989F +%9899989F9899989F9899989F9899989F9899989F9899989F9899989F9899 +%989F9899989F9899989F9899989899A0A0A19F9F98999899989F9899989F +%9899989F9899989F985100FD0B27FD1DFFA8272752272727522727275227 +%274BC199C198C199C198C199C198C199C198C199C198C199C198C199C198 +%C199C198C199C198C199C198C199C198C199C198C198C198C199C198C199 +%C198C199C198C199C198C1C09927282727275227272752272727FD1EFFFD +%0E274AC1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F +%98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1989F98C1 +%989F98C1989F98C1989F989FFD0E27FD1DFFA8FD0F2774C198C198C198C1 +%98C198C198C198C198C198C198C198C198C198C198C198C198C198C198C1 +%98C198C198C198C198C198C198C198C198C198C198C198C198C198C198C1 +%4BFD0E27FD1DFFA8FD102798C19899989F9899989F9899989F9899989F98 +%99989F9899989F9899989F9899989F9899989F9899989F9899989F989998 +%9F9899989F9899989F9899989F989998C16EFD0F27FD1DFFA85227272752 +%27272752272727522727275198C198C199C198C199C198C199C198C199C1 +%98C199C198C199C198C199C198C199C198C199C198C199C198C199C198C1 +%99C198C199C198C199C198C199C198C17527275227272752272727522727 +%275227FD1DFFA8FD10270551989F98C1989998C1989998C1989998C19899 +%98C1989998C1989998C1989998C1989998C1989998C1989998C1989998C1 +%989998C1989998C1989998C198C174FD1127FD1EFFFD12277598C198C198 +%C198C198C198C198C198C198C198C198C198C198C198C198C198C198C198 +%C198C198C198C198C198C198C198C198C198C198C198C198C1994BFD1127 +%FD1EFFFD122700759899989F9899989F9899989F9899989F9899989F9899 +%989F9899989F9899989F9899989F9899989F9899989F9899989F9899989F +%9899989F98C0742705FD102752FD1EFF5227522727275227272752272727 +%5227272728277598C198C198C198C198C198C198C198C198C198C198C198 +%C198C198C198C198C198C198C198C198C198C198C198C198C198C198C198 +%C198C1985127272752272727522727275227272752272752FD1EFF7DFD13 +%27055198C1989998C1989998C1989998C1989998C1989998C1989998C198 +%9998C1989998C1989998C1989998C1989998C1989998C1989998C1742705 +%FD1127F87DFD1EFFA8FD15275198C198C198C198C198C198C198C198C198 +%C198C198C198C198C198C198C198C198C198C198C198C198C198C198C198 +%C198C198C175FD1527A8FD1EFFA8FD152705276EC19899989F9899989F98 +%99989F9899989F9899989F9899989F9899989F9899989F9899989F989998 +%9F9899989F989998994B2705FD1427FD20FF522727522727275227272752 +%2727275227272752272727284B9F98C198C198C198C198C198C198C198C1 +%98C198C198C198C198C198C198C198C198C198C198C198C198C198C19899 +%27272752272727522727275227272752272727522727277DFD20FFA8FD19 +%277598C1989998C1989998C1989998C1989998C1989998C1989998C19899 +%98C1989998C1989998C1989998C1985105FD1727A8FD21FF7DFD19275174 +%C198C198C198C198C198C198C198C198C198C198C198C198C198C198C198 +%C198C198C198C1989F4B4BFD182752FD22FFA8FD19270527267598BA9899 +%9898989998989899989898999898989998989899989898999898989998BA +%9875272705FD1827FD24FF7D272752272727522727275227272752272727 +%5227272752272727282751759F98C198C198C198C198C198C198C198C198 +%C198C198C198C198C198C198994B51272727522727275227272752272727 +%5227272752272727522727277DFD25FF52FD1E2751749998C1989898C198 +%9898C1989998C1989898C198BA98C198994A51FD1C27F852FD27FFFD2127 +%514B99989F98C198C198C198C198C198C1989F74754B51FD1F2752FD28FF +%A8FD22270527274B4A754A756E756E754A514B4B2727052705FD1D27F827 +%A8FD2AFFFD05275227272752272727522727275227272752272727522727 +%275227272728272727282727272827272728272727282727275227272752 +%2727275227272752272727522727275227272752272727522752A8FD2CFF +%FD5027F852A8FD2EFF7DFD4E277DFD31FFA8522727F8FD4527F82752A8FD +%35FF5252FD45277DA8FD38FFA8A87D52FD3D2752527D7DFD42FFA8FFA8FF +%A8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FF +%A8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FDFCFFFDFCFFFD +%FCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDB3FFFF +%%EndData + +endstream endobj 114 0 obj <>stream +%AI12_CompressedDataxeq3 HޚAu_5$FAɲ\m(hY9֭%qR"Yُ|DX/g_?+WO/n޼x7??/o}G?OOi\-zxׯ~~joO^8ŋo~zO7/^~_˟w֕nN/T~/]oyCe-|xW_|O?+~\O~^|^?-垲So_W/^}7?77|曟n~oVRj>-Wwes 8<\y/޼dOq#/.۷ec^t_cڢ7sᖫ5yKKvᗯ;=}=/֘v/߼|Ņ?o_~7$$_|_?{{__|YH~\e̅{ }|7E|QW2G.[<˗}yw7_y_ˏO|Wm.?&_Ws}F_Go<\kN?|"U2;<nǯ]$޼8_Q sSZ{.Y/飄Azw/|ŤDz/z#esKŷk/4/"W_O_{|fHpqng~Nׯ.ϵz:ի_Iv~tIzctŧO??ur'ZtŇO?~g۳՚ZQjImylwjj7j꺭j}jB[R[.>d[oԮ>ԺZSYZR[r_?ozq}/~+~W]ݟbI?l~WSؖ6Ժ[7nn~Co{r1 Z*~&ݪݩi-#MqKY=.i\/7r:0oxMMwy3s5, FvomeRJFu)%J=goy[Ir]ZmJ^sYZW)}16jqZ`O"$^6o>N~jR'鑀˟yo<>-[GMS[CYPfvO޾ֱ4+(Y*G {? y۳ޥ5=dI!zv!mQF!y ->˾{kk/Ջ.U6$W-M].e^Qs&q}o]$°0f/sǖ~mS<OO (ʩEx':~g7=]%9zStGWpzm/|}Ay?RcԵ%{ŻՁ3J 6 Q#28V-ˇ?yhd)-B] gS~itӚӬg b֑>x\x?OY~aJ?V[vV &h8 )C&Z mp \?p6p~;u|#m9[cnhb#ڴ;LMf|=M7QxDl=U@PVNv [AwR.?vO[-jߕlm4Le>%$2G!/̏dz]fGfǙUKæ+>$ +>r^hXڬ[y&3tUm$sgϳ)w7Ym칝nU:Jי^Mp%fԧ$O fʢՔ>'yNpDb=N| +WJ8S$>9x)^WkWKր~I_!AbCMW`MW|?ߧ}@:e_9Y CH|o#owjyTrէ)ܟxNě>rOTl}~Jk<zs`x? + + J({‘'ZI|&] ]J늗Bo#AiHv?%]o]ۙUwc۽5U &\oJLڤ|%\Ks&% mGsc5y胑Z0@@Szn#9oqOH3V SǻoqIACC)|{ Z`X/C?nf}$ۥ6(q˅xk}w"ťVkrOoglצv%sgmɭk2 V"!{'1 ݞ:thy>PL#ˇ1n>:;pnL[E]ӦCc$OGON<.x$]}@I4wϻ!TMQ&WTtj[>=A#x*ͅر;2Nӳ ';\.vs.s>78S_dʿ p=ΏHԸ}`4x/!C?Y0amږ[#"l->Ho.s^Kٮ}!LngnVkSA7ͳλdFbA/l865rݟ-9~Lb}_$;gw" c퍰1"-8. 7)L7?E*l?]â tݬpc.F`X߯c5 ?nIx KQo;:tV\ۨvC68 +u?=XAy`P^x&Wkd ׳ko>uGJlm3oΥچ@p_'A>ʋf8?zQBQ"Q4lبqn8|&/zΧ^zYP=Oz)DxB<)o|ߟ6TJQj noq) TmH$4ƖR5u>)]<F?PAw|\S +u'iKWKKYjԠVO^#-xPO_o󯔺˾>Ni9ŧ>`~[oO#=}Sj +~|{o_~O?З~ObgQ~߼gr_A⪷DxiIQإ‡tGH֡/uv(P GNj-^TS +],9+*J6-A&}jin C=v%a/Ul[Z*`O+*qEֆ AAzwzՓ镅KGU$줭.]z](q|@KBp+E2RG6bY0@0 i}n _j0ү4KgI !ཾ;W!Y.K㱵D# 'KPoE+ i!k.SEg+zXY㯍5X]t*n:*խlz=-]7L5RSѨCrfiA.8zlEC4@~p!YjYt,9%cHVUrZ4NwD½-&5GݚA4 +y O锯c6Fb#p~aiBG;ƴ`B/mDPEa.,rPms=(l-EWzn*C(ut@HU˿SݳJf&ehY<Ω (|6my-~@ߪVXXxr]E_lx&Acs%:HC.iW.⮺,DBe5_EB O aHNck^4"Gތ>W-zY-]U<ۂX5n M5HtCY`Q&nǕAc @K[i B/Ղ IZ^*;Jʝ#0%RMHgWNjbA[ICW: urNO⛞E:ŭ]S(D%.;[W1z;Aƽo(+/ /*V$ltOh!M^M E3`eP;[0i&e#-)Rh:hc:MKnzv/n}!am "1dͬz2ƍ6顚fCCϛ5%,͂Iځ!p aOk[%.ԅ2XҒazP]H0 TliqijEr(|޾Κ9@B:&IW]TMw\2܃lXBZ,wp~eJItb]׮`c}ӣ^(rtfŤ> uRWq֕  +?,`5U8(c!-sn^=$TWВ$c@_FkRP\17f'W0BL|/l8Y+FTgԣ2اuNI-Ab09ckѕ[Q7[Q'zTb< +ieepO}H:;h?~mW?)%X\FX@T O=2g7DD wZŴ(FC\kqƖ: +i[ҟz+Qa=OEʹ)z\̬l88_!+\ZF@k7FkB@A2'BGS9N9 # 7'z}Y8TT(=W-jH:dt#MъW&XX[Z؈.":vu+ 9[R:!a%Bj:K~_ +XM/fvHVk dAF\#/{Io%y"*n:/Dtu?>08=pl芠.ņ♗yGBAڞ^%7Q>jKJjQU8+BIzv \=쓛{ Iܣw5ehi^  (_O jau v>\P"a!k~F-BًYJU_8tuغc}Yyܛ QvfpbI+WFʯBþS 4#~%tm (H*HO7ȍUJnچl'&4{65ٴ)Mj1^V(% mf=$`@KԆG +MWGu$c=FIFHzny6G%a%7;jÎzp(ؕA.}mP`ɘ5LV&߂r)%8P%!Q2Ǭ9glea4*,&mk赡#,Xg_=6 GOy`hl6,hqPUr5DЇn$T)aC@AT^c b+~_=*:挠Yat)-QWgD\]g VJ{zB!xķX XzS9R=.A +ij)@mnN^bS*?"гrTDņH{ jNFVQ{C +4-}ReYPnyׂC癦x6~ͩrK?rCJag"cL `ͱ1RXǨE]AŀqF6n:y`nZkw#zb7VZw n@r |!mneaDGIhQ*uū#١tm~wPjg1YiOj7Z1s]VD!T4)%L#2הp6XVQ>]{Zo^'>~t~w#]CI+)6BJ“j:sua^Mhه.nʼ{Ync~Rn.z f- MaeW>uެU}k6Az4е1[aha3c!q;vda~fs%8L04N/0 + W,6`jKWoBkJ'YKWX,sװv(sOv1=*F( Y쯊&(J +-Ae|K :p<˴)ۈm-(f`FT'VQ=+\&7&ԉ 3GV-VԢn<5{ڒ9-+ =] ;b |3SmiуE <I\@bέ򑪰z:aڰ`A0N-,4 ns_q3KiQ +zS-c6yᐇ2U1 Ɖi ϱKwFq[$m[ =pajvlk"4tһR9#]X<^Fq' RW*)@-gáY5GF b3qře-LOO_ 5ǃo{tB7X'H]pRФ&| JlXn`@)!)T1I.}x YP"X`.d E$#la 3@}=/ +e0-6B8=ϭLfYSvAHS7mRF+*PtV(sI qf7'b$r˅&nq +0O4ī/=y +{zh|PhNBlgU-W=Ѣɬ[&JAmԦ%TaRiTM+=ɁN&Ԟ2RՖvGew6|o +i2$`CFӞoCkН4"*(#Ǡ0 ?&I"}i5Ht,dXôB`z5ri` FׁX=%yIKl ${I :k7 Y9Ek,u%rrX*zڰ"$3瞔 ^kq5#zijm+5-_q> v&SuBA ٭G(\HQtL <ڧT"4-Vp>%~ +}dE/Tq"aAi䗆S@]Fnm o# 1+MBEϧē>Zf}`} +B%cZCE)`1%.ɈrEieeJ·+j wB1XX h|x)YlN"MkuWjkhd0GIsY^ɹZt!>.YdʢSܭnibC ҡkƴ")YϧFHV̏_uH[;ÏWmFzlf2KF !hȒ(镝Աh˭jLXLRE0P yV*/+$wQ T=34XClΦt8I)0Y +*} Le`Gb';p,!u`^{ha."<ѪTeAPZ|BX!CN@y@[?}挗 _YM6>;$KN +74 =(F 0(0B:MQJpT #w< +^^,ݣDm9-У|#b9SF؛q+˿" Sт1aWvz؛ $A)F6o0X +FGՌ:M᭚`ԭjiFb"m_%@X4 &m|]X08Vc5pHF'd<+h)RĮ"p{ap2 rj9(cOh3$` BgqfyDZ>|!;YB J, ݕH\uXšE~KsV;wNBlC^1C!3!090\a@,dn13*&0u'MdfE᳙7a咠Q$1 @ +^1C%XS;avʂf +mVڵ  + IA`3\Z荸=Vdo+=P +=)qVľG$! geE0 #ؿV :*eG%oOSŷ%~__b<!3H`j|m #D-mǫ0&m)*Y>9A {|~L0 QpȪ}3L`o8. !aCogX&N!7hXoH-$jcI`NIY<%dІΦQXYNQFyi}-k; B]=0bȩ8Qa +&uG4yt.|0*bH>f$(l$QHu!pF'N"ޟ|5zGGh6fV)#%4i{h#衛^w^mƚ:#J6'a%uY[KKOS;GÄF rvm?V7Uf< +VM ˦%x߷zIf[jN_!BKܝBjY#Rd@}~.aNt6 1OZuiYL)Q;Y[hd4Wt}E=a&h}%`EVۼnd3>V9n&C?rn1ǯ^A!>Bu-ṃhipc~{J$=`L3RqCb}E( K b;BRfs1;V߁: C"0Pt!AFnnZ8nQ!p7|"U2nDq+Y }!- _iF8NKz4>/4X" M(}w=P Rz[4 c}B A)!4a owai˔^@{H=,x! Bgc9;0gc._n(Fd?ǕM`B¦+A˄ Gϙ~;f&0&NJ'1uhlRL raf m+6b~LܦceIf~pMQ"&5XE*D;,\L_<1A0oxWsO:m/6DxoC9v0PVsFvB .Bc s:HTk;#Lzѝ/KQ+0|B, vՆ҆Y>"Q hqzRkLmhn5mĚ`ʇtvIAبUKaNȶ&4WI0| +*5 C7G%@43MRt}Xpqbh[8<$>3_/g6a 䓎QQbj1: +v^ԔyVSua^T2ͨZGsb=Hhy-n8m` >sJc7♡k;;JD2^l{0i ,oҬgv=\xLCݓ~h|h0lcFF51`d8D !aje}Ԅl8"=%{f~TJE(Nb + PΊ)\jnݣJd&XtX6&#'Lp3kAHNR>ِR[}N;4p'n~رх*J>1yjsd2ѻQ8ije8D.Cq{~54R4P&eN:um>Xf/DDqخͮA"!a6iG`v /B[WGXwPtv#Zvڎدi,th8+v ǹw,́K; 􅔄@s`0J`B^9ylAҁJmla"Y!Z*ej.%fx[buHJ:f:̿y :haA*@&+ZM'|e(_3Q _rab1ÃҎ mL0o?Hqzq'2$BiyCNJ@N7 B=sLGܪj#t, YD=!mbteq +O^gzw[ Lh!=Ԓa`>#b%nEԌ34,V8Q(A+QzآhH9&e[ ̠@5i 0ℷAZ O ~uD^Z9A5j/EbOA'֦!k@ _'Ec&اJ}ܶ!pF "b mo +XI%@Bt!1+ݞivywL .Rי "q;Y)1<%%謘zj,/'X̭Vˁ vşCjmQ &h]jAjhV0_Yw_FHNƍn˖SzD$(Y =4kqô ST|>"v:lV :XóѧB!SH8EXƝ "Ak!Xq}%QZ ԄV1XkƄVK}no uml_/Yl⑖x+Cu m%,Gxw0*懶mr&3AֺK5udO qA?**C+c[Q%h XBU&= +^aBl C|% 9XĸZg٤ Hرɶ*k;8tMf=l-NJ3 +pM>1j>i c f-L)Fƈ@ {[vIcYi ^$Tsl zD/&WbO~Xu }eH/Pf(Nq m&1ğf[-?M~I/`"⓰ub%:b)TJHѐ,' Iy6U,kcjaoK<ƈ0$rM  ?4A/<GK*s94ad:b:SSMD朰>渼 ՛E7)0X32bt8^;e1𹽃ɚ=VK)Grl3<:v:.nu k&'O5@[,_z勎h-<η M2ACl@#NftJtHڗ5\%hB]m6؍cB̉=V|p XXLODn ;@Ԉ^ wꎿ̀yJ&jot cm zIPI /f;@ׁo{C"Gp] O408SɉBbd!,HKx>#-0cy=;tFe6bkDK5I 6 ~GKxǷ)mFh0JC߈B H|1 Doj~pзGBLIBHV@JmX1/CgG(G a ,4z +ߞɹD8 %lUH"*X?%+  O@JR`9+l|C@n4fKt@ c$I:0Ϊ^5@RgYyGC`sA3<"`F$Kܟ4W2IfHD){v R`u.ܼ|&|A_=[Lg_auk J9fuzI6D@= jI΄]y/C$ŎV~l\.A~lVC#8w:XE8|ť +X%6RaiYN_ߡKk߹jM#F5O1P +f- 䠎Qr"I`>ճ KW w f̺d_ﶅI4"P$"e[ 4- /?}4"d/ >9!c7Th5" j6iΙa…K[1}ƳfI(+[L w0!+xsIHQ@'0Z%zd|u& \:LE* A"Z&G[1 c co' _eO⺅At sgOI|g.5tsW%wj$@dAzC"UĦCRpg |nű׆7^4'Dl1Iךؿu`AS"IIC^*±Mas ~ڋ JXGTs3!:#%yMj5$5G%?K3YJ0pSYM3G\ͩI[`sDy0DAQ[ :P*2ŠKփG[n(q+ 5J$1 s9C/>jMZGcoO/D(3ұ5 ~HWh(D Wժ!@FMZB6"6@ wP1X3[,ijXC\Od!52dXT0g0*&3kp+(y̴|B[QRZfM#GIVz;} Uv`ܑߊB8۞$y'990)MNT59‰CV+ +3Ȯ!Ou̓Z,]DȁXa᷻V+݄lP\oK!1[%=jsI&jY^6fTJUv`-'l@ *3".bu3CF(Xvkö~dˇ2>b !p~efx 8 r;1*[842HZا_lO^Ӈ_/!8gSuƜ)1 upq-@X0.iTgY4A&v%oo%NJt7c,*9r,@&a(; A"(ndgwp9.ɏQ1IΘZt rؿ V90HN-bTA#D BVtJe N(*)#(L)V=93 9r m׃0+cᐟH"#؋NFnfC?Ðj-c +>SfL59fiDA3!) F=d٢\vm'tgJ\e LG2;j3pg,ljB#F 9]&VP[jb CK`QxIT;gMHP(d;g7(keK8:|، ~6NtD,K}C0BXpݷ#јp=?n *;ǿĦiJj>nu ^,OJ3WӌvK0@:k{,%g"wFncO =ФգZЃHjNҐQHߟ۳{8rxNRO:-B#jG Yes`g$j@d#d|HcHWlj#€ܴ}Or.)S2,MD aA!R s0ّY /"lHSBX:$ZΉՙVV+Ȓqf{ rRR׆m}K`Y4!R%kqP-{f9um`0oQC+#xausvD\ܣp:7n zwgD9Ƃ^@ S,SS Etbd/ekI󃛞Cmen SUUD&GSqa==:=+99Q[Uvg=4c)!a7۝V);J0l-;L=gzV_YAUىXHID99#Xi͎a!I9&d~bhD]ޕЈ 2?8I1;u\0Ϥdo\A0C EdRR;ev +즑vΰ +;/,f#9pdC^r{5v}"&a%-9lqTs5nxA>Q?VMkVXc@.oZ:lOA`gnJ:v-B#ky3iK^'zZ=#`!LS"dLxk/ &d NV66'l^3jaU*Fd` Ӫxi1@ڝa/Za%4FtѕXN) ,@tzx>՛S%8SNXi` !E$ڤBB)!S@cKDusaV7"PֈWaLX Waԩ*hdLJ39B3#h9E'|/Gab`Zޜ7hRˀYm\ӻԃ7Tlo-{X + \tŎHy@FMyg1bfe惂CP.,E%Զ݈1U 0Fj,3Xd^5( +דyosDgP + M_) -/A2Oh?RCvtVZm%r-p0@30H`zk#UQ>c2ĄqPݭUߴXу5=]Xl":AMhF[,ET=b%ZuFl~륇+XM韃|F8:ipFL ɮ ==-I- W/S`gbxG b.b*̗#HE5!̃篮y0e$E ߞ2R%VV<}E~C(L,8F +B/Y9i|/^޼ +'OO%ju6@s;55fq={8krvČbgpVnzTrp$a\z,6F@/Új?h؁,2ky"f9&[I(X Z޷Ʌ\]0zt֨ea"0_-sHɵ?g v5Ip:j##N<D(*`n +U"ү!.Wg[ʔgyŴrk90u8(X/eMpԺȝ.e#PF+ ~!耺)?nU]\}%GPg@t}mk!dBflZ35,"ѯV:gEM& L6!#oRp29ThҜP "JPƦiO7m`| ~%}z޽r ڍJ*"TY7^gA;s 7qC:C/9˿y' +T~Ͽ}gżWݼgW7ݷO~zkTT@"!bWklxB0ܨI(% &T;^HKUwC8md#UloN$HΒT: x1@Wq\?E +D1w*` GTˑV$;,>ȟԟY7?]ɑ8@F$M3ұGG>\BKC̈́E +:5S[_kxKMyBF\҈W!e|2oY|fdY GQa(dȖ_I=T`![+P<1 {D!LyI6Ab|%GX=8@guXIg8y_r tg8ySPzhvC<>.!9H|O:zy+o{w!-Kl1N8\Idwuǒ=2#%4M6_h G7[q3PF 'BUpòt\,s|,8ppK ^aM@Z5;D8)'9Xk6`˗$5>z|z6kŁ XtV'5"{-U;p"݅FiƳHXKnA%|]dFJXZs{dim89v;5oFVA̦PRgT'b<~NLδŧi?k8V C136I494ȳEj嚨Hp !e?c;8 +ziO9.8&PkID{DT+M V"$-$9qdˋ' CFHJ00ӮV:%˦1G "h¡X8`Zi˒B;e.Sr&7%H3YaKþ{RMp] )l~rBڝMg޳Y͖a†K&ա&*O(z QqVDSN\@I`F91:(xbKd+IۉsQ'(o[QM,c FAsk?6ť5QIsT@x&ʰJ LNM_"˫ R>q5(X32MBN:1 = ҋSٯvFf$=ť4f *R0\̀$-Z1WSƕ׆[a 6*ӛW\٪: .$ +ka!@ a-\5@dH0^"Ν낃Б1B<[ .y;!bP#5Σ|vB^ЉM-pI^,T.:ϩOʍ6g]#t(M8%ʇDU*@NQF`JY ! +cf̀n*PL"&EmuRO1"]XƂTL̺i鑩b6c%A+fڳҭm?Uf$JڝL%Wt%<b؎zBD^;-1Qی .Po5!vW8J8L`]1SqlR-~:XHI9^lE!W*ԞAlF e )dY8&=eBCvOdM%G$0!8ΰy3#/ Vׁ鈧/, 5>Jeb0 +jRuEs2n"qp5vu1ɍ+J2j\ o2 *iE!/pUsbQPc +i; +0+xQ2mq\#\O(̴>,2VՅ}%#uw7!B=Iߒj !ylud { ^DTa "ӏwRmH#!ibm J? M"{Zg+48Z!JE [wKR:{8F`è&>Փ31APRtڣ)ʩH ޝ:=J[S\ٷXAf{-@`]GT4:]JUxsδM"#j[ԄM$`?3nJѓ Ep{,]D%xFA_϶(UQý^ =׹(\lv% c;_d=0lX!nCG9QjS#@:ϰ 'T)4;3?Ci!W[iW> lC$)j ϟP2w)$(j%;`EPL 6AT%WН][;K5%א"+(a`Xfu-TӛW4=ʥ@"Ϝjz@pj$OfQ}J|et9)`Qis*C԰vۡJU;MSTJ uP/~Xԙ$ӜOළ :!hD \FN] +y)2W..]Hx5^ETfka=`ٽvdtDC`v_yKfHs;mwl1wgtQB8I @"vs7=@TIϵ +ZgqfFW gaJ%RtȺȮL#̮z}>aNnP ٘M8<㒛qV!GIxs[ݱNуz{yw ݸ,ݲD˚KNg9SF/Mc9FnrSN:JQ#tx]xmd0ˋ;˽8_p +~DϜG0D6FjC&Xa÷.9Һ9&,mzxFMAez4G_\_pƀ\a'5"TCB8ZsyN}G)Cp_]'4(8`?fȑq/3 +I.o5 l÷ֆ>P/y ;`vJ ţMsWA4|bKԀkUJ&+|K%%YyA 8}'5?Ѻ;->28|n`5^֓Hz܁"٫ -FIBfa`/Q/%\+g-;# +EDp\ԙ5a +HS87kEpmT!*aDH%M64-}zSw#e8cRQ;FP5tMlV!L<83PֹcÛ:Yl $uGӗZAn.\HF]%x#1#P̺Dɩh}sS*u-ݘ( .F!Jb0v  +4L)սQLڤX$OZCzqd/d"IT'D=  IVq7筕h#VΞA]=ȡ"~Z nX +Wx %x)f33:cB\kSBT:ȵ ל4RHXk[] 1dHAѷ^,lZH1$k6D7f"wq`9zts䄃1-fdj-DndK~t]5;X'ywv1b]Ak]4(n 6>{W,B5tdpl3o 2ye~1re +s +a/jֱmжd%jf76Iesdݣ,R6Ǝԣ .~2w >lRq6\ErS󖜽rÿ"[XE Hsg Tb9 rD?l"~esN@,]ߋ qN/5W$ AXP;$vNGhsl.ǟXσOFIl~K\(2c3q3#Bd?1=kh30-ЀͮR"j'KaQؐè iTgh9IKϲCw+$Dn#2:'dٞ8b( / *V?o'hOOr8~! +E + 66ÝGA7w!VlzT%ȧ<wZm\ ghxST8OHKAѼ/"Õ0$GIS -ӴB&{=УLVtWUUTMG0s؅Mc@[#d)k,#Bʞ@A_d^uY]e﬍| dyiʪ I"Fxto*qK F*L2:B58x*-gMD[1؂?|X<$hv5:ۼR3/DGX9 A"jCU닜\Kw_5ؑ}E"PhK%jh +3r08`]wNC+V:6{3d Op[ -nl 'ȷN~r]0 +6E aJM +H2xntgԕ20'{o$Y:+#B{gmo7PAP;\HwcJ3޲,a!N0:7c|FQي-,qwE K@mEٵ&r (D=% -[}oZ(|>PTuv{qL$CؚHC<\V<}AD3vB*6o^??[Zĸ1gSDj"1AgΤb#yJgǏ˰iPE oaE@BRKDfL>KjJ TS&V ((Y@FY"B@m6o\ҞCӅ m2D`$C-5A,F}mi+#i0$Ѹ 3upyg',\ Zl:BOSʨG 1NH +놛(EVWlN'`{"%G +;/M7p$ LZ6P~Gf2KDDUD_h`%9%ifpΩEj~(t@ g6i_Ǟ0sS34]17:G `mV&0Uq vMŀz#~_:k8-}:r $>WF;hL3;xFpKe, ݈E$1:OJ Zu$Bg(}:N֎" L w,}v,_ ͤ x2 Br+uJ}SԜ̎e1K teDJr[5 hU)*YA527IhԀ%P H hQ }Sh|Y4Ԑ>u$ {*rK+SC_ZÁ賈ifpgVa6x?:.PUC-^tA͍a m\nXHhT9ZOeuuCHƝq ų^AL55 +77 m])n3{J;XLTI($0orgX1IKݶ_6w"B'!!l2%E@e}M8@B>-mr(JҫRQxVߥ-y $9/S85>'5eiLekLj9;]rt%oPPQ%VCYi"1N@tɻ(Tf+0X2;:bwCr[EBaNO-*36\ +Ij՜cXZ40ѠOB> \iQo&e^ +!"UȀy#t'\ MA|^J S$G@OGt +S vQuhݰ>̷0\إgRegB)&ҬSJTbYL,]rq/EE9Xi~; sw>BE9OǴwߧZZ$!UB +i=0Df!cx$ԾjWt0]D&ӸЕ\@v)YP'UNcc +e,s]ʮ7NU 1@\!B] +GP_S)ЦIOj# ZGq bi +Ԃ9lIGLP6QjK"<)}4OTE4Y=2.NQF+Kh1yƂ^\|z=ԧJYV7kp{Mm.~hjG$搑TXn-E- _h5U>:ċNb:+cS,F-GaUlKwC;`ۖIkH:c硣@< d [JfgEOXpiAQEMz.>)Ȃ4(c~&R.X(SWb`Lj+#0d9#f/wDWL̄ ԀIN /#f])N\[Wa e>RIf$~=Q]E*[j?1'r|]Y~, W(wMtDyaKx{( 4uc +|Lw)R^L٩hipP~r{ܝ]ʇ6`_"D(4ۧ)ȑdK!*B=翾 WZ4VؒC)AAjLyc)&Un7=;*C&7!R^+MYG؄ fGL4&#*'#2ehI .Bw̮bOq͹=,5ahPCGðIeTQO%59li DiR?Q)q& ++'*ހI;t^wtf;|w 2m9%xaҎTԐvlnςm\fQlv3ttPT`4-)WV6Xve@A͠{645cO2kRtYtjK`']1s^+':u!8-ëHg@ݶƕc7>D`09lqݙx\}NAZRg/[NDSw)a-&Ĉ41cU#EH (~ Gkd<]F覮s̑*5&v2Ug6dCUle{Bc^}bhAMs:B Jna覐tҥU6, ER?!z5r06/ rY.~Ejfza&AAV$mQf}Ѩ&#0`%8χ$|>IM`ׁD+20#J TwϲK$ңrncڋg;E %avȁWȊ +re>SV,+Y2d:H_S Y.gams_#Q?.nN#~~_ʯăwUyHlR6‡~'"4((m>]}&F:=W.43}U@L3r?S +6Ǭ ;ՙ_ K[L48Ne'~椀b|+=Z6zT/@  *b(Ø[o4֡LG بRό1#.wp|7;YSR̵Zgɷ CM܀=EL@y ` Kg{&daRXiң!c fٞff̌@nǎ)M&tpnаZED@BȺTS|:1#@7=bE~ .!>e'jF+U dFg{: _[ۃa0觨6/L(6P(;%d,&UsY~8R-^/o: ^,<%z/ʁH^~CG] HM%zpX=,hc%IܡU:q@ʨtz'rFVN1$۶44j8ƺ5JWu>lj] O@2(&+݄%VwO7*DF$󓏌 Ō.M)kE g0Ӱ4RHE%mԝ +V3g/͟ߕ&s`f)ԅ7` gETe/6Q":K-S FSX@ri bu':`Jv_v$d办PcO3ZL.%B%P$`񳷟USs*7GbW(p !/4 =.Fzg?Z[Zo5֞"K-T8rpP PXۋ+!Qg?@U X8RWEzV> +6iRxO3.: j,bFT:+* +ri5Tt B$+ +iňxd;T:tX ?g_q S'|r虁Ը/"pQ%̀ \;zI~gVa"dONm _.;Êܠ3w cNQ@~ +J"Qa2_xG_*[>|%[1B+:NZHՃ#I vj iR#Qam'›6 Sh] L#h.`'cу-7UKFI$+w0 e0buyc$ K'#qe(s4bfE)ȰD1cz:ʸoRpsPOa~.pr8*ub޿Y/AYtK0k +6}=99"{C2bU5@=CzVBy;?ӡ>ΦddJGB4BFVT4.db<pR1Dg*GMYt+9&\aSԷ+7 keBSZMd5(>P&`r3C BgZy* +?f_4d}5{^zd 5-5ަ}+)ӗՈ}O`#"<V]s_ߖslϰ>H vYsž3 QUˤTwLm #/)˓'jT#b#ylVYf$gޭ+%N"NȳAzg LCX%s3 nɛQQz4\VgE'!$R^,5e7N^pՂ_˿FGe~TVǕoPhW俾IOڥ@v)L#Erg[Bv!"H4\>[(ehrSmO1982 +vf7N|3|g|DxSmW-w}Z'z ^*>^W/h/磆6RO#@NJS8<72TZȋhN~ %4ø旰D4_%CO x;kGXȸ>k\D )|ǡ)y4?0Wj8GLBʱaB +rbwY 추&LN*DN"<4*SN|Ʃt֢,lt<!}Yo;XHM؄;TiUkjGgb {*ZDS6עwH F8N&hvOV?' +&=`u; r63I/3%y@O@3=7],&'M.Z\)г&U*jB2BJU2@)>^RsZl`G13!n!UO5 +"j,iiU薖 7=%p4Խ u~HShAA͒VMrV,oBX +L^5rL⠌~gfN3!iY|֨k4D]MF BaeF{MV3&u6SQCc<l~ƭnnKGhܲ+FʃJbܐDLӃB@W^/ag Գa[(c>.M;J RFX,0 F *f rHSBI6:!j]j]<>oF]ͬЛ~R2[BWLl̚LW(g>u1z*vb8(AXWo"u*̫[& k&􌶷(4]ERƝŎmyϭQP MMknW =(aSa7Z 0X jbE4z_4[x'=HJ\D*b#e" iII&QX}{q{PLƽV1Z8nA΃BݾP` _L n=V=mi9t[w'dڌ]|wt lԤ#lEwh1t *W "0&BWGU-w,B];.2d7 gM}? ֻ2L((Vo8i9': 7d!?Xv{jm7JMNX$#d# W6a== sI'8ʒ@ lwvKywuxMO?x b'Wdk0V4Zqn"ÑC@٢'[H+@%tNR!D;e[n:h@1BzPt80W˹3E%j3Ь® J؉Ɂ|)`>Pds %T+~: S`@fvbS0X\#~"*P製,AJvF<8PfC'qz +T#ЇbCs88?Yuhf}h ɔ +*(p -%7c5n<ȈM7]cB-pָ8əzKw\tąkk4MS ((e<@ũ0Y/dZ Tw.]y 1I( r$u_ɦc`|XD’dǃu "s3f؞aL'OedPZ/GuAV +H>:Dį jhPミ9rgh=p de/Dcq%gZwIa) ^@JkK !lXmAڛSF>e Li1> 9T}(D$KES|ER<ƥXqvT<_* #SB4Fu6g +O*H8CTEYnzv$O +e +S4?/ۭd@x)dW/TEL4 +)wÒ!~gT/aӒC %ǻ$qfTKFY6e_d_|U;.n-"Ĉ^c ./ YAj` ؽ#JOTj7z, +P bܦyG1ciZ{=EA*aպ|J)/!/rCE"E| z@$+Yh`3w&u<_t{b 4C7s*IR.&O%xmgƷ} g5tk1TMp+G>T$kE* TSC1& 4(F,tR5ܹ:KsF)RS9>TGm?wM(% d6+U9'G&C@0Uڳ+9E[45Bhs 0,GSM l;nx$R;D*ɂ]"#DRrL,*J,aC$hjΖ\ r*hxAJI!Agx*HX1fƓ#f!wˏg)pN#@T+ +,BQlqDFI6r{A))gXb#iTF`0!U_z-Ҁlk͓]ό΋@5E^'hdaBz=Bid|7Ib=|䆤HB14ϭcC4Q?K'ŃD%Ł=jPW"Y)5E(ሆ[/?"}Fb׎@:8#h=Mk"ƴꙉ="{xRf z9etNTԧF,oxŔ(ۄɉ=+ut Ta1әi ¿*y> #/nI0ORg!9:/\FuEz1|TqOڷ!aĿȍm:zP RЊDƳ1-7.D6hPDuq#xPA#? [לe" NBjwq#^/VDIxzũ`#(`23B!DAGpBaU>EJ-,U446Ѕ+i-Uy@:Y#:VLƁתp=, +bm09 *U4vpHvƒԜ;g9T f^%Mc2EyW^^:/ M:G8p(}aNT-P %^%\IPr1аЁmgqŌiݜN o j2BNP4BwRk|3/AЈaSY`D5:lͰiVY*qZHU-Zɔ+F<͘~'j +SRbw5X;{ ~Ptk ޻|MhmVULݘ5xJV=?\3\5t"e㼙Dt)]^4"@k-G,Nx2V82!`K |(Ƭtވ%29pB'~jnL֬Y-Sfth@_m]m3/ c[^augѺ3r_5%"[XP3k_e1*FY n;n +_#42; txfݞg#@dEdKN6Ꝉ`)/qa_-ז +HrX4"YHHPYo#'?]XEE3p?iB +vRgs3bU\*2 +F5.gҪ@ &Z ^7ǨFETxJ5da?qCƵ4tάr\"a[v%[dZo@߾`"s@Ʌm#* -N5~=f,+ 0B)`;Kd +b. (!ĕnBtaVuRQ dxH,tI@C9 +25FRd2@BJ =yjb)d^_Vp6[31Џ/k8mmq<#Xԣ'U.T ,Pq3M֊<[xcb'|LΒ(`XC)%G=QY%Ajy$p+FhϢD<5Dߩ}:e&"(z*5Oe=yԦEkQNT0Tj!f!80(+\F FrPmBmFCE5#OLi$%]R67MY5B +(71.%ؒר=0uZ Aef&=و'zb*`HbCJP:rU rb{POKJˀC|5i,㠃JaI\4/8L?@A7)M8^[?7Z)~Yglm ;̮ 5؅ŸcߗX8yiԹ9_ È_)yEqļ{5( ~Q0P2i2r(D 2"C\ٷ[f3PR gɇ +,"G1/t-;3eΆ!,~ ^ +96/ڌ*}JEkz6Y``]Gh'ʒɶ=잨r {~-A%%gRAdfx +}O}Ȳv}G [y(ZA +ӶlwRۛl[GYS +yPqQc 8%tӄB@tQV m +cy[=:=Y:T͘h+lQy IF,_FfbgDțzRݩp1ko] |[ljbi93]Wrd>0[+ c9Y*mW=|cQ[~#Ja:1rX$ ,Gѳxx;1\={?ak^ V܈Y4?TczҎzX6Q{F>tM֧NA"9R + E߆Y3D b~FۗQ_UD<|oEb(9J; :Uu{)P6AQ@xׂhQϤ U>,,B p +ҳ`̓./T8@c*$R3kryR2nabAh~ؽԋ#W gi7>MlČBNr +nXLAjļ/K'㻂Ǖ M0 +NDyD螕)q >?XxQzWn zrKj H tc0 +Ψ8#L7W\̎aņ8UT[ŝP6)@Y(/- r3Lx_j7V^_K?Oss[DqX]'ve~ʄC2m >%Z}F%ֺ,FC{(i`O,u lSpʒ7D^1:-~CgJ^! +QMz|z=[lA4IK) YQ=-xX-EG>,e~# E_Fq,g +a8)I yJ)[J!7 ;,]V@EE鲕ho)Q:챳JʼnYf˔shT/;lxbWM](Vg}sr=4ˉO-/}LSLsr(JȞ9iML * {G&|cu xlS}4C e9ЉcH/1@IgS_s$RLWN@ͫiSXztP~N/YqU@a0H64ޠ#]xi.V~o/†|qiB&ekś|N}gLhu^x2 R52[е7B#a硠'P -sg L MW3= VOt+uINEgSQKe%$h s ,Ln+e$`tPnծ@}vb4 5n͂e'h]pw@*M*G蒯eWhHex{ q/I|B%{[ ФStW1-IksHŚ=-}1Z2+ïp  +(%S\3DP[>5^{ +D^iGtTu%Ǎ i1l&,3[@]#tjg9K']b]\]e :\*Xn\@&W֓cb + +TDxr; +R+nU䅩AJ/P3 +[1?4?/K1Ǭ;>N ʍ,mQau)BHt8[0A  +\0I}2=P+fU#NAbJ@~hP*>!\)O ӀbQ`DUV7RBin[^Ӫ t,(r6ָ1[7PRB jdT,ٞe>z=wR4K+Ry;νy9(v{FP8!M4ܫTKJ@gEq< !Oٴҵ YQ"Qҋv*mT(Lé-Uq F ziLjWMK]þ 3IG@&KN;OE`83FmS~Źdi@c@M%U{@n'*/sS+.v\Ԗ#5 #xzGԛwPi ghC0udцQs6'HN2dfŸZ d%'.~=id{ B| +mjdpȣg4Y0tA)VPsIiK?|'y%LdX(ŀjMHNǵf 9\,@(0)j(#@7S#ԿpxӁo*I3ஃ ųtk[np.CȈEy%=M"4 ?09OاG Bv+Jut@"q|ь*餡||&Mcκa#( |Ĺt0.ckMu>XDŨn?W% ZA:{Köbє8V < m1Nd'n1ҮH.@&2*@褀Q +;<0f@kgEԟOonju8ϰ; Pbx~hI X.ѼlMN0x"0+z2llqw њ1=ѵ}m *.VHT$h: d~(bA"KbAz䒱~=]Yßlt?3[H e+A?RT+H--n;o^ͯ#܀x{5;r'R8Vݴhv_}RL̳`$aѶSw6M&LjԍpQzDb&͋b{ັuJy%)Ǖ e+نfec:s2K8%9i}}1J:#=%2nG;1X?% Gh"8"h+:ߩGq21AWKo!BKz:h[5.hN<}K[/*DqP22Ƕb#a0{=hFЅ?jf1Yev+PT#I j*cϰB0@N| XmӃy5;VLvP&a8 L@2ЙfzMqhcr2̙JꙪg%0Ly:Od#cycYI5V%#ZM[憻-9em"PH fV%зQn3r8@#^/6_xq;#x^5aѴb +eܸ^БnK,j1v?w{&Nɟ3e,(ruA\NL}RS:{ ΁X'^#!n;" a\(NiP=]AS3gv&J>fǂSD)B<|rg>**GQ<\͏a#;C˒aAc{Wl|Y/7=DoF6S0~0P@Tzt7z2d>ބQJ:R(^tٜ`mv2z"{ېOH@tFpG9F R*rZ蹨?R[Yb: >6#tq5,ahKXؤgGLmIXuX۟_S@A/|@A&ahX{.IjGe$ph/c>,S VOu(WrL==Lt(5YH I@>YW!5f ifIJ0# zAPQ>G.l)ԅ^Y@;Sri~UM5N:PbL81kt+ꥠ,8Yz aH(R4H,5)+\Kˆ$U[) N%9k4n?耱Qsgmv^j°cR2^jy g{h8l!IaJ`S[,}ݑHM_"^=ozۋe{(:3PRԗ~=Vw]B lK|qRTVA@P䘽]`,+qҵ-AAT&9:}@DSv_hv!L]bE]6X5$ T|j<1S($_Gi1"ZaM&@60$&2 +-fE8#}> Z|B!݀tjRLujPjLR8<3o@5WQば)=R ^.`,)'BC=P#~WW{k+;B-yrnj8o[੎9d]PAԷqRQL +;~\%BR,[;?y9¨ZHYUϠ\L܄f{#Ppe/vi@waY}ѐQXEuEu>a4ήW.JȕTn{z?H@;0Kgؙt@e$MznE}/ +=H#fDQ+UصpP]1e1=CDUZ jGhUYf:T6fha<x +p%iq.BxHB֧Y+J!i R. PSB8~wi$emnŀGhWud>YPaRYat/ M9CxUh8x[|A9B ϋpx{\:nYtnn( <2V4ʊ Sfs c#u=I7VK!& 82K|L;_2>R&}Jj]j\⮠mg'%>i^({v ~v #T'\R(I4R+F3;8H %z& qH gORAp<Pd2VWFwT& +j:«-u@ϻARJ+"| C J膽r4nX(R~錅)Fl=HXekę+ Z?@JbvS,[x^ hG{U%L>}ZgH,.a&4'نfq +nЬ}ri})f ѹ +:BM+?6C~ $='#QeS5Đ^o—F"p )\80E&N1ڳ,5,}ȠķTL;ԃij) .!>u`Š f@K[kб;KٱxXX\;h!O{)> *? .@-jAVi.ekpD\%Ki}ʚҴrT}'XAԙ ~ 5:,볥Uit\A:H p8*lyּO*m"-M?nūÃubwߵKe+oBg[[ѽfs@-e[kDE-y +b SM9G[pKH+B(rЬ_7cL4|9`͡k= ++IOʹl=Eb́`9<dEP#䙍+=а +]\~^SN#(*-`nPLJO3Dt`63hvcb`+4~P`jmb4d$ff3u-T=c]h#Mtx)_"P"[x , Xb 9lhFVta45fGI߯0G1皩 Q +`PB u6V!%s?'=<'`C9^34DC0;M1)BoQ'8X\zBlkV㌌@m%_-8R#wS=0RG~ by9.H!g_0{ބGL[[1Eā̡ +ayѺ?78!3=a,^.*;߈4 {h,< +C]H7zEOx:xKkJT!qB `\?ixS;鴶xu^EbfBŷ~et"Ax dDi\V']3 YNک(\" oҬZE.PC#<#0g*gY9}(w@axsSN|Ǚ:Swh}#gqhR`}ؙe \?籠s(A wn!IW:bŽ)*ɖWy':pr&ԋc=;ڷ˯~!%;,l1%3|U$Q8̸" qRh[y!DeO#VCmtEB(wbol~8ud}mTpn^tn/{g^Oy9!5-/DC]f<>B="2UEOv@sCCr!PvHa"G8NŠvOZC&5d*Jk iCRK6|4)C C7̳V/a۬`:{yKGwМvuD "Pbx7t Ox ͙*(MdM+=cF⾈*.8~Vgf"bCqc8"㤢[eX:Gy`%)eQ7G3p[w (>1PYcpA76~%(^QЏ'S(%4UJܣy; xBةoԚnʖ0 ut^>wˎDbhV+-.n|ƫMŸs3B 4Bd@.D=eG?띬ggXUjf"TQjD;Fl6M#5G Lj`FIQti!@(=L)pPZ N +,oV-çt}P=FTr0cK38!U2F9~+[:9Wz eSxo%l +0 c/3,(ӜIL"BY{H:kZrـ*-CPK9w+>"kT |A΂bȆ7 f}؛}Xv?o$|Go;ZvqugL40ºVvcTf9B*j윚; +Ep !mÁZ{#E̚L; | 3:4?1:2VY(S~.s!(Ϡx1+D@Vn'R0dusJ+5xr#V6fLFŠ4>fwhEH9`;,x_48@WOk@ߺ: dDR6\ + oj4 Dʸ7E@p@|@jSՈZ$ n,t;^ U$򟾂OjE( %X̡f#^ZMQ@`HKɁ J C*jNx +^zhαQoaN6kT@/;_ mQ&bXJ7{ѤĒBaBY}He'VFMGR1QRďa +|-TѱT071?ؤΨSyOM*ujQ28B1Dt/} N "t'<|H !h]Pʑ0!كšX@ u nFV!ڏu|{VY(y=wg[TaEh6.i`1>k/s*Y^T-@葹d;B:WWLZؓex%u=Լ>u5WHi{ ~ǟrzFF-d+ZY?fkGΤ^%6>ԑRZ ~^.Q38,4#2|WU^-ԨNNӾ hv/4CwQ^1yvB5ICי&lPԹ hxg5܏zև}ƓoPdtU+:gB2 +EKQ'͍Au]MIJw"s2J:|Ȉ3X<*@@ӥgz =-OCC":褢nJvdc VtҐwZVϋ("kRnDYNFjhgh'成Dbf$z- ʚ'= ,+QHlLYjdy+i$ZJ6׃nD6%<O0 L16=11OШCƕ@Bi]Ꝉ^}gPlN}~3VşC8 q[LZ_^&U/,ߺfv`ح#֏u33j ֮%W+/Hy\Wޅ<Mqid}a(!4lZm. )H:˳{e#ZCh mp[6:ogY0Z,0+F#/[q E銓cՏ~ h6_C(mA%l ˤkX +~K ^iUy +l\ A$JА_Fĩ2˕:7}BK[s/fUuD6ӻ:NE:V'1b' +EN TL6K& +dVSq! #,Nw C/Ќ|ψ ]ۑ$ %Z_p2?/hX;HJ=\G +O{쫺-G!Z"4ik!6@6^m@F%[~ydjIi"$ϵEAa% R %1TFe;t+w|!7 ' Ěz1\ rn1hgq<J&wvFuvW准GX{ 杗hN:kPFLADz0BK%Z-d\-Jᨀ?l{ +<<  sA.n,гrQƓG!x|6eUS?kܣl0[wjKAPz:@_nao+: y¶|m`}dQ.y7[At_ͤ@z~ +ew,64C4SNB7"Wɇ>0/DW|@i}$qI*fGaҎϥg(bYlbiߍJ&ju!:LSJy)̷:BaHL`%R2LsRygdTPs) *<_[+奥EAGZa{lʈ!`֟A /c}e-C g-TY(I}6vwF&Iޡ_A{ &х+F@Rq D'"~r|:O  ݢNs[.x!gMZJ7D8C!sJ=apȀUV(I%"}'8W2/[UOq 8S9$(!DWe +}US/zu].ʎu)b-ѕid`aYx{3眑;y{d%z+_q]jrc,Q +oO:&]v&RPE`y*RG 5& -ch)04>AS/qz+.Tǩ(t6Az{%Y83ȕLDi`5TЩF![ۊy[Q +Bq46C Em 0`_-$T>X|cJF,5ꔌ?Բ/j J\|A[Z$KEx1JfɄ7SASFYX3BRG7EE*ʂ=PDz%+5V̆IE6&Npu\M\lFZ­95‚kpQo!um- cɠ+%D匉[ +  .hs%IT~}HK)30Q[ >5u<TԎ K)r *]*D"w5a铕a1E{~z.<虿*p}v8mMhfr2RѤhg!NtrEvNf%!Gp,(f/!y*D:`R^,kys0T{u+?25׍Uӷq1{ $f! "FR\IYQ.D Jb']U51]`tJ/t,YKa3wiy/mi8/Rw#\Yg +Uߜ30l|s0?]y[}=|Azoqcep_0$ DB,snu8@RvD$"U &@+!hэ%s?rX1@B$$ LBf+QsuJ2$>V] ԃel=L@@d%:L[L2ñȨZ"E9zL\)(tL/ +1Rى.Z0-bHHqI`/?9  =9$H c'Qؚg~m[ aD4|(^3jYv͘i)G!6܄`JPs-9(qy w\K-DF E,E'٬6}qe0.'a`@!eF$KC(LsZ&lj[ FsJ8+*06LȇcG0h(2#81Ht}f +Ȅh2 !\IX/`$zdonޟ?t4" +rj#^ʢQ8m:IsPiDd 967A`r&_ ]|x[-*G1%ЀD*P4On+ۖɬ0#t>pzÎ0Hxy`EVU`(vPd!i4Tn$%p<Yak"2$ Pe^(EgGFiJ!k&ystd\x}QSh(RuM; j$H1?ʃ|e@w(g.]zC\jV>6#8XвH7~2UA bGxOS@caa +J0 =hw9y,f +NjAox!9 G+Bv +6cB9CFZN!D&!Jp<%XKbH/b7ѥ0\HQ.Kߐ hBMujo_d6jЃ\!Wr,̡Rޘ'd{x!rHڂ͠_7La6ɛ-+0CL&T8Ia9$ 2VD]DBKZOM})YLxDw3&7Y`Y`LORdym tjAߐ#6Ey"% +iWMg z̡Mt/ᦼ&ay92SʉCfӁoX<LjzI(2'$Գ7&S[}Aj+$#R&8q VZ&$F U6ab0}d(,s U0,?A*4b\bEȧ$,_p9,P^T)2 &ʊǗp! 4CKRm'0ʒYL ^ 5G -- "_+IQy-ݸ^oDA#x=$Kd[&!dJ +?'# |hPt_6hB[AEpII>VJ4[ +*E\%@^:nd1VK0JUP}t#,r|s,$=qĨppcpYYbF=Ġ%q 0`0B|#+&O"G^m(ב@A'xJ xy">P!qB@Mpp|$͐` ;eI #[HJWDĎ$#E\3#aЄAbgƗ +'"&9ˑl9ѐ]eaIxA9 " +5pKbԀP hѠ4 -rE"q u&9!3I)>)1HzC 5=-NÈPmO7h, r8 IAQ#$WLba.Jn θvIOR&4')0:`f[0zؐ) 'q"e&(HI 7*F%#AquϨ\E!L^.eBWH} kB_UB\DZBx6|h#0@aX00\]@DW1v[jZh1lB(Mgץaj>",}+L\(h](CEFDV#1eh#Sni@創 +:4l\ej`e? %ւ$ )!{ HXdƦ,4JQ/%j ƢHaFO7#Z&`)"ȓ ¼P:`+kVARPS(l }o1l,@yKrl2,>% +*u" xLж[qҰ M>(RF6ì(c~@Ly0$ GQ5P䫻I|z_L(~+5as'HIVer|=X~ FU@tB"&l{!;d@jo$$(d!X006a l`Sb&B* MD A$DI? +:d )LQ'^"Us8hj!dOp I' ,Ȱm¶UYvȸc>+^?;+e3AhP@; +(7b,/FD;הWBXZbt&T s<ԋxܓWL]H )I*hxpK(WEtQM@\ß/4. #yм-sVpУJfO2AI,D8YL` nՇI>h$I*!P@"[!Fe)C.I6A E@%V"3#RlR"k E\^"w\.Q8FQMgt0j'mwɄ)w ȓȲlk`װ?z|\PyX¬g8 ,$I![' Ht)aV"A'e& \³HK<}ckOmB\0P"m  $Z$ϡ&ZmbWx` 8E.Bw e~ATGQ`2^9AYfZ;F%Q+`0.[q-DC$j +-BP6"̶QnmӰ<$@)Kۂ0:^a)G2͆BeC1K8xj6]O,Af T{cҌE(6e%+N0Ԗ|[{gٖL@~<PP!%dOY~)G c&K&eX]QptRNkqsS842.锎1ál)B XMTW+<0[惤,&;Bdv|ßJ\`' E(lRJƌ# *$-/pTeRMJ #"y!"桄iZJ99uB%=5.Lzu,ʀ!jCE :Q|_i6fP"b 6[5 gL0V }q PD?y>|*W?<VSb⑄Qlp*w(=H0" Z@$#Xh<1۠E ">OY$9|~dՍeC>ؘK1FYȓ@QhfRlS}ML9.M0Chqg^I22:0=#U +h,|P7M\8h|kN_E%kCgB܉-*eɤ#,^堀u&~˹4aN sz fPt k@Qt9“i=h`;熍 @a$;90fe냐"4I<+ +0Db8jv0oR6J``ʁ(9zqH}$+g-[߄`\ȄA=,==亹!mb[VHb%;=LsCr +5l(԰71۞Rk_!w3R;!u~v8;`h\@NbD@K!ijIQf5}m(sKѥ Bb)ZbX83@=LLDO*wjC,8%wCMWg ͜/DCv0zzM4[vf`Rd;m_)Pb +,ҫz@DÆ%p$+TΪ,棔 :KUy[F(ء:t6) ]!{z܈4`/H`F4!`6S]|`ٴw1w W1gaDl Pa銵}qG98{I'Zq#a@j~AtL-V MEU֖` jQ w51;re 4|9z r? +(H`*ȧ;R(Ҫ% Vbn+u7?`KӉ'_:"C)/mAv!E#Ñ!f,o̍mDm"z>KL&' N@r R mPGH\|ўCAaRtR=f鸉9]>DYL=U@*~lTzTIuPzd9%?GAvn. 27B"H*R1H0QUCqF#ӒEBgopIA/ŴPM_6M˂8p%V,Z8uȔ3خǀ%|L%=NJtHzV/J@"o]H NuGoAe5bY `mKj#iYAtbJ>PxH;o)PthAkN5= }8APba#,!qCE _8I5Fv~ 5ɯ\LTfӝ.@ & JBm̳˼  +溉@G +>DhU1 .bfH,Uiq5GF8H̤+iŷb;C@Lr] :c#>fWD+a2$;DH?V c}>kfcEz<~0=/de%yiǛ@4@u&bW?j{L+u/#=)S?\q4pN%dC2/ #dۺ#NITnIva|b`ݤFm epR $% cx +xeas_q?$Zv\@X# :"fxXbp(vk bx-dB°=2ZQqr@,_I\gxkbayZ?gG@`~#Q;E1鑬%hXK\ϰQ>e-B*iff|s eN;=cȥK.[az} W /BwbļhJAd@fAsW!](-KZOkC4V=)x}P\b#z:ta֓,|\Qbã/A%5Źd".hD 3Um2dɹ1"rC Opr }!s `h`` \[ CfA^9ϕ +d̑IA/~!%,dM@j?## 6%XO$,i2LJ#L#u[ K8 HJI%ZPXu˘!hM~`9ݔ 5Mr\$XADO`Bo,L p"yE"!ŁEl"`Gf68J*^?&F%ǂ"#@ILS XrxS_j%= B` 0'fO(6*>oa)AY V3B(a`B<0@ 9k,PFz}wJsd.-t$E<~sE:{q"S53Bcvx$lC _XHT +.(mAbP{,Fm;z_ $; +d@ע-P&qe@!TD=H#,u,si>( +i ( aV&,X1`j#K&cBz%ɷ>stream +Y(4!ٺ,"gxX$DC HHp;TM +/B!\}f7P :~c1yMTb-tSlY5sPXާC` [Z}G`@[H(Jl}Bɑ } v`))# njK& 6cl$Pa1 G6&IN,r֬"gqfR +D, +@$Rtc N"ﰺ"U am`|b(,ԕ/eXT5C?V ķ)A!hdqI!?qKɺ -qf _H lΦeҞ +"` [[0DRf '7o̴H5`ZЇ`m8ё$h`]YZ^pB"c5iؠm3e:a`, +[=:|8d5ݺ<2#22pm +ć1>;0xR􅘾n yL6xxvRxh5=21 +AF+ k>)Yz[Ddm-"~pIwSeod mKQR)zQT +JX@@'|A@u ՃW&"yla '#$ـZ2%>UmFȟR [Y8Mm6K4pRAݳPpZF3Qh4bw#_(zcI0`]BĂ&l$8q9mH%d\FE6eVy6 +zM5d^Y7#fC~* Se aģ;J20&@Y`_}h -i ([:yr{-ִd\8q #]I?AڸОŬ!;̜D+r M]Iu"QOK jyr!BxH҂>!I` 4 l`s[ +UG XȬ0\*?W@ + *3~lAq$Jx {R>VLsR7^&+S3 +bmPj:(V/4s%RB7M!,LILۃ/LA|V$B}($ȖJyL@Ӂr x.O+s"A.}f.<$ +u%xDx,#{}2V%Bʛ@;F5ŹZ@r \ҭi,L-(M)H]R0"sd- qdW= lRIF-P8t{Kw{%S.Ⱦd=t@ .-nx8 2Ì,ƒ_7n>4!H +"C" 5A̵>TYT2* #iE1M % +;ɛ6:<.@Ǘ"я5A6S,B +3Ej9h;L adr`Ey6yl$I l:ˡXEDNL]E%WtbH=y +Ѷ^ ^}I=&;VtW1D(Ppؠv b/ʃ Ey k:א2%c+`tqU[Cy5 +F>ތ',Op!^T§LHiGp`%@J+5`ùJ}yD)Ϡ4tH4sNsA)+|{(CrA3TS! ӌb|Ț!4B>*8 i=F(D3 ,-oFɊF|XL0!w[ 󡔄D_Sd~2~'tpW™E#_2Z>WJLc`d,lT tAK~SIa:|IM #|b;,Ov ZJ:`МE& U}:O$vo#\Q҅[@q9(6xЀ f+HeB{2 sP*ySD?qs\bqˆ kjLRogϒ@m!k^1+0hv-aUjL"9贘C"p3OxF$@}3^)1{,ĉVb_z}E[Fg0i;U'?nsgĠsiAbZB K7<`/@-^WD"_.jȁ<0i@td\ ) &cV|ޢ2m)Gtg%IŠpP )0ʪ2'DLM +hN$&2\O'EL0)5A#ci` +|>!dg q]#p ԣ] 0 +2X4@Y;Ɠ׀5҄-111cQWFAtMQn Xjy׀$rwiX&ʨ +Df"C +^?OJR-DXBF׬6Ci$`+$W#2 ܒP_U;/A Hf|p B&6xP8dv/`0ᑒj;D6%ly7%gҠ*oTȓ^  "P@r65,R'8r@`}9! V1ń 6j&-.d & J(`fna ljilF H [2ȵAsž lE$M@ +7+y֑%𖀗A0DZM wALd +I^ZjEK6FDp3c|ȞBw)c %QIsol +>Q?K?Q? TDҲ[-6H GmbC2=bzȰΑ>@Mm Zyڢ/@"!lmsp§7aPA!8-FH!tMbd#gPlg8_@ck Q*xy&ɺ 5@WByIM'Vͅ%<&ۇ2!UsBU,6H}X8%A Q`'=8-Id41S9Yqcio " +c%^O0(6Y-h&4i,Fy&уtUt"gwˤ|`<:/?I\Hu;q55¾' yܯR_1+J1* ĂK+ĹK-gq89Р/_Xh+ZqIZGTڛ)_zʟɭ=WƽZ>.7n^us/w{^{ QD]\5g\0w>߬*o>=m>_,}6.jg&6?3LdMaey'F^S*~e~oO_gfF'+SWP3eΏ<2|/Z︗̷sC>{Da4r +^ĵ΢?88fb{Lƚ1&ɼ#!]|޼ KaU%O442/?%HO)sYXz1v.ޖ}Vazq9!G_O:]ar+|C!,Sf쿶A`Be)K#Яi(H! :V7qO`Yw0w};c{x1ߑڎ9 +\92J .Ly 2q~ ^ϽLk^4{ee`TpQ('#u~2m%b;6KV>ɫͥ4>yj|x +Wr4.d1Hƹ\\}T)Y0ZR\s7+h?ŒWn(-Ti8!4?{ ^Z|pVb @{:[؝V˟w#t0^Uv5FL<LW\g+V׬ϯϗǹ}sg6?UC~){ :eͶw YR +dsg{\kKʕFr*uwv},ni~MӌɃ\=^N!w]֣Ⱥ?\~^ON_mrVǫ\/>kr%hNu٦׭n֩J5Z=k] X9VF^ +z1f8]Swhӡq6GiR7O{kZ7Fӡu:NOԽi u-{\=*ަ`},T]-lYI:6St:ew(,ߋZwC>+z+/t]7[F^ K%Pnf+8 }< S(9X Ub[lD*dJb\o)+fXWR+]YW`_|T=ǠCS_nOةޓg\>RYUj4M+6Kӧq4NwEgR,7S5'캑3:m1w04a&@z?qNsS۹b7keb)bwL4;IN DRY'}<rjiHgryiGDn~@s7Qqd+;L/}"tAKS67-VZTJ 8`K{~и͹ojwZPAi +1CAӿ4?aR&n=EQ1 7BHEu=$Ɯr;sr\ڍKq>r~-?ۋWMvI` )fkPN4>MSJ~ձTWK|| m[׃&Nsv Bh2ɟ̅UǏ=w?0}Ք8N~ +EAooD4af DI1[,@n.v' R8Wyfܨd*alb*-Կ+;NϺۻ~M:+FT ZLC,4ؼ_}hhKϽ柣i +UXsfsF%EM};{6(ҸFR\tP$b];2hq~utYuL!.Lc!z<1o ^U +l;;N*Bpnvjdz@V+uEou ,Jm!?onp;d!һSK$}ж_],^e:M݌yux%cQc뤏֏Y-r&jiHp pX/_0 )Q^oih>|ۚKa0d??iF𯖙^FaC}4)r+i'mP$HЕڡVwSw\7*fKɎn~Z^VN!Y.\دJ?{pxt !ソiέ*2;qyN;\\(U+1W sZ-<3t7ݍCw8t7vu~}s`{4.x. {aY oI.jHi"d 1nD ojךP-{I]n~u7yaӀLӬv..ޡ'{I9;hZqhCuhջj 0PT*Z.鴺ui.or[AZ HYUFG!`KJ$Z +<|͏1bﰤiA)?Gj._l {"`&dc? +&qoz\{ T7٠-ΡoLK&Jm츪_LK,>X)n & ?Ɣf {ڻ{vް+.y.ۆvzιgi -)JSeCn 뙽 v}-䥗Vtue `@7=VAgClu}]sw8r !Hz?iOk񕊍\i6rwKӎ)S571_@(őu+un`\8> <<Ք șz8N> qGr?<#yC܁Ro&闾 ^qzXHa? 7BA 8=8yHq84Hr8e\SSԤ}ߍR +R(ͺ=gwS_.׻f=(x3C+6~ڻABNj9A'?$4E%^C&qmX ~#wbPigzp^tu\garV p&Xk%W\w 7!E@3jr0U 茄0nCJj?Z6t >?y>9_*dͳÌ27E~Uol~MYB]yhRw!rд|!>*ۿU~wۥ3y{*qfvV?lʹ٪v2#.J>^R8jc&ًm'3':Pֵt62Ubkt`r?ܣ~\^K/\' vk孲{Z>Kr\̘׳9`/9&I?Y7cx?^jz⁖Iݟ/K`I}d]k $CLdf^ +:M9Hf13AlYg-Lcpłdqkwl!+]?|= >ñ>ٞEW>rILӎ,H5vjRׇr2 ?v̳qζZ7R3"qX<ib0teִSu^<(M/n& z1r9۹ZIߩsᎻf*\4+lV4NfiU+6n6Tڪiur"oۮ4v|E|l'Ε2ܴZvNx|O-(g_zeWwQ煢m R^-_E]՛`Gk;2WvJRF,m=;39Ah~v??#'TV躮p:^Zr fhřYqPߒIyY*5~_>ELm^|Gv:m3ձ?ocyrZ A+?%M@ruWٟ):zk_0kW "iߏз#eف5{z .\7Bۊ2m[nzeE:r^R3w-kIvښC$;rXotxU~&MNKnx f\)eO?d!s񐁴t%B' nZ[WKM:&s^oB1֋DWrQBO^8 :Ƹ6bZO߹/[MZEp'HM&~۷駪47}5}~B,2^\F*tDÊKvZ4O6Cۦr%Ej+qҲ"|ˡKL^Y\n(C5@ysVfvnZ+p\u +"-ps}O@\ϊBC;aVlJ|+]G9]avEA kPLNprt/Kҫ5Y֫5C[Աȯ ]cD</8?a3E"J 6 E خvqrai$P:317qզx` ˶(y;6F1y᭑/OǾDVz(DܟeޘtrwyoE4)'Rvr} ,K8l:]c>F(-kFֹ#NV*C,RGFdpGO=H}u7@G_3J?r8&ˍe,3=m+im-s^=ތw+G3|۲ԫ +0&DzhYQ&: ~Xoy-Zᦇٶe%k=Qd+/;k7uZ1W ]Zzze' k-6t=q=|ПJONir:RG];?=c֥㢒1vBpEw\)n4bćzh֧I0 _e#>m럞'P+7֞oLo櫫|db3VMEp$/v23 lkYbˢY{nPZuKX46ؾ>a"яrf9{`#,}K{瑥kZ u\ ZB2T/7^x Kg}M&AeɗTJ'XNrc)=?|y_;mP}|}5ԝnnr +c\>7 N7+6oSuX.n^FJn0\߈Ζ+^J$.ɤ\~ jmig2}BތE~ t>` Ղ_]rݨ][~ X bqe+g&( +0IƼDZA}7B+ 4 X`*wn7ӜOKOF]wO}1ֵ+kMTݐ5k6A5U+Ž{.^tcJS_@fh0q7rˌO5֚5֔43Z:fs壳QٮTϪyQ\WJZFejM;FmJA +M+\gR1 ȞjfίeNވk$)+\#ϟTŎ$hkP5:wV,d]{Rֆv嬡Wzrlnk8TsոszVjՄ4oZH +Y[ͦ*]̕껭1Uc]q{ًjd FX*6g洭 M^cV"ȕ۷R*mۯYٹZJ;qTo H;Pگ'o;^KP/Sϯhky3q)7ZCzTy?2])J;gqjXh < .s=M%=|dnf`'.Uv6۠v/Ru'6B4kpfGM{ E-Z#.ȴw ׏򉻪9x:.:kTre +S3]Zn߱"-dvϪU݋zV? a&r;ի1}X zY]tTv+n*ӕvFyW^KE4_ |KZm)Y"%K9l.-Ab31~S}kҏkkKyUJ>WJhbG'K[3֌նOVJd%'\QQ5+3{x5 p+{Wk846*R+ J.SWv&Ցd-q\yն#6UǍOvrX,ŭyux^x]6mW`wr!|]3a\735.*v]K۶I+[{ywPR_r/Шk?ƕfruk'.OgȕsC=,n=k䚧biGymm}緥Uw]TfD {v)Wx=;]k @eB0Et)Mc=LXֶV.k>۟7B/ȳշ g"-=C]V7l :4qR.?9R:Ɖf\-dCMXpos/Zd=;kRnw .ۏjbhU}09  ,Ԏ֨Nw梶DU/ھD8/m_iAWo.iϲ6N]|٭,s,pݴ +2E3;熣]j<ԺwH%P@X0'uSK-o^VMw:,/ÖQ0w;4s>x؅b^dcfi$&kY ׅ3r/Og}\iǵscpiKўhӷ7j\B3 \hlz}e\S𬜿~F_ՙrl^xnQ)Zԍi',J]#,׹-WrdC@ż>X^;2.kxXn;_d)̜j'LI _l}ظ֖\ܭ-oc{:0{|0]UgDW337>aT|}q2 "K~d=/7KoLZ\ Fӂ,)BGmO"dv=Ե vO&7=߰=DW6fS4R[˶Emkmop<ߟPwߙ֖.N>N{C*q1LfC{MZ9^/A]92+r_yalx`oN|֮\*tg:.[rəR4 6VX/mQYK7yl@C0B\ݨh?L+%Di7pL̹7W +P5^pR^{G#ŅS,Ⱥ$}N;Qz8ÉWw($_V6p~j׭6jQR,VNŦnۅ*V=4>w\Uk}\6u}uk.krPtpgQ- {E/ .w[Id>`$Hmsn^].(eG౹&͟4E'Dα x*Xgx=jw@皉 'dڢvJA+úqm~l[JGe2kz1{xO-wԣKˮzII]K]tWִ~%O_S$2-ILj7C6@Z +ҷU̲pUS?L.i dE.R lv,~}4,[ɍ-n;sl퍕w<}W6>n;;^_]?䗥lݑlvm/oG?&k?\S;57|:*^FՅ1xO_6{.ɯ=ǯsu -i>|ҩ&FpC#zwfZ쬎l:|yXߛů|UoifgVve1}ï&CNy AJo"nwjx?{z|g<\:q֟sfVz'>-yaggѷ L͏f5g^S^<߽ ǧ9їhLC3NˇmީXOC\?zgI1i<SYipErI|ܜQ /,ώ|oi>d?~}QiYMe[.~.dW=>~̮mUG/빱擩}y0_mWWg_Go+1߾ +_>fywv;q%Ȟ[ϲ}VfVoeg#ٵhuacd|at|d't}ˈqƩ~̈p܃ǑwsG}Y<YYVYlkd^G^5^>ΗFFw#чskHhHu;\cw_#;]٨rޅ޸35>~ti]ketɣ/ѽGߍNUGFs۹h|{:~U?fx߱^|e_މ>E5{gqIݽ̙1ƻsΛw>ξ޾{0Ƌ;'_gwՙ#cSgw?\yoO=<`<xhrl7y:nvsXke{z?rݓ̍?VƟ'cW/7ޞ?r`]xQqmn|D㍻kw'O@}(;1s#ģacbyeĆ{sDi' /F&.ũ`}oK홓o&T/&oл7m{o&&ZΫ뉗[7GGKogĺ~RZYU%ՁZطm?;e?,oͬZowɥݲ㻓_E+g;Xx|o{ OP}y,{;Aiލ>~MVp­~~6x> +ʽ^a&|Y5|mOF[OȎ_lG 㯫ї߽|`e$דޣ%;/J9wquenin}no\s51=|p80~|GO->z|0h3w΋GGKoxj:dzzڧ'['KOj\tvku|};1}Z8=g?:wr˅wGo;^BtEժ-|^X]S13y{n|`ҝg`k|kffӛꗍw+w|G+ɫF}5`Ѫ7~^]cV?7VMM֞YыgK۵go?;W:Z׵Wk_3{xtǯ/&>_43inW/w?UJg7~ʾ?\ ڟyjymBS_ZN`YwA᡽Aƒ$dA HBH0g~sa"&7Mc̹biݝY2-ߧ{oOqyk>f.kUku۾l"w|r hkc}pzl ;5j>; w.s[{~>2'˰ǟgo<|WOo4rM?c-\מÞbo=l)ݺMfjL ~1H?ߓ &&& +{7j u=5ކhIoi5 +(x5"by3F Z@Y -fJ@Ls9=3sz^n&o";уѦcnL880\,Z |s4tcpr/"J,৫oQ8g3 Sa61ڤo7Gmj$ 9c/k ^*{>P봰Yտ6䫳no;d?:q?qM3hgD(7%6o$`9&7; + +O|Ihׂ( G0Aw6"6Z):_ [8e +|ioLx}߳յa +}0 w0rsS랻evj~v-`G_ۈR(,=L$4OvǤRߤ$tq% %dVz&=3'V_s߅3&t&e>*_jvʵ/ރs,Q32cF5?V,iW/97%έ\Q׽zsRjZ,aLLE>/w\ܮ,^U`]}Wi18$S{ mN)r qeGVsRm3u%'j.3^-}KoWE>ӹE"t=2\XMbb^|ʹ.0;9uf9y$rgaߐ_ l KJ(IgI5mݟ3bc, JףQ?S0߼{\fTJ}'@,wpS) +۩M9}h.̙ 6E ߅MD283>e&_ N&U5sujaH?gpUP,vScyR~qXwPgkhZ؞ +]'xh +Iڎ(WOz>q|_%s+dRo۝B!*,|܉w}M_#m^ᝮew + 8:O{ťl-WqlOPkdwٽh<7|gIߞl7{-~T4l[z4[™/<o_2{7zx |/orb#ޱT;'a(~_eL`yG,? i1?xܶRLkMwaJi!$ W}$U: qAoU~>+${@qhJ[ :ȗht~:+*c7Stc +G*Ã+YM}"kh܉>ctp7>+4qߘl-|$Ofp''n*F:[zd/wϲuEȊ/Pxv4թ0[_{4'1h0(|ԊmBR Gw*!d@SlwRʌF_7\wG`$ W;5S7^# +iTs ?eJ Af>O&YjVDzmG k:D7k'ZB{pK~,H3Ũ>":9l&Rw{OIyՒ>wnxQK&}\1IL_΢''gi,\ݭFʓF7[{;)$#Xg~,{@s:E) {X\|i45< f}W + '^EӇƤѵwi >WI.(~Doٮj,7,xjyPWw ;jyI -i%^:(ar3Wevs?'TTQ:Fm+)^VήM k.NN+uDB#7L[Rn%j/_윫ƕ^\i94f7_8&F'm:YRVGgUVmm 4ei󒛑YׇxUٔ- \6 “iՑ:6$et1f$k@@Fy-Ht|R@(&>@!)8CoO M`!LvJI=٧.}Emaa>dhr249!>a5,9!QuJQ>ipBx+qf4F'{ݯdkb Y2P9Fx`8~e/K٨^ׯ6wfoudys+/ӻ+ձH BLsB-ޡw\A\-@hx9-|\kſ⌳֓w*sd0*NN ]#Fh KM]pTbBVx=q.|G ++6E0}'Ô^VRYBpdD˨pY!KXm2TWy4c/! <*:yR;ZzkBAPL;ý +m1iE U&Z]*0vQE΢I%zs/.7fW48$$zl~Da7{u^-m=JM[T54"i{ry俞hhheLzgQp/? MaV5c=a@ڿ?ڔs$QԹYmOP3[b~/rpdn2Y)A P}d+P 0bcH27)?[lR?T9p*q]WSO) >okblSYK[)FZ\6WM#A1D./K^" }dLp&1U_6idZH\AYuaϫD`o-$3IW I:VGW.G~/h,9@q1>L&>d(agOʤg&u_2R-C?=d[wXBgxQ%>]7pZ?2?:Xb>Lcid?af3?w{]-q<֫G9>;PfK*}EpmL^b7-7WҎIJc\f3AWncow@ۮѭ1cM.<>izB:ut5EnIۭ[J~ tGږ(&ͤ.~Ȣ /ݍ'Yeq{x6ΤJMH/G(hFeeuU5FUQg.l3ǘ,bu^O3*{b=+ן2i([.41 +AV=eC&=\/89q[7g.kgJ<4sO +nC(0R8/.'mzxz.e4o_IdEF߅Њtw蠔oipC0uO>NO\"*[ڪޅԓa) CpٝQE?a5*Dx ]6I:M/JjҀJH8 H2e#uUڶT:BǿY|T."Kd Ay'sy"' @f'dB欐:01oZ JpLۅH#C,;'ܘjU1ؖrٔiY64a٫m𓭵J@[-@e*?]< {CܱMq2[0cc_Pz;ەݽ&gk$֠s_}|;FL6[4Ort~5ؠpfh`>纑&{/M7A()t'$PO2S8n_M +'P SOxLq@C}۫~mɹU=v/4ff|2POXh彞:mMKo/r?갓3YxT l}7&R'>aE1RԾ6lς5\08dV*'Xq>3oNO^ +-cє1MR=2D_J1y2Nݺ'yQ7\Ŀ$@+sU>MJL_VT,EX[R23Lk6Qho +n]<`qQOi[j ڪ֠T&L!X$Kilo^JXz8?n '(ڴ6Aͨ۩§Iu7ԷUJ^oB/3H\Z,kt ?whIu߹KWQP__ 5x<^FW#p@aȘ*s W7$MK NyCZ Qڭ)pfg1xDc[9 41|͞>% X|nJ.R#vFT/l +-U +d`(qKF. +/݇mַ +R'3` +|R[qDPw;It7[6)KcԟpXr/bf5>b"6x3;vJ(l(ɉ:]x|a0/P$5h:. K-7`N|;֋~#ލ`< G绁'x+F#j''&#4 [ ܴʘnݥYkKɠyAFn4]43lzAP1d(}brfջX`j{ +xx7oͻX}t>^smV>+MtoVTkGl3:Yo|M>:/Żt +?^_?`Uc؜|ͼR[ Op|S8C-8ݸ/L]}cQkɯ4u>}|svqs i4ͿѣVJF`',~b_? Q}ܫl_6AwZ.3i\*ƟQ O28-U#w[=?RCt=|#YO%/:7~e +X9ƛa׃+Vbk↵9z۽u +hһ +,Al:ξnX+a`+4{#U ͈G7z7:zzq=ؿv7}e=譇ic#a,vB*%+ ghG PAwh6lnV,7C#-f-v*FlXxCchjck*jʛ0HiFkCnPMrfl-6ȷͭ3uɯApLDXŇ,KywfBvKNEV֓&ޖ"aS'3h*Olڳ 8Ζ5&tܭŵ +ގtO]r m^ +nk[ RBա%YQk%>1k%"TK[l6P`( =sfrmG%Q,kxNv ή3'gM l $gӘ#ӾFb 1w0##8z0׹HL"Ή[ͪ~!^Q&Q0aʛ2Hճq̀]cp\ q6/?W5Uy9Iy6/zfԵY@Fμ.1\rR+2I#w@%tH=X^ F| 3G[֎蓱My8E)ɔ/t`wR 9N\L"X&DžY-4L=}2KMIcέs^3ItB}$TG*#]! #,) uhfO{.7=nϓ5<֚kIȣUGK\j r3<%V/i.g^^~jBhKMnTsIR{q˨ `ʧyf{:c\zۏ]֢+7O3UrtI0:V~J}ɍ,-B;kKnqVl-`]P ERABce[B'UHqo o l\dZ!݉3BWjNtZon ~sn-^w~PcySRԷ=(vX݁YE,Bn6plVnEnFӼusel9E󊳕G# "κ>rVtzY5wgA]+]8zi`##R}d̀BQ|@ VU8l _847u;^Q[}>>Gm%(D\5(̃B +f6>6Zˊ߭s0K56kh E3v>Y7Md;8v +qx8>|٫X7$c3ԐLF}MC9h_>$]-sO/mn'Bhc6ST( J f;v3̔5^ONܥڪ.Kk6252,hm8>)CkIи:&}Uh.~(P^[ [8ovJ\9S$X\xXďXLrGU{lfmv>^>uxv>^ ﹆v>^ k|޽|f=|޽|3Ky\K;w/lr|޽|ֲ{l%{g+5z}${8>>x6&Y;,mLymne ˽z +oIu/Rռra7Zm;pռ*/e>:VP@f] 6&BƊoIty"{n.#Ζc{΢yעMzKj0Ls&h*1qkSH\mi ƶ3[wD6Q#C+y|<ZzZqLۮc2϶n#!f=7<;} jݓz @~Q (zhE&f}DYh;)Kͦ-_d߶JI4 +6 |QKF|k>&kXB1ͼfR޸Q #Bٛ ڎg[&8~Ó BuO|f9|43Ş'+go1C\[&{ak~U7QКqJ Pwќx7-Df[cnvq^CsE[T3<]gN]Vs +T>5uU 1˥Be\xߵ^-~3.E (HC9l|w~μ9\Sgeo[wgLgc1,ԭ3obXyhy$(\GwgMuA[3O|,יVy<}ny} gƼt +vUzyc+iہ_޳TRtIU_Ay۫ +=A@>7_Yq*KYѱ,<.R%I;zJ (f]xT +4_2ضhc#B R!hL7FJGƟ@su}aGB;c4q7loTvm}[O,5/d-{hB, T7d[]?.ݺd${&ٻ49褳.EזV<:;n-6婳u5m,%6=)OuΏWls=ޔ~XSj=N<d0boGyﭥVՖ|%5mtBx6o7ʇ^9/2r}i7 +!=bdb ["宆9z\.z423 W@g@D.9y}}O&}Ob<3G}Il#V?5F[;ݑY~-]RKmĖyU޾<=z5J^~5W{I k{go +LXmmw_CC;\g+V {Us9׿̫[gߘsZX7f)/s +^XAW(7tuKnYڎ'ۿ)Z@u۟ R{-_̽Rx16>=gm^};ci~ZS}έ.Sa9&ܜ4\KQ.qro.z+qclލZziNWbΩ"ACvk_ܟ{ ;)iߓ^>*6}Sd*-9\cc}^=g3%c|zRzJf*!WoszHw6\ϩoN%2}N67rcSWl紆ܬo>'PK79ژ}#\gq!-F579um# :Fz }+lsjA3kj:559ܮobcFd5YW_htZɼ[ kx;(,{\fy.9A.rsOK̻Q~H5pێ_3sƤ+`OH1kX9cҕ ˷`S{l2N!v6[?h-;g#Ld9l-كi +ЋUmBrIH_mWu˪vC\Arϙ-݈>rکk" {U?,gu˗,zSCNݘwZܸP`kij6pzC9=.=q{]ouZuZ]-1i69JF'#+qOTO@%cN?6w-CP goxzCe>^>٘Yi] SBjڂ~NyoKom/]1ݜY-Bu=Y߇7_|#>G: uƀZ±dr>jlHvy]sqTyOyw߆~U۰w[U_?wW]=oU1պh^KӆWki0tsV6>OgosK<괻u*7wy~O~[ZZvk(NɯE+@ g=g +ջ}S7Ւg/Uc4+4K<|r=O#ͬB>_1YߞG͏u_`^o:D{j=]BTa~n9 +2< >85ݚEns=]ӎ6}Rުfćóypjuڍw0_10LIv}7ձseabT rOAl1bv=gt6x*yUCt8G墌 l]<ē.;Y v{#u k+U NXr_oz`-xw買n g9<*smª!Hύ[K En]@Oj썈/Ga{mam:a Tij[ɶ+pFjtlK2''%tqK? .>M]SEeTbx<Sq9{*Ǻ Fr?2Ϊ:;]=1G+S*ruܛS7t,ps.$Up+Cm[c3]5v.>fh {=h9#mVZzzmI׀9ld9FyۗinOIHe K^o#Eg+e7CI'{ٞ\mci+@:uƆeOţWrrtfaYDϦ[ڟȥe} + {sy9j= zI=j+@{>|A&Zhsv@DXohKK߈W[#zܖDFr- 桗mIGKkIۏ7+$GJ-~I[#Dҋ$zx[x-pf  +m =yl +ǿB+4tڿBBPab_~X{n:wBs {սW+ÙV<>pj;C ooZCcrx끇)zO;tJ,uwze}O;о +#=O;4.<\l5jCٚN;KW=p\ +Z xD12:5G[ù -᧭x࡟5x9v8G쁇Kp2:5G#z7z~N:PxS=xi֒{Y-qBe xh.ٞ=Іf +q? + +Ctx;><\%@xvf62K7tࡷuCWρzqW?PXEÁZKxݪpࡋ1WO;tix}ڡy#{YCWuZr<:$=qLz~XY|ڡk>mפH>mz7lU˴ v1 v.o(Ycy𭖒'htL_tޔhs1.VaY%lFݗE +ȁwe9 :&+F)4kN"9Qog+F9x:JS]*o.GcMz[&Y.\&-CXNσ}m+bQpkJǒ5E[1"/)z/;-=?xe {Eʥ0g0?t?젲m;Viy_^+A9"]`Dvd1S ПLF3{t;$M%\Tdpħac'~50kQb~!n*v 0uF~9\r&? C%¡χu`ŋN fJgT򢳥 ɢNQቐJ!3 WVcNWB xNG3`Df:ӷ k"AM#4@ϢNp&o:z}BH+&Arzi`7~ Ia\TC%}!weX!S7)vD GL=1oP-uOWM*r+~/ko>Fa6Si0اZ2橹0S9 VoljLgx'2sB(shZ)_Ga Yן r$O嘌;W86 f9 s6~D$mw$92e"5Z<}/4*(חW\ q=RZ >& h|MEլx/, M's!#7JCMEL9# b^K ^}O!m 809> c \[ A!jKJLA͇yNكkze]+5RC@YR4$I c&F!pYiUҬHr5JP=nNGe>{T\~GpzvP+SJ]kyJ/H'x*, :?_lm->/qoNƼL_3?z&$‘℆L* +.3 wY#8@qwxNVQ߫xO&cޘ׮6G|Ҭ߼6GA}v19٧nwRW)7PdQ240{ yäˋi|'qƈ\n.gN+%kG\(&tFg n?cQSo}O6Sl-|,T7єQHͱnwEc>r}},|Pi{j>8aj=jL[1T *~}PA ͝@.998qkи&VqOVï%m]{ i_+0Sxia7?-<:)_ V&998$ݻ,ej?]V@+iU> ўHwWMsmDC#iz(nii" +0jD ^(ޅ"DRړ.y5}|AUh\Ӆ^YUc׫Pc׫PKū@z¯WAc׫@d¯WAfc׫09:16{5Rހw[PBTM kp$\!lۮv~FxRrW_0bEě['$j!'H/F/[Q +Pd_ SW3rL>fixlY~ܤ)!l{v`6j*6hWq0z f51LxDQgV)ğdۣƤR\Qbr5IST甪I)=WN3OVh34fi Q%^ⓖMT +mu݅=?dIo{am0~dNlT㬔S ^F(o'v~J?%ߒo6VvH+|KHi0k3eŶ j ~ &_tϚ.&g v +4b[Re4ljtHXxl m4M˧2`VЎƋj[)!Yj7XkU[s )Hx>uJe5ʑb$a+Nkjۜ׈E.>X{eEWIBL tM53r`nHi4MJi)=ovF}ްR,>47e 5Q^@o%Xdʹ7c>jcnm.' ?:7rnWgzS(oҌ<|jkh,ͳNxǧn$z>ѿ6Sqa3=݈7>T.Ma946Amf?7;_ק̱,0Ȳn/,r2%$H"04g$^`^byydFノ s66^\P 'Vc?R?_pBhtM{յɨ 5u}tR`gV* ? {;}ftnӱu!ѫ<,4NJ[ef?ߝt?Wf{O`oÅ% e}\_zd=9U:ϯGIY,o%1=Fwk5icWvSj;֠[80=[i?yߪQ}>|'wo +){LCu -M0DZ(J@W~PS NdYQ"^ 3< Y%Ff01G*63?P3Rs F٧D w͠ѮA! W3oj#g\b +2 ?4'Ֆ5K>]B&t VၕhQ$sjN5j8c)`,g-( EH+Eԯ +NM_+f-d"-'hPJ( U.\"*8K$4+mB|y\ D~U3g71VM +lNDAgBg._gVQ(fXYh N?폐 UG>πg"/%\ x%򫊞|@Zwb,"KA(3q,$>3e70Px|ЅG?Vwm@`Yө:L + +JeRQ,%d Ey'(W`J9Ny8O[}~,!W|yol1n1N6o3㶸Iڿ+p!;ac86Fɹ϶ƃdi~vs Ӯ<:Kg>D%(`r`D#piUfUOߞw+Ox1_}dnMUgчj͈W^sՋ޽zW/:{E'O_o\sN^q7:y%#X_UZx_y[9-5/Tl T?3D\P.: +LĠcʼnx|[ $ (rD$#2O'dZiEc!TVĭ7g@F)w,WJ'4pBJ( "x$$R$S؄r4ϫNLfPD}BP2nt I'CĠEV&S̀fjG y' dY^&Ą +P,)Se 69MK8 )UGK3*;'dA/1HJ;/+Ã[f`04لh*qЄe'ap g@Fu^q~hȺ3gh:!%pS"Dx5nJ}QNHp2[fe.xRK U" IӰ9U.MR{Gr1qP䍦97! hVV0'E%L\s5`:)u5.5) + Ur2ip=%yMqULhR*bI>x~H7sBt@U =#x.yno9jYTLYZ0od&z_Mte?هDOW7A*E@ yUj*X-e@(tc̃w;~ۼz_mMlwaM]Z H9lGw˲vyWFm?};Jy3> +b"S,c,$ >jM@,XLcq#8"n`cp)hhqh;$n@>Zp" 6@t +fT3ȃ!Ead&0(M l|qieʣЁG)SY Jg0R@&L`LA RzSx gYG@DyhX4Ba 0q<"*pG`LF 7 +xL gqS3ʌ CJ.C4>8LbI,Q"0W wQ`YK dԀtC0f|l}qN! <#0 rF\ Xt$#Yd^Q%^)HwH0a %R,ѯσAc ǁW:Z`01}j`SqG_\Ey1td , XieIJd,jT@&fY!s1ĉ#1@ty BBDbqF +ZD&0a  Y` 1ƅГs$2 b0!p D]d6Dqb>ٝ (2)?a/MKitU`*m +DOF|;F:EL}b9XXRLŢ{pA2hŁyAíY`,9@& h$ +(ư w +$bDt`C]`Y<S ,YⵑfRt{n3pD &j`޹JrTQt?xcl z2FnZ;ڷEL ̌g <n6Ԟ^Dt1e;<zd&gPgP6lQ͜U-cnd +b}M@u*o4~&k+"HBqyem:C. g3:0P- ΏhضGh71 L99FFIbeN_a7XxC=08+3Հol +YI#lYH{qAq>[|3?>*ﶧR xF㛊B$*: +q&rKXg>Qpr&b;.ӏD04;t/iXYr 8ycD" 6d-y9q0lleNdiv( +=ƞ;(ӆR*/=S7myޤ8fo\=.2jS@''$؆ +襶gٽ>/mA"9lF^nGg+\j}NG9c9>y|ô`pDO27I3g24FYaG-~iDpI>0& +UF):H\WF$c j݅-BIKJ2<"8vX7m*~YzY2RvQ^ْ' +6+ FUd)l464[Vm\6#FK#VILʭ6unTQ֡YЫe&Ըp%@ JGIEG~GmUjNW4A`{k؏*wɚQ' <-dbvAM^DǕrvg~zzB$XKCIWe%ŶJ % \(\2y?@%+$'9rU*B O#`(Pur`W*A\3J%[jB+NK5HJ.8d,,ZC[%*Ѓ"Xu /{ʄ۱WzNXߗ7 +%Co[p؈4\V3SԘ 7w5Q``!6rm9{摆WP&.veW>nC@dkxaX"" \n}nU]-bBtD{xjPJC9ײl t1NQ(TUj9γأer,%C?ZSH'fTn &_)m+mb3kS@2"sY셴Yp32H[LjOx H VuS%)=b3I}2[AGҧ56Jof3১K5SXo+yK΂,ϺUQ<oR 'QM'%XC,no ٖh] 19#gm :D/etI9{;ƶfch"&j",q 5;!M!!jӭFetO>nRYm17qM@GJVU*6b$kIExuXymUSZg /%$җDH_K"}I/%$җDH_K"}I/%$җDH_K"$&75=Bdg@Rf4Z/C\K}I/I%$ї$D_I_J/.%`̗0_K| /%`/̏)+&L퇿/%?}WM>W][^Ym6AlKԊZcC*iK||U/{q3x'oRX/_l7^^~Zˋ7A?=z%_^G}qo^ w˷J{wJ>3ӳ?Wxݻ?/17;|wˇ?/?KWwտW +endstream endobj 8 0 obj <> endobj 5 0 obj <> endobj 6 0 obj <> endobj 9 0 obj <> endobj 7 0 obj <> endobj 44 0 obj <> endobj 41 0 obj <> endobj 42 0 obj <> endobj 45 0 obj <> endobj 43 0 obj <> endobj 71 0 obj [/View/Design] endobj 72 0 obj <>>> endobj 69 0 obj [/View/Design] endobj 70 0 obj <>>> endobj 67 0 obj [/View/Design] endobj 68 0 obj <>>> endobj 65 0 obj [/View/Design] endobj 66 0 obj <>>> endobj 63 0 obj [/View/Design] endobj 64 0 obj <>>> endobj 33 0 obj [/View/Design] endobj 34 0 obj <>>> endobj 31 0 obj [/View/Design] endobj 32 0 obj <>>> endobj 29 0 obj [/View/Design] endobj 30 0 obj <>>> endobj 27 0 obj [/View/Design] endobj 28 0 obj <>>> endobj 25 0 obj [/View/Design] endobj 26 0 obj <>>> endobj 84 0 obj [81 0 R 83 0 R 80 0 R 79 0 R 82 0 R] endobj 116 0 obj <> endobj xref +0 117 +0000000004 65535 f +0000000016 00000 n +0000000338 00000 n +0000023967 00000 n +0000000010 00000 f +0000167839 00000 n +0000167906 00000 n +0000168045 00000 n +0000167759 00000 n +0000167975 00000 n +0000000012 00000 f +0000024019 00000 n +0000000013 00000 f +0000000014 00000 f +0000000015 00000 f +0000000016 00000 f +0000000017 00000 f +0000000018 00000 f +0000000019 00000 f +0000000020 00000 f +0000000021 00000 f +0000000022 00000 f +0000000023 00000 f +0000000024 00000 f +0000000035 00000 f +0000169514 00000 n +0000169545 00000 n +0000169398 00000 n +0000169429 00000 n +0000169282 00000 n +0000169313 00000 n +0000169166 00000 n +0000169197 00000 n +0000169050 00000 n +0000169081 00000 n +0000000036 00000 f +0000000037 00000 f +0000000038 00000 f +0000000039 00000 f +0000000040 00000 f +0000000000 00000 f +0000168193 00000 n +0000168261 00000 n +0000168402 00000 n +0000168112 00000 n +0000168331 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000168934 00000 n +0000168965 00000 n +0000168818 00000 n +0000168849 00000 n +0000168702 00000 n +0000168733 00000 n +0000168586 00000 n +0000168617 00000 n +0000168470 00000 n +0000168501 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000034465 00000 n +0000034535 00000 n +0000034680 00000 n +0000034382 00000 n +0000034607 00000 n +0000169630 00000 n +0000024522 00000 n +0000035453 00000 n +0000031280 00000 n +0000031167 00000 n +0000035340 00000 n +0000034027 00000 n +0000029343 00000 n +0000030593 00000 n +0000027399 00000 n +0000028781 00000 n +0000028829 00000 n +0000033964 00000 n +0000031104 00000 n +0000031315 00000 n +0000034170 00000 n +0000034266 00000 n +0000035222 00000 n +0000035254 00000 n +0000035104 00000 n +0000035136 00000 n +0000034986 00000 n +0000035018 00000 n +0000034868 00000 n +0000034900 00000 n +0000034750 00000 n +0000034782 00000 n +0000035528 00000 n +0000035729 00000 n +0000037119 00000 n +0000054558 00000 n +0000120148 00000 n +0000169683 00000 n +trailer +<<395C71B32FDF194E9215422F8EB1B4FB>]>> +startxref +169896 +%%EOF diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index e9e5f0fca..5d941b083 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -56,7 +56,7 @@ namespace Ombi.Api.Emby return obj; } - public async Task LogIn(string username, string password, string apiKey, string baseUri) + public async Task LogIn(string username, string password, string apiKey, string baseUri, string clientIpAddress) { var request = new Request("emby/users/authenticatebyname", baseUri, HttpMethod.Post); var body = new @@ -71,6 +71,11 @@ namespace Ombi.Api.Emby $"MediaBrowser Client=\"Ombi\", Device=\"Ombi\", DeviceId=\"v3\", Version=\"v3\""); AddHeaders(request, apiKey); + if (!string.IsNullOrEmpty(clientIpAddress)) + { + request.AddHeader("X-Forwarded-For", clientIpAddress); + } + var obj = await Api.Request(request); return obj; } @@ -248,5 +253,49 @@ namespace Ombi.Api.Emby req.AddContentHeader("Content-Type", "application/json"); req.AddHeader("Device", "Ombi"); } + + public async Task> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) => + await GetPlayed("Movie", apiKey, userId, baseUri, startIndex, count, parentIdFilder, "ProviderIds"); + + public async Task> GetTvPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) => + await GetPlayed("Episode", apiKey, userId, baseUri, startIndex, count, parentIdFilder); + + private async Task> GetPlayed( + string type, + string apiKey, + string userId, + string baseUri, + int startIndex, + int count, + string parentIdFilder = default, + string fields = default) + { + var request = new Request($"emby/items", baseUri, HttpMethod.Get); + + request.AddQueryString("Recursive", true.ToString()); + request.AddQueryString("IncludeItemTypes", type); + if (!string.IsNullOrEmpty(fields)) + { + request.AddQueryString("Fields", fields); + } + request.AddQueryString("UserId", userId); + request.AddQueryString("isPlayed", true.ToString()); + + // paginate and display recently played items first + request.AddQueryString("sortBy", "DatePlayed"); + request.AddQueryString("SortOrder", "Descending"); + request.AddQueryString("startIndex", startIndex.ToString()); + request.AddQueryString("limit", count.ToString()); + + if (!string.IsNullOrEmpty(parentIdFilder)) + { + request.AddQueryString("ParentId", parentIdFilder); + } + + AddHeaders(request, apiKey); + + var obj = await Api.Request>(request); + return obj; + } } } diff --git a/src/Ombi.Api.Emby/IBaseEmbyApi.cs b/src/Ombi.Api.Emby/IBaseEmbyApi.cs index 248c0a88f..e2ebc513c 100644 --- a/src/Ombi.Api.Emby/IBaseEmbyApi.cs +++ b/src/Ombi.Api.Emby/IBaseEmbyApi.cs @@ -11,7 +11,7 @@ namespace Ombi.Api.Emby { Task GetSystemInformation(string apiKey, string baseUrl); Task> GetUsers(string baseUri, string apiKey); - Task LogIn(string username, string password, string apiKey, string baseUri); + Task LogIn(string username, string password, string apiKey, string baseUri, string clientIpAddress); Task> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); @@ -32,5 +32,8 @@ namespace Ombi.Api.Emby Task> RecentlyAddedMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); Task> RecentlyAddedEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); Task> RecentlyAddedShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); + + Task> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); + Task> GetTvPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); } } \ No newline at end of file diff --git a/src/Ombi.Api.Emby/Models/Media/Movie/EmbyMovie.cs b/src/Ombi.Api.Emby/Models/Media/Movie/EmbyMovie.cs index e127f75f6..3e8f5407e 100644 --- a/src/Ombi.Api.Emby/Models/Media/Movie/EmbyMovie.cs +++ b/src/Ombi.Api.Emby/Models/Media/Movie/EmbyMovie.cs @@ -5,30 +5,8 @@ namespace Ombi.Api.Emby.Models.Movie public class EmbyMovie { public string Name { get; set; } - public string ServerId { get; set; } public string Id { get; set; } - public string Container { get; set; } - public DateTime PremiereDate { get; set; } - public object[] ProductionLocations { get; set; } - public string OfficialRating { get; set; } - public float CommunityRating { get; set; } - public long RunTimeTicks { get; set; } - public string PlayAccess { get; set; } - public int ProductionYear { get; set; } - public bool IsPlaceHolder { get; set; } - public bool IsHD { get; set; } - public bool IsFolder { get; set; } public string Type { get; set; } - public int LocalTrailerCount { get; set; } - public EmbyUserdata UserData { get; set; } - public string VideoType { get; set; } - public EmbyImagetags ImageTags { get; set; } - public string[] BackdropImageTags { get; set; } - public string LocationType { get; set; } - public string MediaType { get; set; } - public bool HasSubtitles { get; set; } - public int CriticRating { get; set; } - public string Overview { get; set; } public EmbyProviderids ProviderIds { get; set; } public EmbyMediastream[] MediaStreams { get; set; } } diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs b/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs index a83e1f087..7368c194e 100644 --- a/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs +++ b/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs @@ -5,30 +5,8 @@ namespace Ombi.Api.Jellyfin.Models.Movie public class JellyfinMovie { public string Name { get; set; } - public string ServerId { get; set; } public string Id { get; set; } - public string Container { get; set; } - public DateTime PremiereDate { get; set; } - public object[] ProductionLocations { get; set; } - public string OfficialRating { get; set; } - public float CommunityRating { get; set; } - public long RunTimeTicks { get; set; } - public string PlayAccess { get; set; } - public int ProductionYear { get; set; } - public bool IsPlaceHolder { get; set; } - public bool IsHD { get; set; } - public bool IsFolder { get; set; } public string Type { get; set; } - public int LocalTrailerCount { get; set; } - public JellyfinUserdata UserData { get; set; } - public string VideoType { get; set; } - public JellyfinImagetags ImageTags { get; set; } - public string[] BackdropImageTags { get; set; } - public string LocationType { get; set; } - public string MediaType { get; set; } - public bool HasSubtitles { get; set; } - public int CriticRating { get; set; } - public string Overview { get; set; } public JellyfinProviderids ProviderIds { get; set; } public JellyfinMediastream[] MediaStreams { get; set; } } diff --git a/src/Ombi.Api.Radarr/IRadarrV3Api.cs b/src/Ombi.Api.Radarr/IRadarrV3Api.cs index 072e8ef51..0b91b4925 100644 --- a/src/Ombi.Api.Radarr/IRadarrV3Api.cs +++ b/src/Ombi.Api.Radarr/IRadarrV3Api.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Ombi.Api.Radarr.Models; using Ombi.Api.Radarr.Models.V3; @@ -14,7 +15,8 @@ namespace Ombi.Api.Radarr Task GetMovie(int id, string apiKey, string baseUrl); Task UpdateMovie(MovieResponse movie, string apiKey, string baseUrl); Task MovieSearch(int[] movieIds, string apiKey, string baseUrl); - Task AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability); + Task AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability, List tags); Task> GetTags(string apiKey, string baseUrl); + Task CreateTag(string apiKey, string baseUrl, string tagName); } } \ No newline at end of file diff --git a/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs b/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs index 09e985f43..9efea8ee1 100644 --- a/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs +++ b/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs @@ -29,5 +29,6 @@ namespace Ombi.Api.Radarr.Models public int year { get; set; } public string minimumAvailability { get; set; } public long sizeOnDisk { get; set; } + public int[] tags { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Api.Radarr/RadarrV3Api.cs b/src/Ombi.Api.Radarr/RadarrV3Api.cs index 9e7e0f4c2..76656be65 100644 --- a/src/Ombi.Api.Radarr/RadarrV3Api.cs +++ b/src/Ombi.Api.Radarr/RadarrV3Api.cs @@ -72,7 +72,7 @@ namespace Ombi.Api.Radarr return await Api.Request(request); } - public async Task AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability) + public async Task AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability, List tags) { var request = new Request("/api/v3/movie", baseUrl, HttpMethod.Post); @@ -86,7 +86,8 @@ namespace Ombi.Api.Radarr monitored = true, year = year, minimumAvailability = minimumAvailability, - sizeOnDisk = 0 + sizeOnDisk = 0, + tags = tags.Any() ? tags.ToArray() : Enumerable.Empty().ToArray() }; if (searchNow) @@ -156,5 +157,14 @@ namespace Ombi.Api.Radarr { request.AddHeader("X-Api-Key", key); } + + public Task CreateTag(string apiKey, string baseUrl, string tagName) + { + var request = new Request($"/api/v3/tag", baseUrl, HttpMethod.Post); + request.AddHeader("X-Api-Key", apiKey); + request.AddJsonBody(new { Label = tagName }); + + return Api.Request(request); + } } } diff --git a/src/Ombi.Api.Sonarr/Models/SystemStatus.cs b/src/Ombi.Api.Sonarr/Models/SystemStatus.cs index 778570a4c..a78429328 100644 --- a/src/Ombi.Api.Sonarr/Models/SystemStatus.cs +++ b/src/Ombi.Api.Sonarr/Models/SystemStatus.cs @@ -3,23 +3,6 @@ namespace Ombi.Api.Sonarr public class SystemStatus { public string version { get; set; } - public string buildTime { get; set; } - public bool isDebug { get; set; } - public bool isProduction { get; set; } - public bool isAdmin { get; set; } - public bool isUserInteractive { get; set; } - public string startupPath { get; set; } - public string appData { get; set; } - public string osVersion { get; set; } - public bool isMonoRuntime { get; set; } - public bool isMono { get; set; } - public bool isLinux { get; set; } - public bool isOsx { get; set; } - public bool isWindows { get; set; } - public string branch { get; set; } - public string authentication { get; set; } - public string sqliteVersion { get; set; } public string urlBase { get; set; } - public string runtimeVersion { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs b/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs index 8418cddbc..c55d76e8f 100644 --- a/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs +++ b/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs @@ -52,6 +52,7 @@ namespace Ombi.Core.Tests.Engine _subject = _mocker.CreateInstance(); var list = DbHelper.GetQueryableMockDbSet(new RequestSubscription()); _mocker.Setup, IQueryable>(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock()); + _mocker.Setup>(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock()); } [Test] diff --git a/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs b/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs index 16b7cb811..c483d81ba 100644 --- a/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs +++ b/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs @@ -46,8 +46,9 @@ namespace Ombi.Core.Tests.Engine.V2 var requestSubs = new Mock>(); var mediaCache = new Mock(); var featureService = new Mock(); + var userPlayedMovieRepository = new Mock(); _engine = new MovieRequestEngine(movieApi.Object, requestService.Object, user.Object, notificationHelper.Object, rules.Object, movieSender.Object, - logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object, mediaCache.Object, featureService.Object); + logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object, mediaCache.Object, featureService.Object, userPlayedMovieRepository.Object); } [Test] diff --git a/src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs b/src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs index 018ee99e9..dc748e0d9 100644 --- a/src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs +++ b/src/Ombi.Core.Tests/Services/RecentlyRequestedServiceTests.cs @@ -173,6 +173,7 @@ namespace Ombi.Core.Tests.Services [Test] + [Ignore("Flaky")] public async Task GetRecentlyRequested_HideUsernames() { _mocker.Setup, Task>(x => x.GetSettingsAsync()) @@ -182,20 +183,25 @@ namespace Ombi.Core.Tests.Services var releaseDate = new DateTime(2019, 01, 01); var requestDate = DateTime.Now; - var movies = _fixture.CreateMany(10); + var movies = _fixture.CreateMany(10).ToList(); var albums = _fixture.CreateMany(10); var chilRequests = _fixture.CreateMany(10); + movies.Add(_fixture.Build().With(x => x.RequestedUserId, "a").With(x => x.Title, "unit").Create()); _mocker.Setup>(x => x.GetAll()).Returns(movies.AsQueryable().BuildMock()); _mocker.Setup>(x => x.GetAll()).Returns(albums.AsQueryable().BuildMock()); _mocker.Setup>(x => x.GetChild()).Returns(chilRequests.AsQueryable().BuildMock()); - _mocker.Setup>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Alias = "alias", UserType = UserType.LocalUser }); + _mocker.Setup>(x => x.GetUser()).ReturnsAsync(new OmbiUser { UserName = "test", Id = "a", Alias = "alias", UserType = UserType.LocalUser }); _mocker.Setup(x => x.Username).Returns("test"); _mocker.Setup>(x => x.IsInRoleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); var result = await _subject.GetRecentlyRequested(CancellationToken.None); - CollectionAssert.IsEmpty(result.Where(x => !string.IsNullOrEmpty(x.Username) && !string.IsNullOrEmpty(x.UserId))); + Assert.Multiple(() => + { + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.First().Title, Is.EqualTo("unit")); + }); } } } diff --git a/src/Ombi.Core/Authentication/OmbiUserManager.cs b/src/Ombi.Core/Authentication/OmbiUserManager.cs index 124ff1aeb..e80469a00 100644 --- a/src/Ombi.Core/Authentication/OmbiUserManager.cs +++ b/src/Ombi.Core/Authentication/OmbiUserManager.cs @@ -69,6 +69,8 @@ namespace Ombi.Core.Authentication private readonly ISettingsService _embySettings; private readonly ISettingsService _jellyfinSettings; private readonly ISettingsService _authSettings; + private string _clientIpAddress; + public string ClientIpAddress { get => _clientIpAddress; set => _clientIpAddress = value; } public override async Task CheckPasswordAsync(OmbiUser user, string password) { @@ -88,7 +90,7 @@ namespace Ombi.Core.Authentication } if (user.UserType == UserType.EmbyUser || user.UserType == UserType.EmbyConnectUser) { - return await CheckEmbyPasswordAsync(user, password); + return await CheckEmbyPasswordAsync(user, password, ClientIpAddress); } if (user.UserType == UserType.JellyfinUser) { @@ -168,7 +170,7 @@ namespace Ombi.Core.Authentication /// /// /// - private async Task CheckEmbyPasswordAsync(OmbiUser user, string password) + private async Task CheckEmbyPasswordAsync(OmbiUser user, string password, string clientIpAddress="") { var embySettings = await _embySettings.GetSettingsAsync(); var client = _embyApi.CreateClient(embySettings); @@ -196,7 +198,7 @@ namespace Ombi.Core.Authentication { try { - var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri); + var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri, clientIpAddress); if (result != null) { return true; diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 109460ff9..4d3cd2cf9 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -33,7 +33,8 @@ namespace Ombi.Core.Engine INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger log, OmbiUserManager manager, IRepository rl, ICacheService cache, ISettingsService ombiSettings, IRepository sub, IMediaCacheService mediaCacheService, - IFeatureService featureService) + IFeatureService featureService, + IUserPlayedMovieRepository userPlayedMovieRepository) : base(user, requestService, r, manager, cache, ombiSettings, sub) { MovieApi = movieApi; @@ -43,6 +44,7 @@ namespace Ombi.Core.Engine _requestLog = rl; _mediaCacheService = mediaCacheService; _featureService = featureService; + _userPlayedMovieRepository = userPlayedMovieRepository; } private IMovieDbApi MovieApi { get; } @@ -52,6 +54,7 @@ namespace Ombi.Core.Engine private readonly IRepository _requestLog; private readonly IMediaCacheService _mediaCacheService; private readonly IFeatureService _featureService; + protected readonly IUserPlayedMovieRepository _userPlayedMovieRepository; /// /// Requests the movie. @@ -77,7 +80,8 @@ namespace Ombi.Core.Engine var userDetails = await GetUser(); var canRequestOnBehalf = model.RequestOnBehalf.HasValue(); - var isAdmin = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) + var isAdmin = Username.Equals("API", StringComparison.CurrentCultureIgnoreCase) + || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); if (canRequestOnBehalf && !isAdmin) { @@ -252,7 +256,7 @@ namespace Ombi.Core.Engine var requests = await (OrderMovies(allRequests, orderFilter.OrderType)).Skip(position).Take(count) .ToListAsync(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -296,7 +300,7 @@ namespace Ombi.Core.Engine var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -381,7 +385,7 @@ namespace Ombi.Core.Engine // TODO fix this so we execute this on the server requests = requests.Skip(position).Take(count).ToList(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -424,7 +428,7 @@ namespace Ombi.Core.Engine var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -506,18 +510,25 @@ namespace Ombi.Core.Engine allRequests = await MovieRepository.GetWithUser().ToListAsync(); } - await CheckForSubscription(shouldHide.UserId, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } public async Task GetRequest(int requestId) { + var shouldHide = await HideFromOtherUsers(); + // TODO: this query should return the request only if the user is allowed to see it (see shouldHide implementations) var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync(); - await CheckForSubscription((await GetUser()).Id, new List { request }); + await FillAdditionalFields(shouldHide, new List { request }); return request; } + private async Task FillAdditionalFields(HideResult shouldHide, List requests) + { + await CheckForSubscription(shouldHide.UserId, requests); + await CheckForPlayed(shouldHide, requests); + } private async Task CheckForSubscription(string UserId, List movieRequests) { @@ -543,6 +554,23 @@ namespace Ombi.Core.Engine } } } + + private async Task CheckForPlayed(HideResult shouldHide, List movieRequests) + { + var theMovieDbIds = movieRequests.Select(x => x.TheMovieDbId); + var plays = await _userPlayedMovieRepository.GetAll().Where(x => + theMovieDbIds.Contains(x.TheMovieDbId)) + .ToListAsync(); + foreach (var request in movieRequests) + { + request.WatchedByRequestedUser = plays.Exists(x => x.TheMovieDbId == request.TheMovieDbId && x.UserId == request.RequestedUserId); + + if (!shouldHide.Hide) + { + request.PlayedByUsersCount = plays.Count(x => x.TheMovieDbId == request.TheMovieDbId); + } + } + } /// /// Searches the movie request. @@ -563,7 +591,7 @@ namespace Ombi.Core.Engine } var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList(); - await CheckForSubscription(shouldHide.UserId, results); + await FillAdditionalFields(shouldHide, results); return results; } diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 8ccc6d17e..16866d0dd 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -35,7 +35,8 @@ namespace Ombi.Core.Engine public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, ICurrentUser user, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger logger, ITvSender sender, IRepository rl, ISettingsService settings, ICacheService cache, - IRepository sub, IMediaCacheService mediaCacheService) : base(user, requestService, rule, manager, cache, settings, sub) + IRepository sub, IMediaCacheService mediaCacheService, + IUserPlayedEpisodeRepository userPlayedEpisodeRepository) : base(user, requestService, rule, manager, cache, settings, sub) { TvApi = tvApi; MovieDbApi = movApi; @@ -44,6 +45,7 @@ namespace Ombi.Core.Engine TvSender = sender; _requestLog = rl; _mediaCacheService = mediaCacheService; + _userPlayedEpisodeRepository = userPlayedEpisodeRepository; } private INotificationHelper NotificationHelper { get; } @@ -54,6 +56,7 @@ namespace Ombi.Core.Engine private readonly ILogger _logger; private readonly IRepository _requestLog; private readonly IMediaCacheService _mediaCacheService; + private readonly IUserPlayedEpisodeRepository _userPlayedEpisodeRepository; public async Task RequestTvShow(TvRequestViewModel tv) { @@ -161,7 +164,7 @@ namespace Ombi.Core.Engine var user = await GetUser(); var canRequestOnBehalf = tv.RequestOnBehalf.HasValue(); - var isAdmin = await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, OmbiRoles.Admin); + var isAdmin = Username.Equals("API", StringComparison.CurrentCultureIgnoreCase) || await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, OmbiRoles.Admin); if (tv.RequestOnBehalf.HasValue() && !isAdmin) { return new RequestEngineResult @@ -292,7 +295,7 @@ namespace Ombi.Core.Engine .Skip(position).Take(count).ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return new RequestsViewModel { @@ -328,7 +331,7 @@ namespace Ombi.Core.Engine return new RequestsViewModel(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return new RequestsViewModel { @@ -351,7 +354,7 @@ namespace Ombi.Core.Engine allRequests = await TvRepository.Get().ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } @@ -396,7 +399,7 @@ namespace Ombi.Core.Engine ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); // Make sure we do not show duplicate child requests allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); @@ -469,7 +472,7 @@ namespace Ombi.Core.Engine ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); // Make sure we do not show duplicate child requests allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); @@ -523,7 +526,7 @@ namespace Ombi.Core.Engine allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); // Make sure we do not show duplicate child requests allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); @@ -551,7 +554,7 @@ namespace Ombi.Core.Engine allRequests = await TvRepository.GetLite().ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } @@ -570,7 +573,7 @@ namespace Ombi.Core.Engine request = await TvRepository.Get().Where(x => x.Id == requestId).FirstOrDefaultAsync(); } - await CheckForSubscription(shouldHide, new List{request}); + await FillAdditionalFields(shouldHide, new List{request}); return request; } @@ -624,7 +627,7 @@ namespace Ombi.Core.Engine allRequests = await TvRepository.GetChild().Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } @@ -643,7 +646,7 @@ namespace Ombi.Core.Engine } var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); - await CheckForSubscription(shouldHide, results); + await FillAdditionalFields(shouldHide, results); return results; } @@ -864,14 +867,20 @@ namespace Ombi.Core.Engine } } - private async Task CheckForSubscription(HideResult shouldHide, List x) + private async Task FillAdditionalFields(HideResult shouldHide, List x) { foreach (var tvRequest in x) { - await CheckForSubscription(shouldHide, tvRequest.ChildRequests); + await FillAdditionalFields(shouldHide, tvRequest.ChildRequests); } } + private async Task FillAdditionalFields(HideResult shouldHide, List childRequests) + { + await CheckForSubscription(shouldHide, childRequests); + CheckForPlayed(shouldHide, childRequests); + } + private async Task CheckForSubscription(HideResult shouldHide, List childRequests) { var sub = _subscriptionRepository.GetAll(); @@ -896,6 +905,52 @@ namespace Ombi.Core.Engine } } + private class EpisodeKey + { + public int SeasonNumber; + public int EpisodeNumber; + } + + private void CheckForPlayed(HideResult shouldHide, List childRequests) + { + var theMovieDbIds = childRequests.Select(x => x.Id); + foreach (var request in childRequests) + { + var requestedEpisodes = GetEpisodesKeys(request); + + var playedEpisodes = _userPlayedEpisodeRepository + .GetAll() + .Where(x => x.TheMovieDbId == request.Id && x.UserId == request.RequestedUserId) + .AsEnumerable() + .Join(requestedEpisodes, + played => new { played.SeasonNumber, played.EpisodeNumber }, + requested => new { requested.SeasonNumber, requested.EpisodeNumber }, + (played, requested) => new { played }); + + var playedCount = playedEpisodes.Count(); + var toWatchCount = requestedEpisodes.Count(); + request.RequestedUserPlayedProgress = 100 * playedCount / toWatchCount; + + } + } + + private List GetEpisodesKeys(ChildRequests request) + { + List result = new List(); + foreach(var season in request.SeasonRequests) + { + foreach(var episode in season.Episodes) + { + result.Add(new EpisodeKey + { + SeasonNumber = season.SeasonNumber, + EpisodeNumber = episode.EpisodeNumber + }); + } + } + return result; + } + private async Task AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf, int rootFolder, int qualityProfile) { // Add the child diff --git a/src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs b/src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs index 4296c2cc1..daab65e71 100644 --- a/src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs +++ b/src/Ombi.Core/Models/Requests/RecentlyRequestedModel.cs @@ -16,6 +16,7 @@ namespace Ombi.Core.Models.Requests public string Overview { get; set; } public DateTime ReleaseDate { get; set; } public bool Approved { get; set; } + public bool Denied { get; set; } public string MediaId { get; set; } public string PosterPath { get; set; } diff --git a/src/Ombi.Core/Models/TesterResultModel.cs b/src/Ombi.Core/Models/TesterResultModel.cs index 563fa5cb2..945e0e261 100644 --- a/src/Ombi.Core/Models/TesterResultModel.cs +++ b/src/Ombi.Core/Models/TesterResultModel.cs @@ -5,5 +5,6 @@ public bool IsValid { get; set; } public string Version { get; set; } public string ExpectedSubDir { get; set; } + public string AdditionalInformation { get; set; } } } diff --git a/src/Ombi.Core/Senders/MovieSender.cs b/src/Ombi.Core/Senders/MovieSender.cs index 36d40bdad..f6907e2f5 100644 --- a/src/Ombi.Core/Senders/MovieSender.cs +++ b/src/Ombi.Core/Senders/MovieSender.cs @@ -15,6 +15,8 @@ using Ombi.Store.Entities; using Ombi.Store.Repository; using System.Collections.Generic; using Ombi.Api.Radarr.Models; +using Microsoft.Extensions.Options; +using Ombi.Api.Sonarr; namespace Ombi.Core.Senders { @@ -67,7 +69,7 @@ namespace Ombi.Core.Senders } if (radarrSettings.Enabled) { - return await SendToRadarr(model, is4K, radarrSettings); + return await SendToRadarr(model, radarrSettings); } var dogSettings = await _dogNzbSettings.GetSettingsAsync(); @@ -131,7 +133,7 @@ namespace Ombi.Core.Senders return await _dogNzbApi.AddMovie(settings.ApiKey, id); } - private async Task SendToRadarr(MovieRequests model, bool is4K, RadarrSettings settings) + private async Task SendToRadarr(MovieRequests model, RadarrSettings settings) { var qualityToUse = int.Parse(settings.DefaultQualityProfile); @@ -154,6 +156,17 @@ namespace Ombi.Core.Senders } } + var tags = new List(); + if (settings.Tag.HasValue) + { + tags.Add(settings.Tag.Value); + } + if (settings.SendUserTags) + { + var userTag = await GetOrCreateTag(model, settings); + tags.Add(userTag.id); + } + // Overrides on the request take priority if (model.QualityOverride > 0) { @@ -174,7 +187,7 @@ namespace Ombi.Core.Senders { var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, - settings.MinimumAvailability); + settings.MinimumAvailability, tags); if (!string.IsNullOrEmpty(result.Error?.message)) { @@ -212,5 +225,17 @@ namespace Ombi.Core.Senders var selectedPath = paths.FirstOrDefault(x => x.id == overrideId); return selectedPath?.path ?? string.Empty; } + + private async Task GetOrCreateTag(MovieRequests model, RadarrSettings s) + { + var tagName = model.RequestedUser.UserName; + // Does tag exist? + + var allTags = await _radarrV3Api.GetTags(s.ApiKey, s.FullUri); + var existingTag = allTags.FirstOrDefault(x => x.label.Equals(tagName, StringComparison.InvariantCultureIgnoreCase)); + existingTag ??= await _radarrV3Api.CreateTag(s.ApiKey, s.FullUri, tagName); + + return existingTag; + } } } \ No newline at end of file diff --git a/src/Ombi.Core/Senders/MusicSender.cs b/src/Ombi.Core/Senders/MusicSender.cs index 260b008fe..57bf4ab68 100644 --- a/src/Ombi.Core/Senders/MusicSender.cs +++ b/src/Ombi.Core/Senders/MusicSender.cs @@ -100,7 +100,7 @@ namespace Ombi.Core.Senders addOptions = new Addoptions { monitored = true, - monitor = MonitorTypes.None, + monitor = MonitorTypes.Existing, searchForMissingAlbums = false, AlbumsToMonitor = new[] {model.ForeignAlbumId} }, @@ -199,4 +199,4 @@ namespace Ombi.Core.Senders return new SenderResult { Message = "Could not set album to monitored", Sent = false, Success = false }; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Services/RecentlyRequestedService.cs b/src/Ombi.Core/Services/RecentlyRequestedService.cs index 1c67472d4..26d873c4d 100644 --- a/src/Ombi.Core/Services/RecentlyRequestedService.cs +++ b/src/Ombi.Core/Services/RecentlyRequestedService.cs @@ -73,6 +73,10 @@ namespace Ombi.Core.Services var lang = await DefaultLanguageCode(); foreach (var item in await recentMovieRequests.ToListAsync(cancellationToken)) { + if (hideUsers.Hide && item.RequestedUserId != hideUsers.UserId) + { + continue; + } var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}movie{item.TheMovieDbId}", () => _movieDbApi.GetMovieImages(item.TheMovieDbId.ToString(), cancellationToken), DateTimeOffset.Now.AddDays(1)); model.Add(new RecentlyRequestedModel { @@ -84,8 +88,9 @@ namespace Ombi.Core.Services Title = item.Title, Type = RequestType.Movie, Approved = item.Approved, - UserId = hideUsers.Hide ? string.Empty : item.RequestedUserId, - Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias, + Denied = item.Denied ?? false, + UserId = item.RequestedUserId, + Username = item.RequestedUser.UserAlias, MediaId = item.TheMovieDbId.ToString(), PosterPath = images?.posters?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(), Background = images?.backdrops?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(), @@ -94,24 +99,33 @@ namespace Ombi.Core.Services foreach (var item in await recentMusicRequests.ToListAsync(cancellationToken)) { + if (hideUsers.Hide && item.RequestedUserId != hideUsers.UserId) + { + continue; + } model.Add(new RecentlyRequestedModel { RequestId = item.Id, Available = item.Available, Overview = item.ArtistName, Approved = item.Approved, + Denied = item.Denied ?? false, ReleaseDate = item.ReleaseDate, RequestDate = item.RequestedDate, Title = item.Title, Type = RequestType.Album, - UserId = hideUsers.Hide ? string.Empty : item.RequestedUserId, - Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias, + UserId = item.RequestedUserId, + Username = item.RequestedUser.UserAlias, MediaId = item.ForeignAlbumId, }); } foreach (var item in await recentTvRequests.ToListAsync(cancellationToken)) { + if (hideUsers.Hide && item.RequestedUserId != hideUsers.UserId) + { + continue; + } var providerId = item.ParentRequest.ExternalProviderId.ToString(); var images = await _cache.GetOrAddAsync($"{CacheKeys.TmdbImages}tv{providerId}", () => _movieDbApi.GetTvImages(providerId.ToString(), cancellationToken), DateTimeOffset.Now.AddDays(1)); @@ -123,12 +137,13 @@ namespace Ombi.Core.Services Overview = item.ParentRequest.Overview, ReleaseDate = item.ParentRequest.ReleaseDate, Approved = item.Approved, + Denied = item.Denied ?? false, RequestDate = item.RequestedDate, TvPartiallyAvailable = partialAvailability, Title = item.ParentRequest.Title, Type = RequestType.TvShow, - UserId = hideUsers.Hide ? string.Empty : item.RequestedUserId, - Username = hideUsers.Hide ? string.Empty : item.RequestedUser.UserAlias, + UserId = item.RequestedUserId, + Username = item.RequestedUser.UserAlias, MediaId = providerId.ToString(), PosterPath = images?.posters?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(), Background = images?.backdrops?.Where(x => lang.Equals(x?.iso_639_1, StringComparison.InvariantCultureIgnoreCase))?.OrderByDescending(x => x.vote_count)?.Select(x => x.file_path)?.FirstOrDefault(), diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 09d99d4b7..8a5509963 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -197,6 +197,8 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -244,6 +246,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.HealthChecks/Checks/OmbiPingHealthCheck.cs b/src/Ombi.HealthChecks/Checks/OmbiPingHealthCheck.cs deleted file mode 100644 index 726d02078..000000000 --- a/src/Ombi.HealthChecks/Checks/OmbiPingHealthCheck.cs +++ /dev/null @@ -1,47 +0,0 @@ -using HealthChecks.Network; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using System; -using System.Collections.Generic; -using System.Net.NetworkInformation; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Ombi.HealthChecks.Checks -{ - public class OmbiPingHealthCheck - : IHealthCheck - { - private readonly OmbiPingHealthCheckOptions _options; - public OmbiPingHealthCheck(OmbiPingHealthCheckOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var configuredHosts = _options.ConfiguredHosts.Values; - - try - { - foreach (var (host, timeout, status) in configuredHosts) - { - using (var ping = new Ping()) - { - var pingReply = await ping.SendPingAsync(host, timeout); - - if (pingReply.Status != IPStatus.Success) - { - return new HealthCheckResult(status, description: $"Ping check for host {host} is failed with status reply:{pingReply.Status}"); - } - } - } - - return HealthCheckResult.Healthy(); - } - catch (Exception ex) - { - return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); - } - } - } -} diff --git a/src/Ombi.HealthChecks/Checks/OmbiPingHealthCheckOptions.cs b/src/Ombi.HealthChecks/Checks/OmbiPingHealthCheckOptions.cs deleted file mode 100644 index f89c71a52..000000000 --- a/src/Ombi.HealthChecks/Checks/OmbiPingHealthCheckOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.Diagnostics.HealthChecks; -using System.Collections.Generic; - -namespace Ombi.HealthChecks.Checks -{ - public class OmbiPingHealthCheckOptions - { - internal Dictionary ConfiguredHosts { get; } = new Dictionary(); - - public OmbiPingHealthCheckOptions AddHost(string host, int timeout, HealthStatus status) - { - ConfiguredHosts.Add(host, (host, timeout, status)); - return this; - } - } -} diff --git a/src/Ombi.HealthChecks/Checks/PlexHealthCheck.cs b/src/Ombi.HealthChecks/Checks/PlexHealthCheck.cs index 182c1b2f8..bc90c4756 100644 --- a/src/Ombi.HealthChecks/Checks/PlexHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/PlexHealthCheck.cs @@ -27,7 +27,7 @@ namespace Ombi.HealthChecks.Checks var settings = await settingsProvider.GetSettingsAsync(); if (settings == null) { - return HealthCheckResult.Healthy("Plex is not confiured."); + return HealthCheckResult.Healthy("Plex is not configured."); } var taskResult = new List>(); diff --git a/src/Ombi.HealthChecks/HealthCheckExtensions.cs b/src/Ombi.HealthChecks/HealthCheckExtensions.cs index 2f80378ff..21b9d5dde 100644 --- a/src/Ombi.HealthChecks/HealthCheckExtensions.cs +++ b/src/Ombi.HealthChecks/HealthCheckExtensions.cs @@ -18,39 +18,8 @@ namespace Ombi.HealthChecks builder.AddCheck("Radarr", tags: new string[] { "DVR" }); builder.AddCheck("CouchPotato", tags: new string[] { "DVR" }); builder.AddCheck("SickRage", tags: new string[] { "DVR" }); - builder.AddOmbiPingHealthCheck(options => - { - options.AddHost("www.google.co.uk", 5000, HealthStatus.Unhealthy); - options.AddHost("www.google.com", 3000, HealthStatus.Degraded); - }, "External Ping", tags: new string[] { "System" }); return builder; } - - /// - /// Add a health check for network ping. - /// - /// The . - /// The action to configure the ping parameters. - /// The health check name. Optional. If null the type name 'ping' will be used for the name. - /// - /// The that should be reported when the health check fails. Optional. If null then - /// the default status of will be reported. - /// - /// A list of tags that can be used to filter sets of health checks. Optional. - /// An optional System.TimeSpan representing the timeout of the check. - /// The . - public static IHealthChecksBuilder AddOmbiPingHealthCheck(this IHealthChecksBuilder builder, Action setup, string name = default, HealthStatus? failureStatus = default, IEnumerable tags = default, TimeSpan? timeout = default) - { - var options = new OmbiPingHealthCheckOptions(); - setup?.Invoke(options); - - return builder.Add(new HealthCheckRegistration( - name, - sp => new OmbiPingHealthCheck(options), - failureStatus, - tags, - timeout)); - } } } diff --git a/src/Ombi.I18n/Resources/Texts.hu.resx b/src/Ombi.I18n/Resources/Texts.hu.resx index 4e7cebcfd..bd9f1a19e 100644 --- a/src/Ombi.I18n/Resources/Texts.hu.resx +++ b/src/Ombi.I18n/Resources/Texts.hu.resx @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - New Albums + Új album - New Movies + Új film - New TV + Új sorozat Műfaj: @@ -139,18 +139,18 @@ Epizódok: - Powered by + Biztosítja a(z) - Unsubscribe + Leiratkozás Album - Movie + Film - TV Show + Sorozat \ No newline at end of file diff --git a/src/Ombi.I18n/Resources/Texts.nl.resx b/src/Ombi.I18n/Resources/Texts.nl.resx index e785eb494..873d68ac2 100644 --- a/src/Ombi.I18n/Resources/Texts.nl.resx +++ b/src/Ombi.I18n/Resources/Texts.nl.resx @@ -148,9 +148,9 @@ Album - Movie + Film - TV Show + TV-serie \ No newline at end of file diff --git a/src/Ombi.I18n/Resources/Texts.no.resx b/src/Ombi.I18n/Resources/Texts.no.resx index bf0daa456..b3fd2fe8e 100644 --- a/src/Ombi.I18n/Resources/Texts.no.resx +++ b/src/Ombi.I18n/Resources/Texts.no.resx @@ -118,16 +118,16 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - New Albums + Nye Album - New Movies + Nye filmer - New TV + Nye TV-serier - Genres: + Sjangere: Type: @@ -136,21 +136,21 @@ Sesong: - Episodes: + Episoder: - Powered by + Drevet av - Unsubscribe + Avslutt abonnement Album - Movie + Film - TV Show + TV-serie \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/DiscordNotification.cs b/src/Ombi.Notifications/Agents/DiscordNotification.cs index ae344116f..a1619de69 100644 --- a/src/Ombi.Notifications/Agents/DiscordNotification.cs +++ b/src/Ombi.Notifications/Agents/DiscordNotification.cs @@ -107,7 +107,7 @@ namespace Ombi.Notifications.Agents var discordBody = new DiscordWebhookBody { content = model.Message, - username = settings.Username, + username = settings.Username ?? "Ombi", }; var fields = new List(); diff --git a/src/Ombi.Notifications/NotificationMessageCurlys.cs b/src/Ombi.Notifications/NotificationMessageCurlys.cs index f079c03a3..c1fecb00c 100644 --- a/src/Ombi.Notifications/NotificationMessageCurlys.cs +++ b/src/Ombi.Notifications/NotificationMessageCurlys.cs @@ -153,6 +153,7 @@ namespace Ombi.Notifications RequestedUser = req?.RequestedUser?.UserName; RequestedDate = req?.RequestedDate.ToString("D"); DetailsUrl = GetDetailsUrl(s, req); + RequestedByAlias = req?.RequestedByAlias; if (Type.IsNullOrEmpty()) { @@ -276,6 +277,7 @@ namespace Ombi.Notifications // User Defined public string RequestId { get; set; } public string RequestedUser { get; set; } + public string RequestedByAlias { get; set; } public string UserName { get; set; } public string IssueUser => UserName; public string Alias { get; set; } @@ -339,6 +341,7 @@ namespace Ombi.Notifications { nameof(IssueUser), IssueUser }, { nameof(UserName), UserName }, { nameof(Alias), Alias }, + { nameof(RequestedByAlias), RequestedByAlias }, { nameof(UserPreference), UserPreference }, { nameof(DenyReason), DenyReason }, { nameof(AvailableDate), AvailableDate }, diff --git a/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs b/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs index 9537710a3..c1550e52c 100644 --- a/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs +++ b/src/Ombi.Schedule.Tests/PlexWatchlistImportTests.cs @@ -4,6 +4,8 @@ using Moq.AutoMock; using NUnit.Framework; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Engine; using Ombi.Core.Engine.Interfaces; using Ombi.Core.Models.Requests; @@ -55,7 +57,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task TerminatesWhenWatchlistIsNotEnabled() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false }); await _subject.Execute(null); _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); @@ -145,7 +147,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task NoPlexUsersWithToken() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); var um = MockHelper.MockUserManager(new List { @@ -170,7 +172,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MultipleUsers() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); var um = MockHelper.MockUserManager(new List { @@ -194,7 +196,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_NoGuid() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -245,7 +247,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task TvRequestFromWatchList_NoGuid() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -295,7 +297,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_AlreadyRequested() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -394,7 +396,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_NoTmdbGuid() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -433,6 +435,7 @@ namespace Ombi.Schedule.Tests }); _mocker.Setup>(x => x.RequestMovie(It.IsAny())) .ReturnsAsync(new RequestEngineResult { RequestId = 1 }); + await _subject.Execute(_context.Object); _mocker.Verify(x => x.RequestMovie(It.IsAny()), Times.Never); _mocker.Verify(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny()), Times.Once); @@ -441,10 +444,195 @@ namespace Ombi.Schedule.Tests _mocker.Verify>(x => x.GetAll(), Times.Never); } + [Test] + public async Task MovieRequestFromWatchList_NoTmdbGuid_LookupFromTdb() + { + + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); + _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer + { + MediaContainer = new PlexWatchlist + { + Metadata = new List + { + new Metadata + { + type = "movie", + ratingKey = "abc" + } + } + } + }); + _mocker.Setup>(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny())) + .ReturnsAsync(new PlexWatchlistMetadataContainer + { + MediaContainer = new PlexWatchlistMetadata + { + Metadata = new WatchlistMetadata[] + { + new WatchlistMetadata + { + Guid = new List + { + new PlexGuids + { + Id = "imdb://123" + } + } + } + } + + } + }); + _mocker.Setup>(x => x.RequestMovie(It.IsAny())) + .ReturnsAsync(new RequestEngineResult { RequestId = 1 }); + _mocker.Setup>(x => x.Find("123", ExternalSource.imdb_id)).ReturnsAsync(new FindResult + { + movie_results = new Movie_Results[] + { + new Movie_Results + { + id = 333 + } + } + }); + + await _subject.Execute(_context.Object); + _mocker.Verify(x => x.RequestMovie(It.Is(x => x.TheMovieDbId == 333)), Times.Once); + _mocker.Verify(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny()), Times.Once); + _mocker.Verify(x => x.SetUser(It.Is(x => x.Id == "abc")), Times.Once); + _mocker.Verify>(x => x.GetAll(), Times.Once); + _mocker.Verify>(x => x.Add(It.IsAny()), Times.Once); + _mocker.Verify(x => x.Find("123", ExternalSource.imdb_id), Times.Once); + } + + + [Test] + public async Task TvRequestFromWatchList_NoTmdbGuid_LookupFromTdb() + { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true }); + _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer + { + MediaContainer = new PlexWatchlist + { + Metadata = new List + { + new Metadata + { + type = "show", + ratingKey = "abc" + } + } + } + }); + _mocker.Setup>(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny())) + .ReturnsAsync(new PlexWatchlistMetadataContainer + { + MediaContainer = new PlexWatchlistMetadata + { + Metadata = new WatchlistMetadata[] + { + new WatchlistMetadata + { + Guid = new List + { + new PlexGuids + { + Id = "imdbid://123" + } + } + } + } + + } + }); + + _mocker.Setup>(x => x.Find("123", ExternalSource.imdb_id)).ReturnsAsync(new FindResult + { + tv_results = new TvResults[] + { + new TvResults + { + id = 333 + } + } + }); + _mocker.Setup>(x => x.RequestTvShow(It.IsAny())) + .ReturnsAsync(new RequestEngineResult { RequestId = 1 }); + await _subject.Execute(_context.Object); + _mocker.Verify(x => x.RequestTvShow(It.Is(x => x.TheMovieDbId == 333 && x.LatestSeason == false && x.RequestAll == true)), Times.Once); + _mocker.Verify(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny()), Times.Once); + _mocker.Verify(x => x.SetUser(It.Is(x => x.Id == "abc")), Times.Once); + _mocker.Verify>(x => x.GetAll(), Times.Once); + _mocker.Verify>(x => x.Add(It.IsAny()), Times.Once); + _mocker.Verify(x => x.Find("123", ExternalSource.imdb_id), Times.Once); + } + + [Test] + public async Task TvRequestFromWatchList_NoTmdbGuid_LookupFromTdb_ViaTvDb() + { + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true, MonitorAll = true }); + _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer + { + MediaContainer = new PlexWatchlist + { + Metadata = new List + { + new Metadata + { + type = "show", + ratingKey = "abc" + } + } + } + }); + _mocker.Setup>(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny())) + .ReturnsAsync(new PlexWatchlistMetadataContainer + { + MediaContainer = new PlexWatchlistMetadata + { + Metadata = new WatchlistMetadata[] + { + new WatchlistMetadata + { + Guid = new List + { + new PlexGuids + { + Id = "thetvdb://123" + } + } + } + } + + } + }); + + _mocker.Setup>(x => x.Find("123", ExternalSource.tvdb_id)).ReturnsAsync(new FindResult + { + tv_results = new TvResults[] + { + new TvResults + { + id = 333 + } + } + }); + _mocker.Setup>(x => x.RequestTvShow(It.IsAny())) + .ReturnsAsync(new RequestEngineResult { RequestId = 1 }); + await _subject.Execute(_context.Object); + _mocker.Verify(x => x.RequestTvShow(It.Is(x => x.TheMovieDbId == 333 && x.LatestSeason == false && x.RequestAll == true)), Times.Once); + _mocker.Verify(x => x.GetWatchlistMetadata("abc", It.IsAny(), It.IsAny()), Times.Once); + _mocker.Verify(x => x.SetUser(It.Is(x => x.Id == "abc")), Times.Once); + _mocker.Verify>(x => x.GetAll(), Times.Once); + _mocker.Verify>(x => x.Add(It.IsAny()), Times.Once); + _mocker.Verify(x => x.Find("123", ExternalSource.tvdb_id), Times.Once); + } + [Test] public async Task TvRequestFromWatchList_NoTmdbGuid() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { @@ -494,7 +682,7 @@ namespace Ombi.Schedule.Tests [Test] public async Task MovieRequestFromWatchList_AlreadyImported() { - + _mocker.Setup, Task>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true }); _mocker.Setup>(x => x.GetWatchlist(It.IsAny(), It.IsAny())).ReturnsAsync(new PlexWatchlistContainer { diff --git a/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs b/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs index 2137dd329..2c0bedc45 100644 --- a/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs +++ b/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs @@ -79,11 +79,9 @@ namespace Ombi.Schedule.Jobs.Couchpotato await strat.ExecuteAsync(async () => { // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) - { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); - tran.Commit(); - } + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); + await tran.CommitAsync(); }); // Save @@ -107,13 +105,11 @@ namespace Ombi.Schedule.Jobs.Couchpotato strat = _ctx.Database.CreateExecutionStrategy(); await strat.ExecuteAsync(async () => { - using (var tran = await _ctx.Database.BeginTransactionAsync()) - { - await _ctx.CouchPotatoCache.AddRangeAsync(movieIds); + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.CouchPotatoCache.AddRangeAsync(movieIds); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + await tran.CommitAsync(); }); await _notification.SendNotificationToAdmins("Couch Potato Sync Finished"); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 35cc66be4..7b301d4ed 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -8,10 +8,12 @@ using Ombi.Api.Emby; using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models.Media.Tv; using Ombi.Api.Emby.Models.Movie; +using Ombi.Core.Services; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Hubs; +using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; using Ombi.Store.Repository; using Quartz; @@ -19,108 +21,43 @@ using MediaType = Ombi.Store.Entities.MediaType; namespace Ombi.Schedule.Jobs.Emby { - public class EmbyContentSync : IEmbyContentSync + public class EmbyContentSync : EmbyLibrarySync, IEmbyContentSync { - public EmbyContentSync(ISettingsService settings, IEmbyApiFactory api, ILogger logger, - IEmbyContentRepository repo, INotificationHubService notification) + public EmbyContentSync( + ISettingsService settings, + IEmbyApiFactory api, + ILogger logger, + IEmbyContentRepository repo, + INotificationHubService notification, + IFeatureService feature): + base(settings, api, logger, notification) { - _logger = logger; - _settings = settings; - _apiFactory = api; _repo = repo; - _notification = notification; + _feature = feature; } - private readonly ILogger _logger; - private readonly ISettingsService _settings; - private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyContentRepository _repo; - private readonly INotificationHubService _notification; + private readonly IFeatureService _feature; - private const int AmountToTake = 100; - private IEmbyApi Api { get; set; } - - public async Task Execute(IJobExecutionContext context) + public async override Task Execute(IJobExecutionContext context) { - JobDataMap dataMap = context.JobDetail.JobDataMap; - var recentlyAddedSearch = false; - if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj)) - { - recentlyAddedSearch = Convert.ToBoolean(recentlyAddedObj); - } - var embySettings = await _settings.GetSettingsAsync(); - if (!embySettings.Enable) - return; + await base.Execute(context); - Api = _apiFactory.CreateClient(embySettings); - - await _notification.SendNotificationToAdmins(recentlyAddedSearch ? "Emby Recently Added Started" : "Emby Content Sync Started"); - - foreach (var server in embySettings.Servers) - { - try - { - await StartServerCache(server, recentlyAddedSearch); - } - catch (Exception e) - { - await _notification.SendNotificationToAdmins("Emby Content Sync Failed"); - _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); - } - } - - await _notification.SendNotificationToAdmins("Emby Content Sync Finished"); // Episodes + await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyEpisodeSync), "Emby"), new JobDataMap(new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAdded.ToString() } })); - - await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyEpisodeSync), "Emby"), new JobDataMap(new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAddedSearch.ToString() } })); - } - - - private async Task StartServerCache(EmbyServers server, bool recentlyAdded) - { - if (!ValidateSettings(server)) + // Played state + var isPlayedSyncEnabled = await _feature.FeatureEnabled(FeatureNames.PlayedSync); + if(isPlayedSyncEnabled) { - return; - } - - - if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) - { - var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); - - foreach (var movieParentIdFilder in movieLibsToFilter) - { - _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'"); - await ProcessMovies(server, recentlyAdded, movieParentIdFilder.Key); - } - - var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); - foreach (var tvParentIdFilter in tvLibsToFilter) - { - _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); - await ProcessTv(server, recentlyAdded, tvParentIdFilter.Key); - } - - - var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed"); - foreach (var m in mixedLibs) - { - _logger.LogInformation($"Scanning Lib '{m.Title}'"); - await ProcessTv(server, recentlyAdded, m.Key); - await ProcessMovies(server, recentlyAdded, m.Key); - } - } - else - { - await ProcessMovies(server, recentlyAdded); - await ProcessTv(server, recentlyAdded); + await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyPlayedSync), "Emby"), new JobDataMap(new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAdded.ToString() } })); } } - private async Task ProcessTv(EmbyServers server, bool recentlyAdded, string parentId = default) + + protected async override Task ProcessTv(EmbyServers server, string parentId = default) { // TV Time var mediaToAdd = new HashSet(); @@ -196,7 +133,7 @@ namespace Ombi.Schedule.Jobs.Emby await _repo.AddRange(mediaToAdd); } - private async Task ProcessMovies(EmbyServers server, bool recentlyAdded, string parentId = default) + protected override async Task ProcessMovies(EmbyServers server, string parentId = default) { EmbyItemContainer movies; if (recentlyAdded) @@ -263,7 +200,12 @@ namespace Ombi.Schedule.Jobs.Emby // Check if it exists var existingMovie = await _repo.GetByEmbyId(movieInfo.Id); var alreadyGoingToAdd = content.Any(x => x.EmbyId == movieInfo.Id); - if (existingMovie == null && !alreadyGoingToAdd) + if (alreadyGoingToAdd) + { + _logger.LogDebug($"Detected duplicate for {movieInfo.Name}"); + return; + } + if (existingMovie == null) { if (!movieInfo.ProviderIds.Any()) { @@ -319,36 +261,6 @@ namespace Ombi.Schedule.Jobs.Emby content.Quality = has4K ? null : quality; content.Has4K = has4K; } - - private bool ValidateSettings(EmbyServers server) - { - if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) - { - _logger.LogInformation(LoggingEvents.EmbyContentCacher, $"Server {server?.Name} is not configured correctly"); - return false; - } - - return true; - } - - private bool _disposed; - protected virtual void Dispose(bool disposing) - { - if (_disposed) - return; - - if (disposing) - { - //_settings?.Dispose(); - } - _disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs new file mode 100644 index 000000000..095364037 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs @@ -0,0 +1,146 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Emby; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Quartz; + +namespace Ombi.Schedule.Jobs.Emby +{ + public abstract class EmbyLibrarySync + { + public EmbyLibrarySync(ISettingsService settings, IEmbyApiFactory api, ILogger logger, + INotificationHubService notification) + { + _logger = logger; + _settings = settings; + _apiFactory = api; + _notification = notification; + } + + protected readonly ILogger _logger; + protected readonly ISettingsService _settings; + protected readonly IEmbyApiFactory _apiFactory; + protected bool recentlyAdded; + protected readonly INotificationHubService _notification; + + protected const int AmountToTake = 100; + + protected IEmbyApi Api { get; set; } + + public virtual async Task Execute(IJobExecutionContext context) + { + + JobDataMap dataMap = context.MergedJobDataMap; + if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj)) + { + recentlyAdded = Convert.ToBoolean(recentlyAddedObj); + } + + await _notification.SendNotificationToAdmins(recentlyAdded ? "Emby Recently Added Started" : "Emby Content Sync Started"); + + + var embySettings = await _settings.GetSettingsAsync(); + if (!embySettings.Enable) + return; + + Api = _apiFactory.CreateClient(embySettings); + + foreach (var server in embySettings.Servers) + { + try + { + await StartServerCache(server); + } + catch (Exception e) + { + await _notification.SendNotificationToAdmins("Emby Content Sync Failed"); + _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); + } + } + + await _notification.SendNotificationToAdmins("Emby Content Sync Finished"); + } + + + private async Task StartServerCache(EmbyServers server) + { + if (!ValidateSettings(server)) + { + return; + } + + + if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) + { + var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); + + foreach (var movieParentIdFilder in movieLibsToFilter) + { + _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'"); + await ProcessMovies(server, movieParentIdFilder.Key); + } + + var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); + foreach (var tvParentIdFilter in tvLibsToFilter) + { + _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); + await ProcessTv(server, tvParentIdFilter.Key); + } + + + var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed"); + foreach (var m in mixedLibs) + { + _logger.LogInformation($"Scanning Lib '{m.Title}'"); + await ProcessTv(server, m.Key); + await ProcessMovies(server, m.Key); + } + } + else + { + await ProcessMovies(server); + await ProcessTv(server); + } + } + + protected abstract Task ProcessTv(EmbyServers server, string parentId = default); + + protected abstract Task ProcessMovies(EmbyServers server, string parentId = default); + + private bool ValidateSettings(EmbyServers server) + { + if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) + { + _logger.LogInformation(LoggingEvents.EmbyContentCacher, $"Server {server?.Name} is not configured correctly"); + return false; + } + + return true; + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + //_settings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs new file mode 100644 index 000000000..36ed0ae7a --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs @@ -0,0 +1,229 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ombi.Api.Emby; +using Ombi.Api.Emby.Models; +using Ombi.Api.Emby.Models.Media.Tv; +using Ombi.Api.Emby.Models.Movie; +using Ombi.Core.Authentication; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Schedule.Jobs.Emby +{ + public class EmbyPlayedSync : EmbyLibrarySync, IEmbyPlayedSync + { + public EmbyPlayedSync( + ISettingsService settings, + IEmbyApiFactory api, + ILogger logger, + IUserPlayedMovieRepository movieRepo, + IUserPlayedEpisodeRepository episodeRepo, + IEmbyContentRepository contentRepo, + INotificationHubService notification, + OmbiUserManager user) : base(settings, api, logger, notification) + { + _userManager = user; + _movieRepo = movieRepo; + _contentRepo = contentRepo; + _episodeRepo = episodeRepo; + } + private OmbiUserManager _userManager { get; } + + private readonly IUserPlayedMovieRepository _movieRepo; + private readonly IUserPlayedEpisodeRepository _episodeRepo; + private readonly IEmbyContentRepository _contentRepo; + + protected async override Task ProcessTv(EmbyServers server, string parentId = default) + { + var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); + foreach (var user in allUsers) + { + await ProcessTvUser(server, user, parentId); + } + } + + protected async override Task ProcessMovies(EmbyServers server, string parentId = default) + { + + var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); + foreach (var user in allUsers) + { + await ProcessMoviesUser(server, user, parentId); + } + } + + + private async Task ProcessMoviesUser(EmbyServers server, OmbiUser user, string parentId = default) + { + EmbyItemContainer movies; + if (recentlyAdded) + { + var recentlyAddedAmountToTake = 5; // to be adjusted? + movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, 0, recentlyAddedAmountToTake, user.ProviderUserId, server.FullUri); + // Setting this so we don't attempt to grab more than we need + if (movies.TotalRecordCount > recentlyAddedAmountToTake) + { + movies.TotalRecordCount = recentlyAddedAmountToTake; + } + } + else + { + movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, 0, AmountToTake, user.ProviderUserId, server.FullUri); + } + var totalCount = movies.TotalRecordCount; + var processed = 0; + var mediaToAdd = new HashSet(); + + while (processed < totalCount) + { + foreach (var movie in movies.Items) + { + await ProcessMovie(movie, user, mediaToAdd, server); + processed++; + } + + // Get the next batch + // Recently Added should never be checked as the TotalRecords should equal the amount to take + if (!recentlyAdded) + { + movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, processed, AmountToTake, user.ProviderUserId, server.FullUri); + } + await _movieRepo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + } + } + + private async Task ProcessMovie(EmbyMovie movieInfo, OmbiUser user, ICollection content, EmbyServers server) + { + if (movieInfo.ProviderIds.Tmdb.IsNullOrEmpty()) + { + _logger.LogWarning($"Movie {movieInfo.Name} has no relevant metadata. Skipping."); + return; + } + var userPlayedMovie = new UserPlayedMovie() + { + TheMovieDbId = int.Parse(movieInfo.ProviderIds.Tmdb), + UserId = user.Id + }; + // Check if it exists + var existingMovie = await _movieRepo.Get(userPlayedMovie.TheMovieDbId, userPlayedMovie.UserId); + var alreadyGoingToAdd = content.Any(x => x.TheMovieDbId == userPlayedMovie.TheMovieDbId && x.UserId == userPlayedMovie.UserId); + if (existingMovie == null && !alreadyGoingToAdd) + { + content.Add(userPlayedMovie); + } + } + + private async Task ProcessTvUser(EmbyServers server, OmbiUser user, string parentId = default) + { + EmbyItemContainer episodes; + if (recentlyAdded) + { + var recentlyAddedAmountToTake = 10; // to be adjusted? + episodes = await Api.GetTvPlayed(server.ApiKey, parentId, 0, recentlyAddedAmountToTake, user.ProviderUserId, server.FullUri); + // Setting this so we don't attempt to grab more than we need + if (episodes.TotalRecordCount > recentlyAddedAmountToTake) + { + episodes.TotalRecordCount = recentlyAddedAmountToTake; + } + } + else + { + episodes = await Api.GetTvPlayed(server.ApiKey, parentId, 0, AmountToTake, user.ProviderUserId, server.FullUri); + } + var totalCount = episodes.TotalRecordCount; + var processed = 0; + var mediaToAdd = new HashSet(); + + while (processed < totalCount) + { + foreach (var episode in episodes.Items) + { + await ProcessTv(episode, user, mediaToAdd, server); + processed++; + } + + // Get the next batch + // Recently Added should never be checked as the TotalRecords should equal the amount to take + if (!recentlyAdded) + { + episodes = await Api.GetTvPlayed(server.ApiKey, parentId, processed, AmountToTake, user.ProviderUserId, server.FullUri); + } + await _episodeRepo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + } + } + + + private async Task ProcessTv(EmbyEpisodes episode, OmbiUser user, ICollection content, EmbyServers server) + { + + var parent = await _contentRepo.GetByEmbyId(episode.SeriesId); + if (parent == null) + { + _logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this", + episode.Name); + return; + } + if (parent.TheMovieDbId.IsNullOrEmpty()) + { + _logger.LogWarning($"Episode {episode.Name} is not linked to a TMDB series. Skipping."); + return; + } + + await AddToContent(content, new UserPlayedEpisode() + { + TheMovieDbId = int.Parse(parent.TheMovieDbId), + SeasonNumber = episode.ParentIndexNumber, + EpisodeNumber = episode.IndexNumber, + UserId = user.Id + }); + + if (episode.IndexNumberEnd.HasValue && episode.IndexNumberEnd.Value != episode.IndexNumber) + { + int episodeNumber = episode.IndexNumber; + do + { + _logger.LogDebug($"Multiple-episode file detected. Adding episode ${episodeNumber}"); + episodeNumber++; + + await AddToContent(content, new UserPlayedEpisode() + { + TheMovieDbId = int.Parse(parent.TheMovieDbId), + SeasonNumber = episode.ParentIndexNumber, + EpisodeNumber = episodeNumber, + UserId = user.Id + }); + + + } while (episodeNumber < episode.IndexNumberEnd.Value); + + } + } + + private async Task AddToContent(ICollection content, UserPlayedEpisode episode) + { + + // Check if it exists + var existingEpisode = await _episodeRepo.Get(episode.TheMovieDbId, episode.SeasonNumber, episode.EpisodeNumber, episode.UserId); + var alreadyGoingToAdd = content.Any(x => + x.TheMovieDbId == episode.TheMovieDbId + && x.SeasonNumber == episode.SeasonNumber + && x.EpisodeNumber == episode.EpisodeNumber + && x.UserId == episode.UserId); + if (existingEpisode == null && !alreadyGoingToAdd) + { + content.Add(episode); + } + } + } + + +} diff --git a/src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs b/src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs new file mode 100644 index 000000000..80434bddb --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs @@ -0,0 +1,6 @@ +namespace Ombi.Schedule.Jobs.Emby +{ + public interface IEmbyPlayedSync : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs index 58dacaed5..3eab17a35 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -228,7 +228,12 @@ namespace Ombi.Schedule.Jobs.Jellyfin // Check if it exists var existingMovie = await _repo.GetByJellyfinId(movieInfo.Id); var alreadyGoingToAdd = content.Any(x => x.JellyfinId == movieInfo.Id); - if (existingMovie == null && !alreadyGoingToAdd) + if (alreadyGoingToAdd) + { + _logger.LogDebug($"Detected duplicate for {movieInfo.Name}"); + return; + } + if (existingMovie == null) { if (!movieInfo.ProviderIds.Any()) { diff --git a/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs b/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs index 87922efd9..6f81a4539 100644 --- a/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs +++ b/src/Ombi.Schedule/Jobs/Lidarr/LidarrAlbumSync.cs @@ -55,7 +55,7 @@ namespace Ombi.Schedule.Jobs.Lidarr using (var tran = await _ctx.Database.BeginTransactionAsync()) { await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrAlbumCache"); - tran.Commit(); + await tran.CommitAsync(); } }); @@ -85,7 +85,7 @@ namespace Ombi.Schedule.Jobs.Lidarr await _ctx.LidarrAlbumCache.AddRangeAsync(albumCache); await _ctx.SaveChangesAsync(); - tran.Commit(); + await tran.CommitAsync(); } }); } diff --git a/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs b/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs index c4d9a3ee2..c8ef9e0f2 100644 --- a/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs +++ b/src/Ombi.Schedule/Jobs/Lidarr/LidarrArtistSync.cs @@ -52,11 +52,9 @@ namespace Ombi.Schedule.Jobs.Lidarr await strat.ExecuteAsync(async () => { // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) - { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrArtistCache"); - tran.Commit(); - } + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM LidarrArtistCache"); + await tran.CommitAsync(); }); var artistCache = new List(); @@ -76,13 +74,11 @@ namespace Ombi.Schedule.Jobs.Lidarr strat = _ctx.Database.CreateExecutionStrategy(); await strat.ExecuteAsync(async () => { - using (var tran = await _ctx.Database.BeginTransactionAsync()) - { - await _ctx.LidarrArtistCache.AddRangeAsync(artistCache); + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.LidarrArtistCache.AddRangeAsync(artistCache); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + await tran.CommitAsync(); }); } } diff --git a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index 46f3a7e56..d1363a455 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -15,15 +15,24 @@ namespace Ombi.Schedule.Jobs.Ombi { public class MediaDatabaseRefresh : IMediaDatabaseRefresh { - public MediaDatabaseRefresh(ISettingsService s, ILogger log, - IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo, - ISettingsService embySettings, ISettingsService jellyfinSettings) + public MediaDatabaseRefresh( + ISettingsService s, + ILogger log, + IPlexContentRepository plexRepo, + IEmbyContentRepository embyRepo, + IJellyfinContentRepository jellyfinRepo, + IUserPlayedMovieRepository userPlayedMovieRepo, + IUserPlayedEpisodeRepository userPlayedEpisodeRepo, + ISettingsService embySettings, + ISettingsService jellyfinSettings) { _plexSettings = s; _log = log; _plexRepo = plexRepo; _embyRepo = embyRepo; _jellyfinRepo = jellyfinRepo; + _userPlayedMovieRepo = userPlayedMovieRepo; + _userPlayedEpisodeRepo = userPlayedEpisodeRepo; _embySettings = embySettings; _jellyfinSettings = jellyfinSettings; _plexSettings.ClearCache(); @@ -34,6 +43,8 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly IPlexContentRepository _plexRepo; private readonly IEmbyContentRepository _embyRepo; private readonly IJellyfinContentRepository _jellyfinRepo; + private readonly IUserPlayedMovieRepository _userPlayedMovieRepo; + private readonly IUserPlayedEpisodeRepository _userPlayedEpisodeRepo; private readonly ISettingsService _embySettings; private readonly ISettingsService _jellyfinSettings; @@ -41,6 +52,7 @@ namespace Ombi.Schedule.Jobs.Ombi { try { + await RemovePlayedData(); await RemovePlexData(); await RemoveEmbyData(); await RemoveJellyfinData(); @@ -52,6 +64,23 @@ namespace Ombi.Schedule.Jobs.Ombi } + private async Task RemovePlayedData() + { + try + { + const string movieSql = "DELETE FROM UserPlayedMovie"; + await _userPlayedMovieRepo.ExecuteSql(movieSql); + + const string episodeSql = "DELETE FROM UserPlayedEpisode"; + await _userPlayedEpisodeRepo.ExecuteSql(episodeSql); + } + catch (Exception e) + { + _log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Played Data Failed"); + } + } + + private async Task RemoveEmbyData() { try diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs b/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs index 326cf35f9..85a926376 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexWatchlistImport.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.Logging; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; +using Ombi.Api.TheMovieDb; +using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Authentication; using Ombi.Core.Engine; using Ombi.Core.Engine.Interfaces; @@ -14,6 +16,7 @@ using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; using Quartz; +using Serilog; using System; using System.Collections.Generic; using System.Linq; @@ -30,13 +33,15 @@ namespace Ombi.Schedule.Jobs.Plex private readonly IMovieRequestEngine _movieRequestEngine; private readonly ITvRequestEngine _tvRequestEngine; private readonly INotificationHubService _notificationHubService; - private readonly ILogger _logger; + private readonly Microsoft.Extensions.Logging.ILogger _logger; private readonly IExternalRepository _watchlistRepo; private readonly IRepository _userError; + private readonly IMovieDbApi _movieDbApi; public PlexWatchlistImport(IPlexApi plexApi, ISettingsService settings, OmbiUserManager ombiUserManager, IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, INotificationHubService notificationHubService, - ILogger logger, IExternalRepository watchlistRepo, IRepository userError) + ILogger logger, IExternalRepository watchlistRepo, IRepository userError, + IMovieDbApi movieDbApi) { _plexApi = plexApi; _settings = settings; @@ -47,6 +52,7 @@ namespace Ombi.Schedule.Jobs.Plex _logger = logger; _watchlistRepo = watchlistRepo; _userError = userError; + _movieDbApi = movieDbApi; } public async Task Execute(IJobExecutionContext context) @@ -109,9 +115,16 @@ namespace Ombi.Schedule.Jobs.Plex var providerIds = await GetProviderIds(user.MediaServerToken, item, context?.CancellationToken ?? CancellationToken.None); if (!providerIds.TheMovieDb.HasValue()) { - _logger.LogWarning($"No TheMovieDb Id found for {item.title}, could not import via Plex WatchList"); - // We need a MovieDbId to support this; - continue; + // Try and use another Id to figure out TheMovieDB + var movieDbId = await FindTmdbIdFromAlternateSources(providerIds, item.type); + if (string.IsNullOrEmpty(movieDbId)) + { + _logger.LogWarning($"No TheMovieDb Id found for {item.title} for user {user.UserName}, could not import via Plex WatchList"); + // We need a MovieDbId to support this; + continue; + } + + providerIds.TheMovieDb = movieDbId; } // Check to see if we have already imported this item @@ -143,6 +156,43 @@ namespace Ombi.Schedule.Jobs.Plex await NotifyClient("Finished Watchlist Import"); } + private async Task FindTmdbIdFromAlternateSources(ProviderId providerId, string type) + { + FindResult result = null; + var hasResult = false; + var movie = type == "movie"; + if (!string.IsNullOrEmpty(providerId.TheTvDb)) + { + result = await _movieDbApi.Find(providerId.TheTvDb, ExternalSource.tvdb_id); + hasResult = movie ? result?.movie_results?.Length > 0 : result?.tv_results?.Length > 0; + } + if (!string.IsNullOrEmpty(providerId.ImdbId) && !hasResult) + { + result = await _movieDbApi.Find(providerId.ImdbId, ExternalSource.imdb_id); + if (movie) + { + hasResult = result?.movie_results?.Length > 0; + } + else + { + hasResult = result?.tv_results?.Length > 0; + } + } + if (hasResult) + { + if (movie) + { + return result.movie_results?[0]?.id.ToString() ?? string.Empty; + } + else + { + + return result.tv_results?[0]?.id.ToString() ?? string.Empty; + } + } + return string.Empty; + } + private async Task ProcessMovie(int theMovieDbId, OmbiUser user) { _movieRequestEngine.SetUser(user); diff --git a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs index 94970305a..3eaf0154a 100644 --- a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs @@ -40,18 +40,14 @@ namespace Ombi.Schedule.Jobs.Radarr { try { - var strat = _ctx.Database.CreateExecutionStrategy(); - await strat.ExecuteAsync(async () => - { - // Let's remove the old cached data - using var tran = await _ctx.Database.BeginTransactionAsync(); - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM RadarrCache"); - tran.Commit(); - }); + // Let's remove the old cached data + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM RadarrCache"); + await tran.CommitAsync(); var radarrSettings = _radarrSettings.GetSettingsAsync(); - var radarr4kSettings = _radarr4kSettings.GetSettingsAsync(); await Process(await radarrSettings); + var radarr4kSettings = _radarr4kSettings.GetSettingsAsync(); await Process(await radarr4kSettings); } catch (Exception) diff --git a/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs b/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs index 91920dcd8..2882b7cf8 100644 --- a/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs +++ b/src/Ombi.Schedule/Jobs/SickRage/SickRageSync.cs @@ -49,11 +49,9 @@ namespace Ombi.Schedule.Jobs.SickRage var strat = _ctx.Database.CreateExecutionStrategy(); await strat.ExecuteAsync(async () => { - using (var tran = await _ctx.Database.BeginTransactionAsync()) - { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SickRageCache"); - tran.Commit(); - } + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SickRageCache"); + await tran.CommitAsync(); }); var entites = ids.Select(id => new SickRageCache { TvDbId = id }).ToList(); @@ -84,12 +82,10 @@ namespace Ombi.Schedule.Jobs.SickRage strat = _ctx.Database.CreateExecutionStrategy(); await strat.ExecuteAsync(async () => { - using (var tran = await _ctx.Database.BeginTransactionAsync()) - { - await _ctx.SickRageEpisodeCache.AddRangeAsync(episodesToAdd); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + using var tran = await _ctx.Database.BeginTransactionAsync(); + await _ctx.SickRageEpisodeCache.AddRangeAsync(episodesToAdd); + await _ctx.SaveChangesAsync(); + await tran.CommitAsync(); }); } } diff --git a/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs b/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs index 2bda53875..969f2b6cd 100644 --- a/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Sonarr/SonarrSync.cs @@ -69,7 +69,7 @@ namespace Ombi.Schedule.Jobs.Sonarr { using var tran = await _ctx.Database.BeginTransactionAsync(); await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SonarrCache"); - tran.Commit(); + await tran.CommitAsync(); }); var sonarrCacheToSave = new HashSet(); @@ -97,7 +97,7 @@ namespace Ombi.Schedule.Jobs.Sonarr { using var tran = await _ctx.Database.BeginTransactionAsync(); await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM SonarrEpisodeCache"); - tran.Commit(); + await tran.CommitAsync(); }); foreach (var s in ids) @@ -156,7 +156,7 @@ namespace Ombi.Schedule.Jobs.Sonarr await _ctx.SonarrEpisodeCache.AddRangeAsync(episodesToAdd); _log.LogDebug("Commiting the transaction"); await _ctx.SaveChangesAsync(); - tran.Commit(); + await tran.CommitAsync(); }); } diff --git a/src/Ombi.Schedule/OmbiScheduler.cs b/src/Ombi.Schedule/OmbiScheduler.cs index 41602f641..3b4f6316f 100644 --- a/src/Ombi.Schedule/OmbiScheduler.cs +++ b/src/Ombi.Schedule/OmbiScheduler.cs @@ -99,6 +99,7 @@ namespace Ombi.Schedule await OmbiQuartz.Instance.AddJob(nameof(IEmbyContentSync), "Emby", JobSettingsHelper.EmbyContent(s)); await OmbiQuartz.Instance.AddJob(nameof(IEmbyContentSync) + "RecentlyAdded", "Emby", JobSettingsHelper.EmbyRecentlyAddedSync(s), new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, "true" } }); await OmbiQuartz.Instance.AddJob(nameof(IEmbyEpisodeSync), "Emby", null); + await OmbiQuartz.Instance.AddJob(nameof(IEmbyPlayedSync), "Emby", null); await OmbiQuartz.Instance.AddJob(nameof(IEmbyAvaliabilityChecker), "Emby", null); await OmbiQuartz.Instance.AddJob(nameof(IEmbyUserImporter), "Emby", JobSettingsHelper.UserImporter(s)); } diff --git a/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs b/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs index ed2775480..4e40bcee7 100644 --- a/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs +++ b/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs @@ -15,5 +15,6 @@ namespace Ombi.Settings.Settings.Models public bool EnableOAuth { get; set; } // Plex OAuth public bool EnableHeaderAuth { get; set; } // Header SSO public string HeaderAuthVariable { get; set; } // Header SSO + public bool HeaderAuthCreateUser { get; set; } // Header SSO } } \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/External/RadarrSettings.cs b/src/Ombi.Settings/Settings/Models/External/RadarrSettings.cs index 1b3e0982f..1791761c5 100644 --- a/src/Ombi.Settings/Settings/Models/External/RadarrSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/RadarrSettings.cs @@ -9,6 +9,8 @@ public bool AddOnly { get; set; } public string MinimumAvailability { get; set; } public bool ScanForAvailability { get; set; } + public int? Tag { get; set; } + public bool SendUserTags { get; set; } } public class Radarr4KSettings : RadarrSettings diff --git a/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs b/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs index 8e0b37524..332d97357 100644 --- a/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/SonarrSettings.cs @@ -19,11 +19,11 @@ public string RootPathAnime { get; set; } public int? AnimeTag { get; set; } public int? Tag { get; set; } + public bool SendUserTags { get; set; } public bool AddOnly { get; set; } public int LanguageProfile { get; set; } public int LanguageProfileAnime { get; set; } public bool ScanForAvailability { get; set; } - public bool SendUserTags { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/FeatureSettings.cs b/src/Ombi.Settings/Settings/Models/FeatureSettings.cs index 9d0149e5d..f541d1e0d 100644 --- a/src/Ombi.Settings/Settings/Models/FeatureSettings.cs +++ b/src/Ombi.Settings/Settings/Models/FeatureSettings.cs @@ -21,5 +21,6 @@ namespace Ombi.Settings.Settings.Models { public const string Movie4KRequests = nameof(Movie4KRequests); public const string OldTrendingSource = nameof(OldTrendingSource); + public const string PlayedSync = nameof(PlayedSync); } } diff --git a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs index b80943cc5..fb51ae147 100644 --- a/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs +++ b/src/Ombi.Settings/Settings/Models/JobSettingsHelper.cs @@ -1,5 +1,6 @@ using Ombi.Helpers; using Quartz; +using System; namespace Ombi.Settings.Settings.Models { @@ -104,7 +105,9 @@ namespace Ombi.Settings.Settings.Models private static string ValidateCron(string cron) { - if (CronExpression.IsValidExpression(cron)) + CronExpression expression = new CronExpression(cron); + DateTimeOffset? nextFireUTCTime = expression.GetNextValidTimeAfter(DateTime.Now); + if (CronExpression.IsValidExpression(cron) && nextFireUTCTime != null) { return cron; } diff --git a/src/Ombi.Store/Context/ExternalContext.cs b/src/Ombi.Store/Context/ExternalContext.cs index f13c1e74f..da6bcce71 100644 --- a/src/Ombi.Store/Context/ExternalContext.cs +++ b/src/Ombi.Store/Context/ExternalContext.cs @@ -41,6 +41,8 @@ namespace Ombi.Store.Context public DbSet SonarrEpisodeCache { get; set; } public DbSet SickRageCache { get; set; } public DbSet SickRageEpisodeCache { get; set; } + public DbSet UserPlayedMovie { get; set; } + public DbSet UserPlayedEpisode { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 0139e2adc..eea83a03d 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -61,22 +61,20 @@ namespace Ombi.Store.Context var strat = Database.CreateExecutionStrategy(); strat.Execute(() => { - using (var tran = Database.BeginTransaction()) + using var tran = Database.BeginTransaction(); + // Make sure we have the API User + var apiUserExists = Users.ToList().Any(x => x.NormalizedUserName == "API"); + if (!apiUserExists) { - // Make sure we have the API User - var apiUserExists = Users.ToList().Any(x => x.NormalizedUserName == "API"); - if (!apiUserExists) + Users.Add(new OmbiUser { - Users.Add(new OmbiUser - { - UserName = "Api", - UserType = UserType.SystemUser, - NormalizedUserName = "API", - StreamingCountry = "US" - }); - SaveChanges(); - tran.Commit(); - } + UserName = "Api", + UserType = UserType.SystemUser, + NormalizedUserName = "API", + StreamingCountry = "US" + }); + SaveChanges(); + tran.Commit(); } }); @@ -233,11 +231,9 @@ namespace Ombi.Store.Context { strat.Execute(() => { - using (var tran = Database.BeginTransaction()) - { - SaveChanges(); - tran.Commit(); - } + using var tran = Database.BeginTransaction(); + SaveChanges(); + tran.Commit(); }); } } diff --git a/src/Ombi.Store/Entities/Requests/ChildRequests.cs b/src/Ombi.Store/Entities/Requests/ChildRequests.cs index 9d4376452..dca501456 100644 --- a/src/Ombi.Store/Entities/Requests/ChildRequests.cs +++ b/src/Ombi.Store/Entities/Requests/ChildRequests.cs @@ -59,6 +59,9 @@ namespace Ombi.Store.Entities.Requests return string.Empty; } } + + [NotMapped] + public int RequestedUserPlayedProgress { get; set; } } public enum SeriesType diff --git a/src/Ombi.Store/Entities/Requests/MovieRequests.cs b/src/Ombi.Store/Entities/Requests/MovieRequests.cs index 415efded2..3c3c75893 100644 --- a/src/Ombi.Store/Entities/Requests/MovieRequests.cs +++ b/src/Ombi.Store/Entities/Requests/MovieRequests.cs @@ -84,5 +84,10 @@ namespace Ombi.Store.Entities.Requests [NotMapped] public override bool CanApprove => !Approved && !Available || !Approved4K && !Available4K; + + [NotMapped] + public bool WatchedByRequestedUser { get; set; } + [NotMapped] + public int PlayedByUsersCount { get; set; } } } diff --git a/src/Ombi.Store/Entities/UserPlayedEpisode.cs b/src/Ombi.Store/Entities/UserPlayedEpisode.cs new file mode 100644 index 000000000..2966f4892 --- /dev/null +++ b/src/Ombi.Store/Entities/UserPlayedEpisode.cs @@ -0,0 +1,10 @@ +namespace Ombi.Store.Entities +{ + public class UserPlayedEpisode : Entity + { + public int TheMovieDbId { get; set; } + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + public string UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Entities/UserPlayedMovie.cs b/src/Ombi.Store/Entities/UserPlayedMovie.cs new file mode 100644 index 000000000..7f28e9d99 --- /dev/null +++ b/src/Ombi.Store/Entities/UserPlayedMovie.cs @@ -0,0 +1,8 @@ +namespace Ombi.Store.Entities +{ + public class UserPlayedMovie : Entity + { + public int TheMovieDbId { get; set; } + public string UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs new file mode 100644 index 000000000..0e2e290b7 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs @@ -0,0 +1,566 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.MySql; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + [DbContext(typeof(ExternalMySqlContext))] + [Migration("20230406152218_MovieUserPlayed")] + partial class MovieUserPlayed + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .HasColumnType("longtext"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ForeignAlbumId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.Property("PercentOfTracks") + .HasColumnType("decimal(65,30)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TrackCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ArtistName") + .HasColumnType("longtext"); + + b.Property("ForeignArtistId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("GrandparentKey") + .HasColumnType("varchar(255)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("ParentKey") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ParentKey") + .HasColumnType("longtext"); + + b.Property("PlexContentId") + .HasColumnType("longtext"); + + b.Property("PlexServerContentId") + .HasColumnType("int"); + + b.Property("SeasonKey") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("ReleaseYear") + .HasColumnType("longtext"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TmdbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexWatchlistHistory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("HasRegular") + .HasColumnType("tinyint(1)"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("MovieDbId") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs new file mode 100644 index 000000000..48336a03d --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + public partial class MovieUserPlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedMovie", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TheMovieDbId = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedMovie", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedMovie"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs new file mode 100644 index 000000000..96cb5c176 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs @@ -0,0 +1,589 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.MySql; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + [DbContext(typeof(ExternalMySqlContext))] + [Migration("20230515182204_MovieEpisodePlayed")] + partial class MovieEpisodePlayed + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .HasColumnType("longtext"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ForeignAlbumId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.Property("PercentOfTracks") + .HasColumnType("decimal(65,30)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TrackCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ArtistName") + .HasColumnType("longtext"); + + b.Property("ForeignArtistId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("GrandparentKey") + .HasColumnType("varchar(255)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("ParentKey") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ParentKey") + .HasColumnType("longtext"); + + b.Property("PlexContentId") + .HasColumnType("longtext"); + + b.Property("PlexServerContentId") + .HasColumnType("int"); + + b.Property("SeasonKey") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("ReleaseYear") + .HasColumnType("longtext"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TmdbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexWatchlistHistory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("HasRegular") + .HasColumnType("tinyint(1)"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("MovieDbId") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.cs new file mode 100644 index 000000000..0e0cd54a3 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + public partial class MovieEpisodePlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedEpisode", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TheMovieDbId = table.Column(type: "int", nullable: false), + SeasonNumber = table.Column(type: "int", nullable: false), + EpisodeNumber = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedEpisode", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedEpisode"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs index 1e86ddf7b..c4de7028e 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs @@ -16,7 +16,7 @@ namespace Ombi.Store.Migrations.ExternalMySql { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("ProductVersion", "6.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => @@ -488,6 +488,46 @@ namespace Ombi.Store.Migrations.ExternalMySql b.ToTable("SonarrEpisodeCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => { b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs new file mode 100644 index 000000000..f1162e20f --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs @@ -0,0 +1,564 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.Sqlite; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + [DbContext(typeof(ExternalSqliteContext))] + [Migration("20230310130339_MovieUserPlayed")] + partial class MovieUserPlayed + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ForeignAlbumId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.Property("PercentOfTracks") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ArtistName") + .HasColumnType("TEXT"); + + b.Property("ForeignArtistId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("GrandparentKey") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ParentKey") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ParentKey") + .HasColumnType("TEXT"); + + b.Property("PlexContentId") + .HasColumnType("TEXT"); + + b.Property("PlexServerContentId") + .HasColumnType("INTEGER"); + + b.Property("SeasonKey") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("ReleaseYear") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexWatchlistHistory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("HasRegular") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("MovieDbId") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs new file mode 100644 index 000000000..23345e7a1 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + public partial class MovieUserPlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedMovie", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TheMovieDbId = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedMovie", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedMovie"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs new file mode 100644 index 000000000..adf675d22 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs @@ -0,0 +1,587 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.Sqlite; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + [DbContext(typeof(ExternalSqliteContext))] + [Migration("20230515161757_EpisodeUserPlayed")] + partial class EpisodeUserPlayed + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ForeignAlbumId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.Property("PercentOfTracks") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ArtistName") + .HasColumnType("TEXT"); + + b.Property("ForeignArtistId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("GrandparentKey") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ParentKey") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ParentKey") + .HasColumnType("TEXT"); + + b.Property("PlexContentId") + .HasColumnType("TEXT"); + + b.Property("PlexServerContentId") + .HasColumnType("INTEGER"); + + b.Property("SeasonKey") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("ReleaseYear") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexWatchlistHistory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("HasRegular") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("MovieDbId") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.cs new file mode 100644 index 000000000..b4125f905 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + public partial class EpisodeUserPlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedEpisode", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TheMovieDbId = table.Column(type: "INTEGER", nullable: false), + SeasonNumber = table.Column(type: "INTEGER", nullable: false), + EpisodeNumber = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedEpisode", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedEpisode"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs index 2f5de3382..e53ed4363 100644 --- a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Ombi.Store.Migrations.ExternalSqlite protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => { @@ -486,6 +486,46 @@ namespace Ombi.Store.Migrations.ExternalSqlite b.ToTable("SonarrEpisodeCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => { b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") diff --git a/src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.cs b/src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.cs new file mode 100644 index 000000000..8e292cb8a --- /dev/null +++ b/src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public interface IUserPlayedEpisodeRepository : IExternalRepository + { + Task Get(int theMovieDbId, int seasonNumber, int episodeNumber, string userId); + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs b/src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs new file mode 100644 index 000000000..966171b3a --- /dev/null +++ b/src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public interface IUserPlayedMovieRepository : IExternalRepository + { + Task Get(int theMovieDbId, string userId); + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs b/src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs new file mode 100644 index 000000000..c5dda0ef6 --- /dev/null +++ b/src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Ombi.Store.Context; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public class UserPlayedEpisodeRepository : ExternalRepository, IUserPlayedEpisodeRepository + { + protected ExternalContext Db { get; } + public UserPlayedEpisodeRepository(ExternalContext db) : base(db) + { + Db = db; + } + + public async Task Get(int theMovieDbId, int seasonNumber, int episodeNumber, string userId) + { + return await Db.UserPlayedEpisode.FirstOrDefaultAsync(x => x.TheMovieDbId == theMovieDbId && x.SeasonNumber == seasonNumber && x.EpisodeNumber == episodeNumber && x.UserId == userId); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Repository/UserPlayedMovieRepository.cs b/src/Ombi.Store/Repository/UserPlayedMovieRepository.cs new file mode 100644 index 000000000..aaff5f2b1 --- /dev/null +++ b/src/Ombi.Store/Repository/UserPlayedMovieRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Ombi.Store.Context; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public class UserPlayedMovieRepository : ExternalRepository, IUserPlayedMovieRepository + { + protected ExternalContext Db { get; } + public UserPlayedMovieRepository(ExternalContext db) : base(db) + { + Db = db; + } + + public async Task Get(int theMovieDbId, string userId) + { + return await Db.UserPlayedMovie.FirstOrDefaultAsync(x => x.TheMovieDbId == theMovieDbId && x.UserId == userId); + + } + } +} \ No newline at end of file diff --git a/src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs b/src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs new file mode 100644 index 000000000..576899476 --- /dev/null +++ b/src/Ombi.Tests/Middlewear/ApiKeyMiddlewearTests.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using Moq.AutoMock; +using NUnit.Framework; +using NUnit.Framework.Constraints; +using Ombi.Core.Authentication; +using Ombi.Test.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Tests.Middlewear +{ + [TestFixture] + public class ApiKeyMiddlewearTests + { + private AutoMocker _mocker; + private ApiKeyMiddlewear _subject; + private Mock _serviceProviderMock; + + [SetUp] + public void Setup() + { + _mocker = new AutoMocker(); + _serviceProviderMock = new Mock(); + _mocker.Use(_serviceProviderMock); + _subject = _mocker.CreateInstance(); + } + + [Test] + public async Task NonApiAccess() + { + var context = GetContext(); + context.Request.Path = "/notanapi"; + await _subject.Invoke(context); + + _mocker.Verify(x => x.GetService(It.IsAny()), Times.Never); + } + + [Test] + public async Task ValidateUserAccessToken() + { + var context = GetContext(); + context.Request.Path = "/api"; + context.Request.Headers.Add("UserAccessToken", new Microsoft.Extensions.Primitives.StringValues("test")); + var user = new Store.Entities.OmbiUser + { + UserAccessToken = "test", + UserName = "unit test" + }; + var umMock = MockHelper.MockUserManager(new List + { + user + }); + umMock.Setup(x => x.GetRolesAsync(user)).ReturnsAsync(new List { "Admin" }); + _mocker.Setup(x => x.GetService(typeof(OmbiUserManager))) + .Returns(umMock.Object); + + + await _subject.Invoke(context); + + _mocker.Verify(x => x.GetService(It.IsAny()), Times.Once); + umMock.Verify(x => x.UpdateAsync(user), Times.Once); + } + + [Test] + public async Task ValidateUserAccessToken_Token_Invalid() + { + var context = GetContext(); + context.Request.Path = "/api"; + context.Request.Headers.Add("UserAccessToken", new Microsoft.Extensions.Primitives.StringValues("invalid")); + var user = new Store.Entities.OmbiUser + { + UserAccessToken = "test", + UserName = "unit test" + }; + var umMock = MockHelper.MockUserManager(new List + { + user + }); + umMock.Setup(x => x.GetRolesAsync(user)).ReturnsAsync(new List { "Admin" }); + _mocker.Setup(x => x.GetService(typeof(OmbiUserManager))) + .Returns(umMock.Object); + + + await _subject.Invoke(context); + + Assert.That(context.Response.StatusCode, Is.EqualTo(401)); + umMock.Verify(x => x.UpdateAsync(user), Times.Never); + } + + private HttpContext GetContext() + { + var context = new DefaultHttpContext(); + context.RequestServices = _serviceProviderMock.Object; + return context; + } + } +} diff --git a/src/Ombi.Tests/Ombi.Tests.csproj b/src/Ombi.Tests/Ombi.Tests.csproj index ce3b45533..77e1769f9 100644 --- a/src/Ombi.Tests/Ombi.Tests.csproj +++ b/src/Ombi.Tests/Ombi.Tests.csproj @@ -9,6 +9,7 @@ + @@ -18,6 +19,7 @@ + diff --git a/src/Ombi/.vscode/settings.json b/src/Ombi/.vscode/settings.json index 6274dd334..96d5fba50 100644 --- a/src/Ombi/.vscode/settings.json +++ b/src/Ombi/.vscode/settings.json @@ -24,7 +24,8 @@ "details", "requests", "sonarr", - "plex" + "plex", + "wizard" ], "rpc.enabled": true } diff --git a/src/Ombi/ClientApp/.storybook/preview-body.html b/src/Ombi/ClientApp/.storybook/preview-body.html index 15a751241..c0e8acaa7 100644 --- a/src/Ombi/ClientApp/.storybook/preview-body.html +++ b/src/Ombi/ClientApp/.storybook/preview-body.html @@ -1,3 +1,7 @@ +