Merge pull request #2729 from tidusjar/develop

Develop
pull/2748/head v3.0.4119
Jamie 6 years ago committed by GitHub
commit 5ee25be669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,15 +1,148 @@
# Changelog
## (unreleased)
### **New Features**
- Added a global language flag that now applies to the search by default. [tidusjar]
- Updated the frontend packages (Using Angular 7 now) [TidusJar]
- Added capture of anonymous analytical data. [tidusjar]
- Added {AvailableDate} as a Notification Variable, this is the date the request was marked as available. See here: https://github.com/tidusjar/Ombi/wiki/Notification-Template-Variables. [tidusjar]
- Added the ability to search movies via the movie db with a different language! [tidusjar]
- Added the ability to specify a year when searching for movies. [tidusjar]
- Update NewsletterTemplate.html. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update HtmlTemplateGenerator.cs. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update HtmlTemplateGenerator.cs. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update NewsletterTemplate.html. [d1slact0r]
- Update HtmlTemplateGenerator.cs. [d1slact0r]
- Updated boostrap #2694. [Jamie]
- Added the ability to deny a request with a reason. [TidusJar]
## v3.0.4036 (2018-12-11)
- Update EmbyEpisodeSync.cs. [Jamie]
- Updated to .net core 2.2 and included a linux-arm64 build. [TidusJar]
### **Fixes**
- Made the newsletter use the default lanuage code set in the Ombi settings for movie information. [TidusJar]
- Save the language code against the request so we can use it later e.g. Sending to the DVR apps. [tidusjar]
- Fixed #2716. [tidusjar]
- Make the newsletter BCC the users rather than creating a million newsletters (Hopefully will stop SMTP providers from marking as spam). This does mean that the custom user customization in the newsletter will no longer work. [TidusJar]
- If we don't know the Plex agent, then see if it's a ImdbId, if it's not check the string for any episode and season hints #2695. [tidusjar]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Portuguese, Brazilian) [Jamie]
- New translations en.json (Polish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Italian) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Dutch) [Jamie]
- Made the search results the language specified in the search refinement. [tidusjar]
- Fixed #2704. [tidusjar]
- Now it is fixed :) [d1slact0r]
- Android please be nice now. [d1slact0r]
- Fixed title bit better. [d1slact0r]
- Fixed titles. [d1slact0r]
- This should fix the build for sure (stupid quotes) [d1slact0r]
- Fixes build. [d1slact0r]
- Rewritten the whole newsletter template. [d1slact0r]
- Fixed #2697. [tidusjar]
- Add linux-arm runtime identifier. [aptalca]
- Add back arm packages. [aptalca]
- Add arm32 package. [aptalca]
- Fixed #2691. [tidusjar]
- Fixed linting. [TidusJar]
- Fixed the Plex OAuth when going through the wizard. [TidusJar]
- Fixed #2678. [TidusJar]
- Deny reason for movie requests. [TidusJar]
- Set the landing and login pages background refresh to 15 seconds rather than 10 and 7. [TidusJar]
- Fixed a bug with us thinking future dated emby episodes are not available, Consoldated the emby and plex search rules (since they have the same logic) [TidusJar]
- Fixed build. [TidusJar]
## v3.0.4036 (2018-12-11)
### **New Features**
- Changelog. [Jamie]
- Added Sonarr v3 #2359. [TidusJar]
### **Fixes**
- !changelog. [Jamie]
- Fixed a missing translation. [Jamie]
- Fixed a potential security vulnerability. [Jamie]
- Sorted out some of the settings pages, trying to make it consistent. [Jamie]
@ -37,8 +170,7 @@
### **Fixes**
- Fixed #2601 [TidusJar]
- !changelog. [Jamie]
- Made the subscribe/unsubscribe button more obvious on the UI #2309. [Jamie]

@ -27,22 +27,22 @@ test: off
after_build:
- cmd: >-
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\windows.zip"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\windows.zip"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\osx.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\osx.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\linux.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\linux.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\linux-arm.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.1\windows-32bit.zip"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\linux-arm.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\windows-32bit.zip"
# appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm64.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.2\linux-arm64.tar.gz"

@ -26,7 +26,7 @@ var csProj = "./src/Ombi/Ombi.csproj"; // Path to the project.csproj
var solutionFile = "Ombi.sln"; // Solution file if needed
GitVersion versionInfo = null;
var frameworkVer = "netcoreapp2.1";
var frameworkVer = "netcoreapp2.2";
var buildSettings = new DotNetCoreBuildSettings
{
@ -151,7 +151,7 @@ Task("Package")
GZipCompress(osxArtifactsFolder, artifactsFolder + "osx.tar.gz");
GZipCompress(linuxArtifactsFolder, artifactsFolder + "linux.tar.gz");
GZipCompress(linuxArmArtifactsFolder, artifactsFolder + "linux-arm.tar.gz");
//GZipCompress(linuxArm64BitArtifactsFolder, artifactsFolder + "linux-arm64.tar.gz");
GZipCompress(linuxArm64BitArtifactsFolder, artifactsFolder + "linux-arm64.tar.gz");
});
Task("Publish")
@ -227,7 +227,7 @@ Task("Publish-Linux-ARM")
CopyFile(
buildDir + "/"+frameworkVer+"/linux-arm/Swagger.xml",
buildDir + "/"+frameworkVer+"/linux-arm/published/Swagger.xml");
publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer +"/linux-arm/published/updater");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
});

291
package-lock.json generated

@ -1,291 +0,0 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "1.0.3"
}
},
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
"integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
"requires": {
"chalk": "1.1.3",
"esutils": "2.0.2",
"js-tokens": "3.0.2"
},
"dependencies": {
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"requires": {
"ansi-styles": "2.2.1",
"escape-string-regexp": "1.0.5",
"has-ansi": "2.0.0",
"strip-ansi": "3.0.1",
"supports-color": "2.0.0"
}
}
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
},
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"requires": {
"ansi-styles": "3.2.1",
"escape-string-regexp": "1.0.5",
"supports-color": "5.4.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "1.9.1"
}
},
"supports-color": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
"requires": {
"has-flag": "3.0.0"
}
}
}
},
"color-convert": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
"integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"esprima": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"has-ansi": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
"requires": {
"ansi-regex": "2.1.1"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-yaml": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
"requires": {
"argparse": "1.0.10",
"esprima": "4.0.0"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "1.1.11"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-parse": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME="
},
"resolve": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
"integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==",
"requires": {
"path-parse": "1.0.5"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "2.1.1"
}
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
},
"tslib": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz",
"integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw=="
},
"tslint": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz",
"integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=",
"requires": {
"babel-code-frame": "6.26.0",
"builtin-modules": "1.1.1",
"chalk": "2.4.1",
"commander": "2.15.1",
"diff": "3.5.0",
"glob": "7.1.2",
"js-yaml": "3.12.0",
"minimatch": "3.0.4",
"resolve": "1.7.1",
"semver": "5.5.0",
"tslib": "1.9.2",
"tsutils": "2.27.1"
}
},
"tsutils": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz",
"integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==",
"requires": {
"tslib": "1.9.2"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
<ItemGroup>

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

@ -44,6 +44,7 @@ namespace Ombi.Api.Sonarr.Models
public DateTime added { get; set; }
public Ratings ratings { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int id { get; set; }
public DateTime nextAiring { get; set; }
}

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Polly" Version="6.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
</ItemGroup>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -10,7 +10,7 @@
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0"></packagereference>
</ItemGroup>
<ItemGroup>

@ -157,6 +157,24 @@ namespace Ombi.Core.Engine
}
}
private string defaultLangCode;
protected async Task<string> DefaultLanguageCode(string currentCode)
{
if (currentCode.HasValue())
{
return currentCode;
}
var s = await GetOmbiSettings();
return s.DefaultLanguageCode;
}
private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings()
{
return ombiSettings ?? (ombiSettings = await OmbiSettings.GetSettingsAsync());
}
public class HideResult
{
public bool Hide { get; set; }

@ -12,7 +12,7 @@ namespace Ombi.Core.Engine
{
Task<RequestEngineResult>ApproveAlbum(AlbumRequest request);
Task<RequestEngineResult> ApproveAlbumById(int requestId);
Task<RequestEngineResult> DenyAlbumById(int modelId);
Task<RequestEngineResult> DenyAlbumById(int modelId, string reason);
Task<IEnumerable<AlbumRequest>> GetRequests();
Task<RequestsViewModel<AlbumRequest>> GetRequests(int count, int position, OrderFilterModel orderFilter);
Task<int> GetTotal();

@ -10,14 +10,14 @@ namespace Ombi.Core
Task<IEnumerable<SearchMovieViewModel>> PopularMovies();
Task<IEnumerable<SearchMovieViewModel>> Search(string search);
Task<IEnumerable<SearchMovieViewModel>> Search(string search, int? year, string languageCode);
Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies();
Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies();
Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId);
Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null);
Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId);
Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId, string langCode);
}
}

@ -17,6 +17,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<MovieRequests> UpdateMovieRequest(MovieRequests request);
Task<RequestEngineResult> ApproveMovie(MovieRequests request);
Task<RequestEngineResult> ApproveMovieById(int requestId);
Task<RequestEngineResult> DenyMovieById(int modelId);
Task<RequestEngineResult> DenyMovieById(int modelId, string denyReason);
}
}

@ -12,7 +12,7 @@ namespace Ombi.Core.Engine.Interfaces
Task RemoveTvRequest(int requestId);
Task<TvRequests> GetTvRequest(int requestId);
Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv);
Task<RequestEngineResult> DenyChildRequest(int requestId);
Task<RequestEngineResult> DenyChildRequest(int requestId, string reason);
Task<RequestsViewModel<TvRequests>> GetRequestsLite(int count, int position, OrderFilterModel type);
Task<IEnumerable<TvRequests>> SearchTvRequest(string search);
Task<TvRequests> UpdateTvRequest(TvRequests request);

@ -51,7 +51,7 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<RequestEngineResult> RequestMovie(MovieRequestViewModel model)
{
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(model.TheMovieDbId);
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(model.TheMovieDbId, model.LanguageCode);
if (movieInfo == null || movieInfo.Id == 0)
{
return new RequestEngineResult
@ -82,7 +82,8 @@ namespace Ombi.Core.Engine
RequestedDate = DateTime.UtcNow,
Approved = false,
RequestedUserId = userDetails.Id,
Background = movieInfo.BackdropPath
Background = movieInfo.BackdropPath,
LangCode = model.LanguageCode
};
var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
@ -305,7 +306,7 @@ namespace Ombi.Core.Engine
return await ApproveMovie(request);
}
public async Task<RequestEngineResult> DenyMovieById(int modelId)
public async Task<RequestEngineResult> DenyMovieById(int modelId, string denyReason)
{
var request = await MovieRepository.Find(modelId);
if (request == null)
@ -317,6 +318,7 @@ namespace Ombi.Core.Engine
}
request.Denied = true;
request.DeniedReason = denyReason;
// We are denying a request
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MovieRepository.Update(request);

@ -1,23 +1,22 @@
using System;
using AutoMapper;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Rule.Interfaces;
using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
namespace Ombi.Core.Engine
{
@ -36,14 +35,17 @@ namespace Ombi.Core.Engine
private IMapper Mapper { get; }
private ILogger<MovieSearchEngine> Logger { get; }
private const int MovieLimit = 10;
/// <summary>
/// Lookups the imdb information.
/// </summary>
/// <param name="theMovieDbId">The movie database identifier.</param>
/// <returns></returns>
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId)
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null)
{
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId);
langCode = await DefaultLanguageCode(langCode);
var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode);
var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo);
return await ProcessSingleMovie(viewMovie, true);
@ -54,13 +56,14 @@ namespace Ombi.Core.Engine
/// </summary>
/// <param name="search">The search.</param>
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search)
public async Task<IEnumerable<SearchMovieViewModel>> Search(string search, int? year, string langaugeCode)
{
var result = await MovieApi.SearchMovie(search);
langaugeCode = await DefaultLanguageCode(langaugeCode);
var result = await MovieApi.SearchMovie(search, year, langaugeCode);
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -70,13 +73,14 @@ namespace Ombi.Core.Engine
/// </summary>
/// <param name="theMovieDbId"></param>
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId)
public async Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId, string langCode)
{
var result = await MovieApi.SimilarMovies(theMovieDbId);
langCode = await DefaultLanguageCode(langCode);
var result = await MovieApi.SimilarMovies(theMovieDbId, langCode);
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -87,10 +91,15 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -101,10 +110,14 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -115,11 +128,15 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -130,10 +147,14 @@ namespace Ombi.Core.Engine
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12));
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
return await TransformMovieResultsToResponse(result.Take(MovieLimit)); // Take x to stop us overloading the API
}
return null;
}
@ -174,6 +195,10 @@ namespace Ombi.Core.Engine
{
// Check if this user requested it
var user = await GetUser();
if (user == null)
{
return;
}
var request = await RequestService.MovieRequestService.GetAll()
.AnyAsync(x => x.RequestedUserId.Equals(user.Id) && x.TheMovieDbId == viewModel.Id);
if (request)

@ -299,7 +299,7 @@ namespace Ombi.Core.Engine
return await ApproveAlbum(request);
}
public async Task<RequestEngineResult> DenyAlbumById(int modelId)
public async Task<RequestEngineResult> DenyAlbumById(int modelId, string reason)
{
var request = await MusicRepository.Find(modelId);
if (request == null)
@ -311,6 +311,7 @@ namespace Ombi.Core.Engine
}
request.Denied = true;
request.DeniedReason = reason;
// We are denying a request
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MusicRepository.Update(request);

@ -403,7 +403,7 @@ namespace Ombi.Core.Engine
};
}
public async Task<RequestEngineResult> DenyChildRequest(int requestId)
public async Task<RequestEngineResult> DenyChildRequest(int requestId, string reason)
{
var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId);
if (request == null)
@ -414,6 +414,7 @@ namespace Ombi.Core.Engine
};
}
request.Denied = true;
request.DeniedReason = reason;
await TvRepository.UpdateChild(request);
NotificationHelper.Notify(request, NotificationType.RequestDeclined);
return new RequestEngineResult

@ -29,5 +29,6 @@ namespace Ombi.Core.Models.Requests
public class MovieRequestViewModel
{
public int TheMovieDbId { get; set; }
public string LanguageCode { get; set; } = "en";
}
}

@ -11,11 +11,11 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.3" />
<PackageReference Include="Hangfire" Version="1.6.21" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" />
<PackageReference Include="MiniProfiler.AspNetCore" Version="4.0.0-alpha6-79" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
</ItemGroup>

@ -0,0 +1,97 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Ombi.Core.Models.Search;
using Ombi.Store.Entities;
using Ombi.Store.Repository.Requests;
namespace Ombi.Core.Rule.Rules.Search
{
public static class AvailabilityRuleHelper
{
public static void CheckForUnairedEpisodes(SearchTvShowViewModel search)
{
if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
{
search.FullyAvailable = true;
}
else
{
var airedButNotAvailable = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate <= DateTime.Now.Date && c.AirDate != DateTime.MinValue));
if (!airedButNotAvailable)
{
var unairedEpisodes = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate > DateTime.Now.Date));
if (unairedEpisodes)
{
search.FullyAvailable = true;
}
}
}
}
public static async Task SingleEpisodeCheck(bool useImdb, IQueryable<PlexEpisode> allEpisodes, EpisodeRequests episode,
SeasonRequests season, PlexServerContent item, bool useTheMovieDb, bool useTvDb)
{
PlexEpisode epExists = null;
if (useImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ImdbId == item.ImdbId.ToString());
}
if (useTheMovieDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TheMovieDbId == item.TheMovieDbId.ToString());
}
if (useTvDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TvDbId == item.TvDbId.ToString());
}
if (epExists != null)
{
episode.Available = true;
}
}
public static async Task SingleEpisodeCheck(bool useImdb, IQueryable<EmbyEpisode> allEpisodes, EpisodeRequests episode,
SeasonRequests season, EmbyContent item, bool useTheMovieDb, bool useTvDb)
{
EmbyEpisode epExists = null;
if (useImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ImdbId == item.ImdbId.ToString());
}
if (useTheMovieDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TheMovieDbId == item.TheMovieDbId.ToString());
}
if (useTvDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TvDbId == item.TvDbId.ToString());
}
if (epExists != null)
{
episode.Available = true;
}
}
}
}

@ -1,6 +1,4 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
@ -23,15 +21,27 @@ namespace Ombi.Core.Rule.Rules.Search
public async Task<RuleResult> Execute(SearchViewModel obj)
{
EmbyContent item = null;
var useImdb = false;
var useTheMovieDb = false;
var useTvDb = false;
if (obj.ImdbId.HasValue())
{
item = await EmbyContentRepository.GetByImdbId(obj.ImdbId);
if (item != null)
{
useImdb = true;
}
}
if (item == null)
{
if (obj.TheMovieDbId.HasValue())
{
item = await EmbyContentRepository.GetByTheMovieDbId(obj.TheMovieDbId);
if (item != null)
{
useTheMovieDb = true;
}
}
if (item == null)
@ -39,10 +49,14 @@ namespace Ombi.Core.Rule.Rules.Search
if (obj.TheTvDbId.HasValue())
{
item = await EmbyContentRepository.GetByTvDbId(obj.TheTvDbId);
if (item != null)
{
useTvDb = true;
}
}
}
}
if (item != null)
{
obj.Available = true;
@ -59,29 +73,12 @@ namespace Ombi.Core.Rule.Rules.Search
{
foreach (var episode in season.Episodes)
{
EmbyEpisode epExists = null;
if (item.HasImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber
&& e.ImdbId == item.ImdbId);
} if (item.HasTvDb && epExists == null)
{
epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber
&& e.Series.TvDbId == item.TvDbId);
} if (item.HasTheMovieDb && epExists == null)
{
epExists = await allEpisodes.FirstOrDefaultAsync(e => e.EpisodeNumber == episode.EpisodeNumber && e.SeasonNumber == season.SeasonNumber
&& e.TheMovieDbId == item.TheMovieDbId);
}
if (epExists != null)
{
episode.Available = true;
}
await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb);
}
}
}
AvailabilityRuleHelper.CheckForUnairedEpisodes(search);
}
}
return Success();

@ -1,7 +1,5 @@
using System;
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers;
@ -74,56 +72,17 @@ namespace Ombi.Core.Rule.Rules.Search
{
foreach (var episode in season.Episodes)
{
PlexEpisode epExists = null;
if (useImdb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.ImdbId == item.ImdbId.ToString());
}
if (useTheMovieDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TheMovieDbId == item.TheMovieDbId.ToString());
}
if (useTvDb)
{
epExists = await allEpisodes.FirstOrDefaultAsync(x =>
x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber &&
x.Series.TvDbId == item.TvDbId.ToString());
}
if (epExists != null)
{
episode.Available = true;
}
await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb);
}
}
if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available)))
{
search.FullyAvailable = true;
}
else
{
var airedButNotAvailable = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate <= DateTime.Now.Date));
if (!airedButNotAvailable)
{
var unairedEpisodes = search.SeasonRequests.Any(x =>
x.Episodes.Any(c => !c.Available && c.AirDate > DateTime.Now.Date));
if (unairedEpisodes)
{
search.FullyAvailable = true;
}
}
}
AvailabilityRuleHelper.CheckForUnairedEpisodes(search);
}
}
}
return Success();
}
}
}

@ -348,16 +348,16 @@ namespace Ombi.Core.Senders
if (!existingSeason.monitored)
{
// We need to monitor it, problem being is all episodes will now be monitored
// So we need to monior the series but unmonitor every episode
// Except the episodes that are already monitored before we update the series (we do not want to unmonitor episodes that are monitored beforehand)
// So we need to monitor the series but unmonitor every episode
// Except the episodes that are already monitored before we update the series (we do not want to unmonitored episodes that are monitored beforehand)
existingSeason.monitored = true;
var sea = result.seasons.FirstOrDefault(x => x.seasonNumber == existingSeason.seasonNumber);
sea.monitored = true;
//var previouslyMonitoredEpisodes = sonarrEpList.Where(x =>
// x.seasonNumber == existingSeason.seasonNumber && x.monitored).Select(x => x.episodeNumber).ToList(); // We probably don't actually care about this
result = await SonarrApi.UpdateSeries(result, s.ApiKey, s.FullUri);
var epToUnmonitor = new List<Episode>();
var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the orignal member
var epToUnmonitored = new List<Episode>();
var newEpList = sonarrEpList.ConvertAll(ep => new Episode(ep)); // Clone it so we don't modify the original member
foreach (var ep in newEpList.Where(x => x.seasonNumber == existingSeason.seasonNumber).ToList())
{
//if (previouslyMonitoredEpisodes.Contains(ep.episodeNumber))
@ -366,10 +366,10 @@ namespace Ombi.Core.Senders
// continue;
//}
ep.monitored = false;
epToUnmonitor.Add(ep);
epToUnmonitored.Add(ep);
}
foreach (var epToUpdate in epToUnmonitor)
foreach (var epToUpdate in epToUnmonitored)
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}

@ -200,6 +200,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILidarrAvailabilityChecker, LidarrAvailabilityChecker>();
services.AddTransient<IIssuesPurge, IssuesPurge>();
services.AddTransient<IResendFailedRequests, ResendFailedRequests>();
services.AddTransient<IMediaDatabaseRefresh, MediaDatabaseRefresh>();
}
}
}

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nunit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Helpers\Ombi.Helpers.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,52 @@
using System;
using NUnit.Framework;
using System.Collections.Generic;
namespace Ombi.Helpers.Tests
{
[TestFixture]
public class PlexHelperTests
{
[TestCaseSource(nameof(ProviderIdGuidData))]
public string GetProviderIdFromPlexGuidTests(string guidInput, ProviderIdType type)
{
var result = PlexHelper.GetProviderIdFromPlexGuid(guidInput);
switch (type)
{
case ProviderIdType.Imdb:
Assert.That(result.ImdbId, Is.Not.Null);
return result.ImdbId;
case ProviderIdType.TvDb:
Assert.That(result.TheTvDb, Is.Not.Null);
return result.TheTvDb;
case ProviderIdType.MovieDb:
Assert.That(result.TheMovieDb, Is.Not.Null);
return result.TheMovieDb;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public static IEnumerable<TestCaseData> ProviderIdGuidData
{
get
{
yield return new TestCaseData("com.plexapp.agents.thetvdb://269586/2/8?lang=en", ProviderIdType.TvDb).Returns("269586").SetName("Regular TvDb Id");
yield return new TestCaseData("com.plexapp.agents.themoviedb://390043?lang=en", ProviderIdType.MovieDb).Returns("390043").SetName("Regular MovieDb Id");
yield return new TestCaseData("com.plexapp.agents.imdb://tt2543164?lang=en", ProviderIdType.Imdb).Returns("tt2543164").SetName("Regular Imdb Id");
yield return new TestCaseData("com.plexapp.agents.agent47://tt2543456?lang=en", ProviderIdType.Imdb).Returns("tt2543456").SetName("Unknown IMDB agent");
yield return new TestCaseData("com.plexapp.agents.agent47://456822/1/1?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent");
yield return new TestCaseData("com.plexapp.agents.agent47://456822/999/999?lang=en", ProviderIdType.TvDb).Returns("456822").SetName("Unknown TvDb agent, large episode and season");
}
}
public enum ProviderIdType
{
Imdb,
TvDb,
MovieDb
}
}
}

@ -21,6 +21,7 @@ namespace Ombi.Helpers
public static EventId PlexContentCacher => new EventId(2008);
public static EventId SickRageCacher => new EventId(2009);
public static EventId LidarrArtistCache => new EventId(2010);
public static EventId MediaReferesh => new EventId(2011);
public static EventId MovieSender => new EventId(3000);

@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.0.0-pre-05" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
</ItemGroup>

@ -27,12 +27,15 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Ombi.Helpers
{
public class PlexHelper
{
private const string ImdbMatchExpression = "tt([0-9]{1,10})";
private const string TvDbIdMatchExpression = "//[0-9]+/([0-9]{1,3})/([0-9]{1,3})";
public static ProviderId GetProviderIdFromPlexGuid(string guid)
{
//com.plexapp.agents.thetvdb://269586/2/8?lang=en
@ -52,7 +55,7 @@ namespace Ombi.Helpers
{
TheTvDb = guidSplit[1]
};
}
} else
if (guid.Contains("themoviedb", CompareOptions.IgnoreCase))
{
return new ProviderId
@ -60,6 +63,7 @@ namespace Ombi.Helpers
TheMovieDb = guidSplit[1]
};
}
else
if (guid.Contains("imdb", CompareOptions.IgnoreCase))
{
return new ProviderId
@ -67,6 +71,31 @@ namespace Ombi.Helpers
ImdbId = guidSplit[1]
};
}
else
{
var imdbRegex = new Regex(ImdbMatchExpression, RegexOptions.Compiled);
var tvdbRegex = new Regex(TvDbIdMatchExpression, RegexOptions.Compiled);
var imdbMatch = imdbRegex.IsMatch(guid);
if (imdbMatch)
{
return new ProviderId
{
ImdbId = guidSplit[1]
};
}
else
{
// Check if it matches the TvDb pattern
var tvdbMatch = tvdbRegex.IsMatch(guid);
if (tvdbMatch)
{
return new ProviderId
{
TheTvDb = guidSplit[1]
};
}
}
}
}
return new ProviderId();
}

@ -1,10 +1,17 @@
<!doctype html>
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Ombi</title>
<style type="text/css">
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
@ -12,6 +19,7 @@
}
body {
background-color: #1f1f1f;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
@ -20,6 +28,8 @@
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
color: #FFF;
text-align: center;
}
table {
@ -31,39 +41,54 @@
table td {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
color: #FFF;
background-color: #1f1f1f;
width: 100%;
text-align: center;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
margin: 0 auto !important;
max-width: 1042px;
Margin: 0 auto !important;
/* makes it centered */
max-width: 1036px;
padding: 10px;
width: 1042px;
width: 1036px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
Margin: 0 auto;
max-width: 1037px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
color: #FFF;
background: #1f1f1f;
border-radius: 3px;
width: 100%;
text-align: center;
}
.wrapper {
box-sizing: border-box;
padding: 5px;
overflow: auto;
padding: 20px;
}
.content-block {
@ -71,58 +96,241 @@
padding-top: 10px;
}
.media-card {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
vertical-align: top;
padding: 3px;
width: 500px;
min-width: 500px;
max-width: 500px;
height: 252px;
max-height: 252px;
min-height: 252px;
}
.card-bg {
background-image: url({0});
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 500px;
background-color: #1f1f1f;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-clip: padding-box;
border: 2px solid rgba(255, 118, 27, .4);
height: 252px;
max-height: 252px;
}
.bg-tint {
background-color: rgba(0, 0, 0, .6);
}
.poster-container {
vertical-align: top;
width: 150px;
min-width: 150px;
height: 225px;
max-height: 225px;
min-height: 225px;
}
.poster-img {}
.poster-overlay {}
.movie-info {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
vertical-align: top;
padding-left: 4px;
text-align: left;
height: 227px;
}
.title h1 {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-size: 22px;
line-height: 24px;
vertical-align: top;
max-width: 320px;
white-space: normal;
display: block;
height: 50px;
min-height: 50px;
max-height: 50px;
}
.description {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
height: 130px;
max-height: 130px;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
display: block;
font-size: 14px !important;
text-align: justify;
}
.meta {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
max-width: 300px;
min-width: 300px;
padding: 3px 7px;
margin-top: 10px;
font-size: 14px !important;
line-height: 1;
text-align: left;
white-space: nowrap;
vertical-align: middle;
background-color: rgba(255, 118, 27, 0.5);
color: #fff;
border-radius: 2px;
overflow: hidden;
display: block;
}
.footer {
clear: both;
margin-top: 10px;
Margin-top: 10px;
text-align: center;
width: 100%;
font-size: 12px;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #fff;
color: #999999;
font-size: 12px;
text-align: center;
}
h1 {
color: #ffffff;
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #ff761b;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-weight: 400;
margin: 0;
color: #ff761b;
font-size: 22px;
line-height: 24px;
margin: 0 auto;
text-transform: capitalize;
}
p {
p,
ul,
ol {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
font-weight: 400;
font-weight: normal;
margin: 0;
}
p li,
ul li {
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #ff761b !important;
text-decoration: none;
font-weight: 400;
}
@media only screen and (max-width: 1040px) {
.media-card {
display: block !important;
margin-top: 0 !important;
margin-right: auto !important;
margin-bottom: 10px !important;
margin-left: auto !important;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 1059px) {
table[class=body] h1 {
font-size: 28px !important;
font-size: 22px;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 12px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
@ -135,9 +343,52 @@
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
.media-card {
display: flex !important;
margin-top: 0 !important;
margin-right: auto !important;
margin-bottom: 10px !important;
margin-left: auto !important;
height: 100% !important;
width: 100% !important;
width: 500px !important;
max-width: 500px !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
@ -146,66 +397,78 @@
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;font-size: 14px;line-height: 1.4;margin: 0;padding: 0;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate;mso-table-lspace: 0pt;mso-table-rspace: 0pt;width: 100%; background-color: #1f1f1f; color: #fff;">
<tr>
<td class="container" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 14px;vertical-align: top;display: block;max-width: 1042px;padding: 10px;width: 1042px;margin: 0 auto !important;">
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 1037px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Ombi Recently Added</span>
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; max-width: 1042px; width: 100%;">
<tr>
<td align="center">
<img src="{@LOGO}" width="400px" text-align="center"/>
</td>
</tr>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
<br />
<br />
<p style="color: #fff; font-family: sans-serif; font-size: 20px; font-weight: normal; margin: 0; Margin-bottom: 15px; text-align: center;">{@INTRO}</p>
<body class="" style="background-color: #1f1f1f; font-family: 'Open Sans', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; color: #FFF; text-align: center;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" width="100%" bgcolor="#1f1f1f" align="center" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #FFF; background-color: #1f1f1f; width: 100%; text-align: center;">
<tbody>
<tr>
<td class="container" width="1036" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top; max-width: 1036px; padding: 10px; width: 1036px; Margin: 0 auto;">
<div class="content" style="box-sizing: border-box; Margin: 0 auto; max-width: 1037px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Ombi recently added</span>
<table role="presentation" class="main" width="100%" align="center" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #FFF; background: #1f1f1f; border-radius: 3px; width: 100%; text-align: center;">
<!-- START MAIN CONTENT AREA -->
<tbody>
<tr>
<td class="wrapper" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tbody>
<tr>
<td valign="top" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top;">
<img src="{@LOGO}" style="border: none; -ms-interpolation-mode: bicubic; max-width: 100%;">
</td>
</tr>
<tr>
<td valign="top" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top;">
<p style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: normal; margin: 0; margin-bottom: 15px;">{@INTRO}</p>
</td>
</tr>
<tr>
<td valign="top" style="font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top;">
{@RECENTLYADDED}
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</tbody>
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tbody>
<tr>
<td class="content-block powered-by" valign="top" align="center" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;">
Powered by <a href="https://github.com/tidusjar/Ombi" style="font-weight: 400; font-size: 12px; text-align: center; text-decoration: none; color: #ff761b;">Ombi</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
{@RECENTLYADDED}
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
Powered by <a href="https://github.com/tidusjar/Ombi" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Ombi</a>
</td>
</tr>
</table>
</tbody>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">&nbsp;</td>
</tr>
</td>
</tr>
</tbody>
</table>
</body>
</html>
</html>
</html>

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0"></packagereference>
<PackageReference Include="Moq" Version="4.10.0" />
</ItemGroup>

@ -83,22 +83,20 @@ namespace Ombi.Notifications
{
client.Authenticate(settings.Username, settings.Password);
}
//Log.Info("sending message to {0} \r\n from: {1}\r\n Are we authenticated: {2}", message.To, message.From, client.IsAuthenticated);
_log.LogDebug("sending message to {0} \r\n from: {1}\r\n Are we authenticated: {2}", message.To, message.From, client.IsAuthenticated);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
catch (Exception e)
{
//Log.Error(e);
throw new InvalidOperationException(e.Message);
_log.LogError(e, "Exception when attempting to send an email");
throw;
}
}
public async Task Send(NotificationMessage model, EmailNotificationSettings settings)
{
try
{
EnsureArg.IsNotNullOrEmpty(settings.SenderAddress);
EnsureArg.IsNotNullOrEmpty(model.To);
EnsureArg.IsNotNullOrEmpty(model.Message);
@ -120,9 +118,18 @@ namespace Ombi.Notifications
Subject = model.Subject
};
message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress));
message.To.Add(new MailboxAddress(model.To, model.To));
await Send(message, settings);
}
public async Task Send(MimeMessage message, EmailNotificationSettings settings)
{
try
{
message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress));
using (var client = new SmtpClient())
{
if (settings.DisableCertificateChecking)

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using MimeKit;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models.Notifications;
@ -8,5 +9,6 @@ namespace Ombi.Notifications
{
Task Send(NotificationMessage model, EmailNotificationSettings settings);
Task SendAdHoc(NotificationMessage model, EmailNotificationSettings settings);
Task Send(MimeMessage message, EmailNotificationSettings settings);
}
}

@ -17,10 +17,6 @@ namespace Ombi.Notifications
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
if (pref != null)
{
UserPreference = pref.Value;
}
string title;
if (req == null)
@ -41,6 +37,10 @@ namespace Ombi.Notifications
}
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
@ -49,7 +49,8 @@ namespace Ombi.Notifications
}
Overview = req?.Overview;
Year = req?.ReleaseDate.Year.ToString();
DenyReason = req?.DeniedReason;
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
if (req?.RequestType == RequestType.Movie)
{
PosterImage = string.Format((req?.PosterPath ?? string.Empty).StartsWith("/", StringComparison.InvariantCultureIgnoreCase)
@ -66,10 +67,7 @@ namespace Ombi.Notifications
public void Setup(NotificationOptions opts, AlbumRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
if (pref != null)
{
UserPreference = pref.Enabled ? pref.Value : string.Empty;
}
string title;
if (req == null)
{
@ -88,7 +86,13 @@ namespace Ombi.Notifications
UserName = req?.RequestedUser?.UserName;
}
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
DenyReason = req?.DeniedReason;
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
@ -101,22 +105,15 @@ namespace Ombi.Notifications
AdditionalInformation = opts?.AdditionalInformation ?? string.Empty;
}
public void SetupNewsletter(CustomizationSettings s, OmbiUser username)
public void SetupNewsletter(CustomizationSettings s)
{
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = username.UserName;
UserName = username.UserName;
Alias = username.Alias.HasValue() ? username.Alias : username.UserName;
}
public void Setup(NotificationOptions opts, ChildRequests req, CustomizationSettings s, UserNotificationPreferences pref)
{
LoadIssues(opts);
if (pref != null)
{
UserPreference = pref.Enabled ? pref.Value : string.Empty;
}
string title;
if (req == null)
{
@ -126,6 +123,7 @@ namespace Ombi.Notifications
{
title = req?.ParentRequest.Title;
}
DenyReason = req?.DeniedReason;
ApplicationUrl = (s?.ApplicationUrl.HasValue() ?? false) ? s.ApplicationUrl : string.Empty;
ApplicationName = string.IsNullOrEmpty(s?.ApplicationName) ? "Ombi" : s?.ApplicationName;
RequestedUser = req?.RequestedUser?.UserName;
@ -134,7 +132,12 @@ namespace Ombi.Notifications
// Can be set if it's an issue
UserName = req?.RequestedUser?.UserName;
}
AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty;
Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName;
if (pref != null)
{
UserPreference = pref.Value.HasValue() ? pref.Value : Alias;
}
Title = title;
RequestedDate = req?.RequestedDate.ToString("D");
if (Type.IsNullOrEmpty())
@ -217,7 +220,6 @@ namespace Ombi.Notifications
public string UserName { get; set; }
public string IssueUser => UserName;
public string Alias { get; set; }
public string Title { get; set; }
public string RequestedDate { get; set; }
public string Type { get; set; }
@ -235,6 +237,8 @@ namespace Ombi.Notifications
public string IssueSubject { get; set; }
public string NewIssueComment { get; set; }
public string UserPreference { get; set; }
public string DenyReason { get; set; }
public string AvailableDate { get; set; }
// System Defined
private string LongDate => DateTime.Now.ToString("D");
@ -269,6 +273,8 @@ namespace Ombi.Notifications
{nameof(UserName),UserName},
{nameof(Alias),Alias},
{nameof(UserPreference),UserPreference},
{nameof(DenyReason),DenyReason},
{nameof(AvailableDate),AvailableDate},
};
}
}

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0"></packagereference>
</ItemGroup>
<ItemGroup>

@ -7,6 +7,7 @@ using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Schedule.Jobs.Radarr;
using Ombi.Schedule.Jobs.SickRage;
using Ombi.Schedule.Jobs.Sonarr;
@ -21,7 +22,7 @@ namespace Ombi.Schedule
IEmbyUserImporter embyUserImporter, ISonarrSync cache, ICouchPotatoSync cpCache,
ISettingsService<JobSettings> jobsettings, ISickRageSync srSync, IRefreshMetadata refresh,
INewsletterJob newsletter, IPlexRecentlyAddedSync recentlyAddedPlex, ILidarrArtistSync artist,
IIssuesPurge purge, IResendFailedRequests resender)
IIssuesPurge purge, IResendFailedRequests resender, IMediaDatabaseRefresh dbRefresh)
{
_plexContentSync = plexContentSync;
_radarrSync = radarrSync;
@ -39,6 +40,7 @@ namespace Ombi.Schedule
_lidarrArtistSync = artist;
_issuesPurge = purge;
_resender = resender;
_mediaDatabaseRefresh = dbRefresh;
}
private readonly IPlexContentSync _plexContentSync;
@ -57,6 +59,7 @@ namespace Ombi.Schedule
private readonly ILidarrArtistSync _lidarrArtistSync;
private readonly IIssuesPurge _issuesPurge;
private readonly IResendFailedRequests _resender;
private readonly IMediaDatabaseRefresh _mediaDatabaseRefresh;
public void Setup()
{
@ -80,9 +83,9 @@ namespace Ombi.Schedule
RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s));
RecurringJob.AddOrUpdate(() => _newsletter.Start(), JobSettingsHelper.Newsletter(s));
RecurringJob.AddOrUpdate(() => _resender.Start(), JobSettingsHelper.ResendFailedRequests(s));
RecurringJob.AddOrUpdate(() => _mediaDatabaseRefresh.Start(), JobSettingsHelper.MediaDatabaseRefresh(s));
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{

@ -83,7 +83,7 @@ namespace Ombi.Schedule.Jobs.Emby
{
processed++;
if (ep.LocationType.Equals("Virtual", StringComparison.InvariantCultureIgnoreCase))
if (ep.LocationType?.Equals("Virtual", StringComparison.InvariantCultureIgnoreCase) ?? false)
{
// For some reason Emby is not respecting the `IsVirtualItem` field.
continue;
@ -154,4 +154,4 @@ namespace Ombi.Schedule.Jobs.Emby
GC.SuppressFinalize(this);
}
}
}
}

@ -7,18 +7,18 @@ namespace Ombi.Schedule.Jobs.Ombi
{
protected virtual void AddBackgroundInsideTable(StringBuilder sb, string url)
{
sb.Append("<td align=\"center\" valign=\"top\" class=\"media-card\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 12px; vertical-align: top; padding: 3px; width: 502px; min-width: 500px; max-width: 500px; height: 235px; \">");
sb.AppendFormat("<table class=\"card-bg\" style=\"background-image: url({0}); border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 500px; background-color: #1f1f1f; background-position: center; background-size: cover; background-repeat: no-repeat; background-clip: padding-box; border: 2px solid rgba(255,118,27,.4); height: 248px; max-height: 500px; \">", url);
sb.Append("<td align=\"center\" valign=\"top\" width=\"500\" height=\"252\" class=\"media-card\" style=\"font-size: 14px; font-family: 'Open Sans', Helvetica, Arial, sans-serif; vertical-align: top; padding: 3px; width: 500px; min-width: 500px; max-width: 500px; height: 252px; max-height: 252px; \">");
sb.AppendFormat("<table class=\"card-bg\" width=\"500\" height=\"252\" background=\"url(0)\" bgcolor=\"#1f1f1f\" style=\"background-image: url(0); border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 500px; background-color: #1f1f1f; background-position: center; background-size: cover; background-repeat: no-repeat; background-clip: padding-box; border: 2px solid rgba(255, 118, 27, .4); height: 252px; max-height: 252px; \">", url);
sb.Append("<tr>");
sb.Append("<td>");
sb.Append("<table class=\"bg-tint\" style=\"background-color: rgba(0, 0, 0, .6); position: absolute; width: 490px; height: 239px; \">");
sb.Append("<td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top;\" valign=\"top\">");
sb.Append("<table class=\"bg-tint\" width=\"100%\" bgcolor=\"rgba(0, 0, 0, .6)\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: rgba(0, 0, 0, .6); \">");
}
protected virtual void AddPosterInsideTable(StringBuilder sb, string url)
{
sb.Append("<tr>");
sb.Append("<td class=\"poster-container\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; width: 150px; min-width: 15px; height: 225px; \">");
sb.AppendFormat("<table class=\"poster-img\" style=\"background-image: url({0}); border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: transparent; background-position: center; background-size: cover; background-repeat: no-repeat; background-clip: padding-box; border: 1px solid rgba(255,255,255,.1); \">", url);
sb.Append("<td class=\"poster-container\" width=\"150\" height=\"225\" valign=\"top\" style=\"ont-family: sans-serif; font-size: 14px; vertical-align: top; width: 150px; min-width: 150px; height: 225px; max-height: 225px; min-height: 225px; \">");
sb.AppendFormat("<table class=\"poster-img\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">", url);
}
protected virtual void AddMediaServerUrl(StringBuilder sb, string mediaurl, string url)
@ -27,10 +27,10 @@ namespace Ombi.Schedule.Jobs.Ombi
{
sb.Append("<tr>");
sb.Append(
"<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
"<td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", mediaurl);
sb.AppendFormat(
"<img class=\"poster-overlay\" src=\"{0}\" width=\"150\" height=\"225\" style=\"border: none;-ms-interpolation-mode: bicubic; max-width: 100%;display: block; visibility: hidden; \">",
"<img class=\"poster-overlay\" src=\"{0}\" width=\"150\" height=\"225\" style=\"border: none; -ms-interpolation-mode: bicubic; max-width: 100%; \">",
url);
sb.Append("</a>");
sb.Append("</td>");
@ -44,16 +44,16 @@ namespace Ombi.Schedule.Jobs.Ombi
protected virtual void AddInfoTable(StringBuilder sb)
{
sb.Append(
"<td class=\"movie-info\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; padding-left: 4px; text-align: left; height: 227px; \">");
"<td class=\"movie-info\" height=\"227\" valign=\"top\" align=\"left\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; padding-left: 4px; text-align: left; height: 227px; \">");
sb.Append("<table style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; height: 100%; \">");
}
protected virtual void AddTitle(StringBuilder sb, string url, string title)
{
sb.Append("<tr>");
sb.Append("<td class=\"title\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.9rem; vertical-align: top; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; line-height: 1.2rem; padding: 5px; \">");
if(url.HasValue()) sb.AppendFormat("<a href=\"{0}\" target=\"_blank\">", url);
sb.AppendFormat("<h1 style=\"white-space: normal; line-height: 1;\" >{0}</h1>", title);
sb.Append("<tr class=\"title\" valign=\"top\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 22px;line-height: 24px;vertical-align: top;max-width: 320px;display: block;height: 50px;min-height: 50px;max-height: 50px; \">");
sb.Append("<td>");
if(url.HasValue()) sb.AppendFormat("<a href=\"{0}\" target=\"_blank\" style=\"text-decoration: none; font-weight: 400; color: #ff761b;\">", url);
sb.AppendFormat("<h1 style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif;font-size: 22px;line-height: 24px;vertical-align: top;max-width: 320px;display: block;height: 50px;min-height: 50px;max-height: 50px;\" >{0}</h1>", title);
if (url.HasValue()) sb.Append("</a>");
sb.Append("</td>");
sb.Append("</tr>");
@ -61,30 +61,30 @@ namespace Ombi.Schedule.Jobs.Ombi
protected virtual void AddParagraph(StringBuilder sb, string text)
{
sb.Append("<tr class=\"description\">");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.75rem; vertical-align: top; padding: 5px; height: 100%; \">");
sb.AppendFormat("<p style=\"color: #fff; font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: 400; margin: 0; max-width: 325px; text-align: justify; \">{0}</p>", text);
sb.Append("<tr class=\"description\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif;height: 130px;max-height: 130px;max-width: 320px;overflow: hidden;text-overflow: ellipsis;display: block;font-size: 14px !important;text-align: justify;\" valign=\"top\">");
sb.Append("<td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<p style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: normal; margin: 0; margin-bottom: 15px; \">{0}</p>", text);
sb.Append("</td>");
sb.Append("</tr>");
}
protected virtual void AddTvParagraph(StringBuilder sb, string episodes, string summary)
{
sb.Append("<tr class=\"description\">");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 0.75rem; vertical-align: top; padding: 5px; height: 100%; \">");
sb.AppendFormat("<p style=\"color: #fff; font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: 400; margin: 0; max-width: 325px; margin-bottom: 10px; \">{0}</p>", episodes);
sb.AppendFormat("<div style=\"color: #fff; font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: 400; margin: 0; max-width: 325px; overflow: hidden; text-align: justify; \">{0}</div>", summary);
sb.Append("<tr class=\"description\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif;height: 130px;max-height: 130px;max-width: 320px;overflow: hidden;text-overflow: ellipsis;display: block;font-size: 14px !important;text-align: justify;\" valign=\"top\">");
sb.Append("<td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<p style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-weight: normal; margin: 0; margin-bottom: 15px; \">{0}</p>", episodes);
sb.AppendFormat("<div>{0}</div>", summary);
sb.Append("</td>");
sb.Append("</tr>");
}
protected virtual void AddGenres(StringBuilder sb, string text)
{
sb.Append("<tr class=\"meta\">");
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; max-width: 265px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; \">");
sb.AppendFormat("<span style=\"display: inline-block; min-width: 10px; padding: 3px 7px; font-size: 11px; line-height: 1; text-align: center; white-space: nowrap; vertical-align: middle; background-color: rgba(255, 118, 27, 0.5); color: #fff; border-radius: 2px; text-overflow: ellipsis; overflow: hidden; \">{0}</span>", text);
sb.Append("<tr class=\"meta\" style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; max-width: 300px; min-width: 300px; padding: 3px 7px; margin-top: 10px; line-height: 1; text-align: left; white-space: nowrap; vertical-align: middle; background-color: rgba(255, 118, 27, 0.5); color: #fff; border-radius: 2px; overflow: hidden; display: block; font-size: 0.9rem;\" align=\"left\" valign=\"middle\" bgcolor=\"rgba(255, 118, 27, 0.5)\">");
sb.Append("<td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top; \">");
sb.AppendFormat("<span>{0}</span>", text);
sb.Append("</td>");
sb.Append("</tr>");
}
}
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Plex.Interfaces
{
public interface IMediaDatabaseRefresh : IBaseJob
{
Task Start();
}
}

@ -0,0 +1,120 @@
using System;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Store.Repository;
namespace Ombi.Schedule.Jobs.Plex
{
public class MediaDatabaseRefresh : IMediaDatabaseRefresh
{
public MediaDatabaseRefresh(ISettingsService<PlexSettings> s, ILogger<MediaDatabaseRefresh> log, IPlexApi plexApi,
IPlexContentRepository plexRepo, IPlexContentSync c, IEmbyContentRepository embyRepo, IEmbyContentSync embySync)
{
_settings = s;
_log = log;
_api = plexApi;
_plexRepo = plexRepo;
_plexContentSync = c;
_embyRepo = embyRepo;
_embyContentSync = embySync;
_settings.ClearCache();
}
private readonly ISettingsService<PlexSettings> _settings;
private readonly ILogger _log;
private readonly IPlexApi _api;
private readonly IPlexContentRepository _plexRepo;
private readonly IPlexContentSync _plexContentSync;
private readonly IEmbyContentRepository _embyRepo;
private readonly IEmbyContentSync _embyContentSync;
public async Task Start()
{
try
{
await RemovePlexData();
await RemoveEmbyData();
}
catch (Exception e)
{
_log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Media Data Failed");
}
}
private async Task RemoveEmbyData()
{
try
{
var s = await _settings.GetSettingsAsync();
if (!s.Enable)
{
return;
}
const string episodeSQL = "DELETE FROM EmbyEpisode";
const string mainSql = "DELETE FROM EmbyContent";
await _embyRepo.ExecuteSql(episodeSQL);
await _embyRepo.ExecuteSql(mainSql);
BackgroundJob.Enqueue(() => _embyContentSync.Start());
}
catch (Exception e)
{
_log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Emby Data Failed");
}
}
private async Task RemovePlexData()
{
try
{
var s = await _settings.GetSettingsAsync();
if (!s.Enable)
{
return;
}
const string episodeSQL = "DELETE FROM PlexEpisode";
const string seasonsSql = "DELETE FROM PlexSeasonsContent";
const string mainSql = "DELETE FROM PlexServerContent";
await _plexRepo.ExecuteSql(episodeSQL);
await _plexRepo.ExecuteSql(seasonsSql);
await _plexRepo.ExecuteSql(mainSql);
}
catch (Exception e)
{
_log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Plex Data Failed");
}
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_plexRepo?.Dispose();
_settings?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -9,6 +9,7 @@ using MailKit;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MimeKit;
using Ombi.Api.Lidarr;
using Ombi.Api.Lidarr.Models;
using Ombi.Api.TheMovieDb;
@ -24,6 +25,7 @@ using Ombi.Settings.Settings.Models.External;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
using ContentType = Ombi.Store.Entities.ContentType;
namespace Ombi.Schedule.Jobs.Ombi
{
@ -33,7 +35,8 @@ namespace Ombi.Schedule.Jobs.Ombi
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log,
ILidarrApi lidarrApi, IRepository<LidarrAlbumCache> albumCache, ISettingsService<LidarrSettings> lidarrSettings)
ILidarrApi lidarrApi, IRepository<LidarrAlbumCache> albumCache, ISettingsService<LidarrSettings> lidarrSettings,
ISettingsService<OmbiSettings> ombiSettings)
{
_plex = plex;
_emby = emby;
@ -53,6 +56,8 @@ namespace Ombi.Schedule.Jobs.Ombi
_lidarrApi = lidarrApi;
_lidarrAlbumRepository = albumCache;
_lidarrSettings = lidarrSettings;
_ombiSettings = ombiSettings;
_ombiSettings.ClearCache();
_lidarrSettings.ClearCache();
}
@ -66,6 +71,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly INotificationTemplatesRepository _templateRepo;
private readonly ISettingsService<EmailNotificationSettings> _emailSettings;
private readonly ISettingsService<NewsletterSettings> _newsletterSettings;
private readonly ISettingsService<OmbiSettings> _ombiSettings;
private readonly UserManager<OmbiUser> _userManager;
private readonly ILogger _log;
private readonly ILidarrApi _lidarrApi;
@ -162,7 +168,23 @@ namespace Ombi.Schedule.Jobs.Ombi
Email = emails
});
}
var emailTasks = new List<Task>();
var messageContent = ParseTemplate(template, customization);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
var bodyBuilder = new BodyBuilder
{
HtmlBody = html,
};
var message = new MimeMessage
{
Body = bodyBuilder.ToMessageBody(),
Subject = messageContent.Subject
};
foreach (var user in users)
{
// Get the users to send it to
@ -170,17 +192,13 @@ namespace Ombi.Schedule.Jobs.Ombi
{
continue;
}
var messageContent = ParseTemplate(template, customization, user);
var email = new NewsletterTemplate();
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
emailTasks.Add(_email.Send(
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = user.Email },
emailSettings));
// BCC the messages
message.Bcc.Add(new MailboxAddress(user.Email, user.Email));
}
// Send the email
await _email.Send(message, emailSettings);
// Now add all of this to the Recently Added log
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
foreach (var p in plexContentMoviesToSend)
@ -234,7 +252,6 @@ namespace Ombi.Schedule.Jobs.Ombi
});
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
await Task.WhenAll(emailTasks.ToArray());
}
else
{
@ -245,7 +262,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{
continue;
}
var messageContent = ParseTemplate(template, customization, a);
var messageContent = ParseTemplate(template, customization);
var email = new NewsletterTemplate();
@ -305,12 +322,12 @@ namespace Ombi.Schedule.Jobs.Ombi
return itemsToReturn;
}
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings, OmbiUser username)
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings)
{
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.SetupNewsletter(settings, username);
curlys.SetupNewsletter(settings);
return resolver.ParseMessage(template, curlys);
}
@ -318,6 +335,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend,
HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings)
{
var ombiSettings = await _ombiSettings.GetSettingsAsync();
var sb = new StringBuilder();
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
@ -331,8 +349,8 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("<td style=\"font-family: 'Open Sans', Helvetica, Arial, sans-serif; font-size: 14px; vertical-align: top; \">");
sb.Append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; \">");
sb.Append("<tr>");
await ProcessPlexMovies(plexMovies, sb);
await ProcessEmbyMovies(embyMovies, sb);
await ProcessPlexMovies(plexMovies, sb, ombiSettings.DefaultLanguageCode);
await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode);
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
@ -379,7 +397,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return sb.ToString();
}
private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb)
private async Task ProcessPlexMovies(IQueryable<PlexServerContent> plexContentToSend, StringBuilder sb, string defaultLanguageCode)
{
int count = 0;
var ordered = plexContentToSend.OrderByDescending(x => x.AddedAt);
@ -390,7 +408,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{
continue;
}
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId);
var info = await _movieApi.GetMovieInformationWithExtraInfo(movieDbId, defaultLanguageCode);
var mediaurl = content.Url;
if (info == null)
{
@ -453,7 +471,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb)
private async Task ProcessEmbyMovies(IQueryable<EmbyContent> embyContent, StringBuilder sb, string defaultLangaugeCode)
{
int count = 0;
var ordered = embyContent.OrderByDescending(x => x.AddedAt);
@ -474,7 +492,7 @@ namespace Ombi.Schedule.Jobs.Ombi
}
var mediaurl = content.Url;
var info = await _movieApi.GetMovieInformationWithExtraInfo(StringHelper.IntParseLinq(theMovieDbId));
var info = await _movieApi.GetMovieInformationWithExtraInfo(StringHelper.IntParseLinq(theMovieDbId), defaultLangaugeCode);
if (info == null)
{
continue;

@ -10,13 +10,13 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.19" />
<PackageReference Include="Hangfire" Version="1.6.21" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.6.21" />
<PackageReference Include="Hangfire.Console" Version="1.3.10" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.RecurringJobExtensions" Version="1.1.6" />
<PackageReference Include="Hangfire.SQLite" Version="1.4.2" />
<PackageReference Include="Serilog" Version="2.6.0-dev-00892" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="SharpCompress" Version="0.18.2" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.6.13" />

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,9 @@
namespace Ombi.Settings.Settings.Models
{
public class CustomPageSettings : Settings
{
public string Title { get; set; }
public string Html { get; set; }
public string FontAwesomeIcon { get; set; }
}
}

@ -10,6 +10,7 @@
public string CustomDonationMessage { get; set; }
public string Logo { get; set; }
public bool RecentlyAddedPage { get; set; }
public bool UseCustomPage { get; set; }
public void AddToUrl(string part)
{

@ -16,5 +16,6 @@
public string LidarrArtistSync { get; set; }
public string IssuesPurge { get; set; }
public string RetryRequests { get; set; }
public string MediaDatabaseRefresh { get; set; }
}
}

@ -65,7 +65,10 @@ namespace Ombi.Settings.Settings.Models
{
return Get(s.RetryRequests, Cron.Daily(6));
}
public static string MediaDatabaseRefresh(JobSettings s)
{
return Get(s.MediaDatabaseRefresh, Cron.DayInterval(5));
}
private static string Get(string settings, string defaultCron)
{
return settings.HasValue() ? settings : defaultCron;

@ -4,11 +4,12 @@
{
public string BaseUrl { get; set; }
public bool CollectAnalyticData { get; set; }
public bool Set { get; set; }
public bool Wizard { get; set; }
public string ApiKey { get; set; }
public bool IgnoreCertificateErrors { get; set; }
public bool DoNotSendNotificationsForAutoApprove { get; set; }
public bool HideRequestsUsers { get; set; }
public string DefaultLanguageCode { get; set; } = "en";
}
}

@ -1,6 +1,7 @@
using System;
using Ombi.Helpers;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace Ombi.Store.Entities.Requests
{
@ -19,5 +20,14 @@ namespace Ombi.Store.Entities.Requests
public int RootPathOverride { get; set; }
public int QualityOverride { get; set; }
/// <summary>
/// Only Use for setting the Language Code, Use the LanguageCode property for reading
/// </summary>
public string LangCode { get; set; }
[NotMapped]
[JsonIgnore]
public string LanguageCode => LangCode.IsNullOrEmpty() ? "en" : LangCode;
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations
{
public partial class LanguageCode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "LangCode",
table: "MovieRequests",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LangCode",
table: "MovieRequests");
}
}
}

@ -14,7 +14,7 @@ namespace Ombi.Store.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
@ -530,6 +530,24 @@ namespace Ombi.Store.Migrations
b.ToTable("RequestQueue");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestSubscription");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
{
b.Property<int>("Id")
@ -713,6 +731,8 @@ namespace Ombi.Store.Migrations
b.Property<int?>("IssueId");
b.Property<string>("LangCode");
b.Property<DateTime>("MarkedAsApproved");
b.Property<DateTime?>("MarkedAsAvailable");
@ -800,24 +820,6 @@ namespace Ombi.Store.Migrations
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestSubscription");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
@ -1082,6 +1084,13 @@ namespace Ombi.Store.Migrations
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
@ -1146,13 +1155,6 @@ namespace Ombi.Store.Migrations
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")

@ -13,7 +13,7 @@ namespace Ombi.Store.Migrations.Settings
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.3-rtm-32065");
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{

@ -10,11 +10,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.9" />
</ItemGroup>
<ItemGroup>

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Nunit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.8.0"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="15.9.0"></packagereference>
</ItemGroup>
<ItemGroup>

@ -8,14 +8,14 @@ namespace Ombi.Api.TheMovieDb
public interface IMovieDbApi
{
Task<MovieResponseDto> GetMovieInformation(int movieId);
Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId);
Task<List<MovieSearchResult>> NowPlaying();
Task<List<MovieSearchResult>> PopularMovies();
Task<List<MovieSearchResult>> SearchMovie(string searchTerm);
Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId, string langCode = "en");
Task<List<MovieSearchResult>> NowPlaying(string languageCode);
Task<List<MovieSearchResult>> PopularMovies(string languageCode);
Task<List<MovieSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode);
Task<List<TvSearchResult>> SearchTv(string searchTerm);
Task<List<MovieSearchResult>> TopRated();
Task<List<MovieSearchResult>> Upcoming();
Task<List<MovieSearchResult>> SimilarMovies(int movieId);
Task<List<MovieSearchResult>> TopRated(string languageCode);
Task<List<MovieSearchResult>> Upcoming(string languageCode);
Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode);
Task<FindResult> Find(string externalId, ExternalSource source);
Task<TvExternals> GetTvExternals(int theMovieDbId);
Task<TvInfo> GetTVInfo(string themoviedbid);

@ -63,68 +63,80 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<TvExternals>(request);
}
public async Task<List<MovieSearchResult>> SimilarMovies(int movieId)
public async Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode)
{
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("language", langCode);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
public async Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId)
public async Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId, string langCode = "en")
{
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates");
request.FullUri = request.FullUri.AddQueryParameter("language", langCode);
AddRetry(request);
var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result);
}
public async Task<List<MovieSearchResult>> SearchMovie(string searchTerm)
public async Task<List<MovieSearchResult>> SearchMovie(string searchTerm, int? year, string langageCode)
{
var request = new Request($"search/movie", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("query", searchTerm);
request.FullUri = request.FullUri.AddQueryParameter("language", langageCode);
if (year.HasValue && year.Value > 0)
{
request.FullUri = request.FullUri.AddQueryParameter("year", year.Value.ToString());
}
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
public async Task<List<MovieSearchResult>> PopularMovies()
public async Task<List<MovieSearchResult>> PopularMovies(string langageCode)
{
var request = new Request($"movie/popular", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("language", langageCode);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
public async Task<List<MovieSearchResult>> TopRated()
public async Task<List<MovieSearchResult>> TopRated(string langageCode)
{
var request = new Request($"movie/top_rated", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("language", langageCode);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
public async Task<List<MovieSearchResult>> Upcoming()
public async Task<List<MovieSearchResult>> Upcoming(string langageCode)
{
var request = new Request($"movie/upcoming", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("language", langageCode);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
public async Task<List<MovieSearchResult>> NowPlaying()
public async Task<List<MovieSearchResult>> NowPlaying(string langageCode)
{
var request = new Request($"movie/now_playing", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("language", langageCode);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<RuntimeIdentifiers>win10-x64;win10-x86;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;linux-arm;linux-arm64;</RuntimeIdentifiers>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<Version></Version>
@ -12,17 +12,17 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
<PackageReference Include="Serilog" Version="2.6.0-dev-00892" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.1-dev-00771" />
</ItemGroup>
</Project>

@ -94,7 +94,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.SickRage", "Ombi.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Notifications", "Ombi.Api.Notifications\Ombi.Api.Notifications.csproj", "{10D1FE9D-9124-42B7-B1E1-CEB99B832618}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Lidarr", "Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj", "{4FA21A20-92F4-462C-B929-2C517A88CC56}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Lidarr", "Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj", "{4FA21A20-92F4-462C-B929-2C517A88CC56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Helpers.Tests", "Ombi.Helpers.Tests\Ombi.Helpers.Tests.csproj", "{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -250,6 +252,10 @@ Global
{4FA21A20-92F4-462C-B929-2C517A88CC56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FA21A20-92F4-462C-B929-2C517A88CC56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FA21A20-92F4-462C-B929-2C517A88CC56}.Release|Any CPU.Build.0 = Release|Any CPU
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -286,6 +292,7 @@ Global
{94C9A366-2595-45EA-AABB-8E4A2E90EC5B} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{10D1FE9D-9124-42B7-B1E1-CEB99B832618} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869}

@ -34,6 +34,14 @@
<i class="fa fa-th-list"></i> {{ 'NavigationBar.Requests' | translate }}</a>
</li>
</ul>
<div *ngIf="customizationSettings && customizationSettings.useCustomPage && customPageSettings">
<ul class="nav navbar-nav">
<li id="RecentlyAdded" [routerLinkActive]="['active']">
<a [routerLink]="['/Custom']">
<i class="fa {{customPageSettings.fontAwesomeIcon}}"></i> {{customPageSettings.title}}</a>
</li>
</ul>
</div>
<div *ngIf="customizationSettings">
<ul *ngIf="customizationSettings.recentlyAddedPage" class="nav navbar-nav">
<li id="RecentlyAdded" [routerLinkActive]="['active']">

@ -7,7 +7,7 @@ import { ILocalUser } from "./auth/IUserLogin";
import { IdentityService, NotificationService } from "./services";
import { JobService, SettingsService } from "./services";
import { ICustomizationSettings } from "./interfaces";
import { ICustomizationSettings, ICustomPage } from "./interfaces";
@Component({
selector: "ombi",
@ -17,6 +17,7 @@ import { ICustomizationSettings } from "./interfaces";
export class AppComponent implements OnInit {
public customizationSettings: ICustomizationSettings;
public customPageSettings: ICustomPage;
public issuesEnabled = false;
public user: ILocalUser;
public showNav: boolean;
@ -53,7 +54,18 @@ export class AppComponent implements OnInit {
public ngOnInit() {
this.user = this.authService.claims();
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.settingsService.getCustomization().subscribe(x => {
this.customizationSettings = x;
if(this.customizationSettings.useCustomPage) {
this.settingsService.getCustomPage().subscribe(c => {
this.customPageSettings = c;
if(!this.customPageSettings.title) {
this.customPageSettings.title = "Custom Page";
this.customPageSettings.fontAwesomeIcon = "fa-check";
}
});
}
});
this.settingsService.issueEnabled().subscribe(x => this.issuesEnabled = x);
this.settingsService.voteEnabled().subscribe(x => this.voteEnabled =x);

@ -14,13 +14,16 @@ import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { CookieService } from "ng2-cookies";
import { NgxEditorModule } from "ngx-editor";
import { GrowlModule } from "primeng/components/growl/growl";
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, OverlayPanelModule, SharedModule, SidebarModule, TooltipModule } from "primeng/primeng";
import { ButtonModule, CaptchaModule, ConfirmationService, ConfirmDialogModule, DataTableModule, DialogModule, OverlayPanelModule, SharedModule, SidebarModule,
TooltipModule } from "primeng/primeng";
// Components
import { AppComponent } from "./app.component";
import { CookieComponent } from "./auth/cookie.component";
import { CustomPageComponent } from "./custompage/custompage.component";
import { PageNotFoundComponent } from "./errors/not-found.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LoginComponent } from "./login/login.component";
@ -43,6 +46,7 @@ const routes: Routes = [
{ path: "", redirectTo: "/search", pathMatch: "full" },
{ path: "login", component: LoginComponent },
{ path: "Login/OAuth/:pin", component: LoginOAuthComponent },
{ path: "Custom", component: CustomPageComponent },
{ path: "login/:landing", component: LoginComponent },
{ path: "reset", component: ResetPasswordComponent },
{ path: "token", component: TokenResetPasswordComponent },
@ -88,6 +92,7 @@ export function JwtTokenGetter() {
FormsModule,
DataTableModule,
SharedModule,
NgxEditorModule,
DialogModule,
MatButtonModule,
NgbModule.forRoot(),
@ -121,6 +126,7 @@ export function JwtTokenGetter() {
LandingPageComponent,
ResetPasswordComponent,
TokenResetPasswordComponent,
CustomPageComponent,
CookieComponent,
LoginOAuthComponent,
],

@ -0,0 +1,31 @@
<div *ngIf="form">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<button type="button" *ngIf="isAdmin" class="btn btn-info-outline" (click)="isEditing = !isEditing">Edit</button>
<div *ngIf="isEditing">
<div class="form-group">
<label for="Ip" class="control-label">Page Title
<i *ngIf="form.get('title').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Title is required"></i>
</label>
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" formControlName="title" [ngClass]="{'form-error': form.get('title').hasError('required')}">
</div>
<div class="form-group">
<label for="Ip" class="control-label">Font Awesome Icon
<i *ngIf="form.get('fontAwesomeIcon').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Font Awesome Icon is required"></i>
</label>
<input type="text" class="form-control form-control-custom " id="fontAwesomeIcon" name="fontAwesomeIcon" formControlName="fontAwesomeIcon" [ngClass]="{'form-error': form.get('fontAwesomeIcon').hasError('required')}">
</div>
<app-ngx-editor [placeholder]="'Enter text here...'" [minHeight]="100" formControlName="html"></app-ngx-editor>
<button type="submit" class="btn btn-primary-outline">Save</button>
<hr />
</div>
</form>
<div [innerHTML]="form.controls['html'].value"></div>
</div>

@ -0,0 +1,44 @@
$primary-colour: #df691a;
$primary-colour-outline: #ff761b;
$bg-colour: #333333;
$bg-colour-disabled: #252424;
:host app-ngx-editor /deep/ .ngx-toolbar {
background-color: $bg-colour;
border: 1px solid $primary-colour-outline;
}
:host app-ngx-editor /deep/ .ngx-toolbar-set {
background-color: transparent !important;
border: 1px solid $primary-colour-outline !important;
}
:host app-ngx-editor /deep/ .ngx-editor-button {
-o-transition: all 0.218s;
-moz-transition: all 0.218s;
-webkit-transition: all 0.218s;
transition: all 0.218s;
color: $primary-colour-outline;
background-color: transparent;
background-image: none;
border-color: $primary-colour-outline !important;
}
:host app-ngx-editor /deep/ .ngx-editor-button:hover {
color: black;
background-color: $primary-colour !important;
border-color: $primary-colour-outline !important;
}
:host app-ngx-editor /deep/ .ngx-editor-grippie {
background-color: $bg-colour;
border: 1px solid $primary-colour-outline;
}
:host app-ngx-editor /deep/ .ngx-editor-textarea {
border: 1px solid $primary-colour-outline !important;
}
:host app-ngx-editor /deep/ .ngx-editor-message {
display:none !important;
}

@ -0,0 +1,48 @@
import { Component, OnInit, SecurityContext } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { DomSanitizer } from "@angular/platform-browser";
import { AuthService } from "../auth/auth.service";
import { NotificationService, SettingsService } from "../services";
@Component({
templateUrl: "./custompage.component.html",
styleUrls: ["./custompage.component.scss"],
})
export class CustomPageComponent implements OnInit {
public form: FormGroup;
public isEditing: boolean;
public isAdmin: boolean;
constructor(private auth: AuthService, private settings: SettingsService, private fb: FormBuilder,
private notificationService: NotificationService,
private sanitizer: DomSanitizer) {
}
public ngOnInit() {
this.settings.getCustomPage().subscribe(x => {
x.html = this.sanitizer.sanitize(SecurityContext.HTML, this.sanitizer.bypassSecurityTrustHtml(x.html));
this.form = this.fb.group({
enabled: [x.enabled],
title: [x.title, [Validators.required]],
html: [x.html, [Validators.required]],
fontAwesomeIcon: [x.fontAwesomeIcon, [Validators.required]],
});
});
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
}
public onSubmit() {
if (this.form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
this.settings.saveCustomPage(this.form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved Custom Page settings");
} else {
this.notificationService.success("There was an error when saving the Custom Page settings");
}
});
}
}

@ -47,10 +47,18 @@ export interface IMovieUpdateModel {
id: number;
}
export interface IDenyMovieModel extends IMovieUpdateModel {
reason: string;
}
export interface IAlbumUpdateModel {
id: number;
}
export interface IDenyAlbumModel extends IAlbumUpdateModel {
reason: string;
}
export interface IFullBaseRequest extends IBaseRequest {
imdbId: string;
overview: string;
@ -113,6 +121,10 @@ export interface ITvUpdateModel {
id: number;
}
export interface ITvDenyModel extends ITvUpdateModel {
reason: string;
}
export enum OrderType {
RequestedDateAsc = 1,
RequestedDateDesc = 2,
@ -142,6 +154,7 @@ export interface IEpisodesRequests {
export interface IMovieRequestModel {
theMovieDbId: number;
languageCode: string | undefined;
}
export interface IFilter {

@ -34,6 +34,12 @@
background: any;
}
export interface ILanguageRefine {
code: string;
name: string;
nativeName: string;
}
export interface ISearchMovieResultContainer {
movies: ISearchMovieResult[];
}

@ -15,6 +15,7 @@ export interface IOmbiSettings extends ISettings {
ignoreCertificateErrors: boolean;
doNotSendNotificationsForAutoApprove: boolean;
hideRequestsUsers: boolean;
defaultLanguageCode: string;
}
export interface IUpdateSettings extends ISettings {
@ -119,6 +120,7 @@ export interface ICustomizationSettings extends ISettings {
customDonationUrl: string;
customDonationMessage: string;
recentlyAddedPage: boolean;
useCustomPage: boolean;
}
export interface IJobSettings {
@ -136,6 +138,7 @@ export interface IJobSettings {
lidarrArtistSync: string;
issuesPurge: string;
retryRequests: string;
mediaDatabaseRefresh: string;
}
export interface IIssueSettings extends ISettings {
@ -157,6 +160,13 @@ export interface IAuthenticationSettings extends ISettings {
enableOAuth: boolean;
}
export interface ICustomPage extends ISettings {
enabled: boolean;
fontAwesomeIcon: string;
title: string;
html: any;
}
export interface IUserManagementSettings extends ISettings {
importPlexUsers: boolean;
importPlexAdmin: boolean;

@ -37,7 +37,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
});
this.timer = setInterval(() => {
this.cycleBackground();
}, 10000);
}, 15000);
const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) {

@ -87,7 +87,7 @@ export class LoginComponent implements OnDestroy, OnInit {
});
this.timer = setInterval(() => {
this.cycleBackground();
}, 7000);
}, 15000);
const base = this.location.getBaseHrefFromDOM();
if (base.length > 1) {

@ -88,7 +88,7 @@
</div>
<div *ngIf="request.denied" id="requestDenied">
{{ 'Requests.Denied' | translate }}
<i style="color:red;" class="fa fa-check"></i>
<i style="color:red;" class="fa fa-check" pTooltip="{{request.deniedReason}}"></i>
</div>
@ -215,6 +215,14 @@
<p-paginator [rows]="10" [totalRecords]="totalMovies" (onPageChange)="paginate($event)"></p-paginator>
</div>
<p-dialog *ngIf="requestToDeny" header="Deny Request '{{requestToDeny.title}}''" [(visible)]="denyDisplay" [draggable]="false">
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button"(click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>
<issue-report [movie]="true" [visible]="issuesBarVisible" (visibleChange)="issuesBarVisible = $event;" [title]="issueRequest?.title"
[issueCategory]="issueCategorySelected" [id]="issueRequest?.id" [providerId]="issueProviderId"></issue-report>

@ -37,9 +37,12 @@ export class MovieRequestsComponent implements OnInit {
public orderType: OrderType = OrderType.RequestedDateDesc;
public OrderType = OrderType;
public denyDisplay: boolean;
public requestToDeny: IMovieRequests;
public rejectionReason: string;
public totalMovies: number = 100;
private currentlyLoaded: number;
public currentlyLoaded: number;
private amountToLoad: number;
constructor(
@ -130,8 +133,22 @@ export class MovieRequestsComponent implements OnInit {
}
public deny(request: IMovieRequests) {
request.denied = true;
this.denyRequest(request);
this.requestToDeny = request;
this.denyDisplay = true;
}
public denyRequest() {
this.requestService.denyMovie({ id: this.requestToDeny.id, reason: this.rejectionReason })
.subscribe(x => {
this.denyDisplay = false;
if (x.result) {
this.notificationService.success(
`Request for ${this.requestToDeny.title} has been denied successfully`);
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
this.requestToDeny.denied = false;
}
});
}
public selectRootFolder(searchResult: IMovieRequests, rootFolderSelected: IRadarrRootFolder, event: any) {
@ -278,19 +295,6 @@ export class MovieRequestsComponent implements OnInit {
});
}
private denyRequest(request: IMovieRequests) {
this.requestService.denyMovie({ id: request.id })
.subscribe(x => {
if (x.result) {
this.notificationService.success(
`Request for ${request.title} has been denied successfully`);
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
request.denied = false;
}
});
}
private loadInit() {
this.requestService.getMovieRequests(this.amountToLoad, 0, this.orderType, this.filter)
.subscribe(x => {

@ -92,7 +92,7 @@
</div>
<div *ngIf="request.denied" id="requestDenied">
{{ 'Requests.Denied' | translate }}
<i style="color:red;" class="fa fa-check"></i>
<i style="color:red;" class="fa fa-check" pTooltip="{{request.deniedReason}}"></i>
</div>
@ -265,4 +265,15 @@
<button class="btn btn-sm btn-primary-outline" (click)="clearFilter($event)">
<i class="fa fa-filter"></i> {{ 'Filter.ClearFilter' | translate }}</button>
</p-sidebar>
</p-sidebar>
<p-dialog *ngIf="requestToDeny" header="Deny Request '{{requestToDeny.title}}''" [(visible)]="denyDisplay" [draggable]="false">
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button"(click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>

@ -34,9 +34,12 @@ export class MusicRequestsComponent implements OnInit {
public orderType: OrderType = OrderType.RequestedDateDesc;
public OrderType = OrderType;
public denyDisplay: boolean;
public requestToDeny: IAlbumRequest;
public rejectionReason: string;
public totalAlbums: number = 100;
private currentlyLoaded: number;
public currentlyLoaded: number;
private amountToLoad: number;
constructor(
@ -126,23 +129,22 @@ export class MusicRequestsComponent implements OnInit {
}
public deny(request: IAlbumRequest) {
request.denied = true;
this.denyRequest(request);
this.requestToDeny = request;
this.denyDisplay = true;
}
// public selectRootFolder(searchResult: IAlbumRequest, rootFolderSelected: IRadarrRootFolder, event: any) {
// event.preventDefault();
// // searchResult.rootPathOverride = rootFolderSelected.id;
// this.setOverride(searchResult);
// this.updateRequest(searchResult);
// }
// public selectQualityProfile(searchResult: IMovieRequests, profileSelected: IRadarrProfile, event: any) {
// event.preventDefault();
// searchResult.qualityOverride = profileSelected.id;
// this.setOverride(searchResult);
// this.updateRequest(searchResult);
// }
public denyRequest() {
this.requestService.denyAlbum({ id: this.requestToDeny.id, reason: this.rejectionReason })
.subscribe(x => {
if (x.result) {
this.notificationService.success(
`Request for ${this.requestToDeny.title} has been denied successfully`);
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
this.requestToDeny.denied = false;
}
});
}
public reportIssue(catId: IIssueCategory, req: IAlbumRequest) {
this.issueRequest = req;
@ -266,19 +268,6 @@ export class MusicRequestsComponent implements OnInit {
});
}
private denyRequest(request: IAlbumRequest) {
this.requestService.denyAlbum({ id: request.id })
.subscribe(x => {
if (x.result) {
this.notificationService.success(
`Request for ${request.title} has been denied successfully`);
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
request.denied = false;
}
});
}
private loadInit() {
this.requestService.getAlbumRequests(this.amountToLoad, 0, this.orderType, this.filter)
.subscribe(x => {

@ -21,7 +21,8 @@
<button id="unavailableBtn" *ngIf="child.available" (click)="changeAvailability(child, false)" style="text-align: right" value="false" class="btn btn-sm btn-info-outline change"><i class="fa fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}</button>
<button id="availableBtn" *ngIf="!child.available" (click)="changeAvailability(child, true)" style="text-align: right" value="true" class="btn btn-sm btn-success-outline change"><i class="fa fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}</button>
<button id="denyBtn" *ngIf="!child.denied" type="button" (click)="deny(child)" class="btn btn-sm btn-danger-outline deny"><i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}</button>
<button id="denyBtn" *ngIf="!child.denied" type="button" (click)="deny(child)" class="btn btn-sm btn-danger-outline deny">
<i class="fa fa-times"></i> {{ 'Requests.Deny' | translate }}</button>
</div>
<div *ngIf="isAdmin || isRequestUser(child)">
@ -76,7 +77,9 @@
{{ep.airDate | amLocal | amDateFormat: 'L' }}
</td>
<td>
<span *ngIf="child.denied" class="label label-danger" id="deniedLabel" [translate]="'Common.Denied'"></span>
<span *ngIf="child.denied" class="label label-danger" id="deniedLabel" [translate]="'Common.Denied'">
<i style="color:red;" class="fa fa-check" pTooltip="{{child.deniedReason}}"></i>
</span>
<span *ngIf="!child.denied && ep.available" class="label label-success" id="availableLabel" [translate]="'Common.Available'"></span>
<span *ngIf="!child.denied &&ep.approved && !ep.available" class="label label-info" id="processingRequestLabel" [translate]="'Common.ProcessingRequest'"></span>
<div *ngIf="!child.denied && !ep.approved">
@ -98,3 +101,12 @@
</div>
</div>
<p-dialog *ngIf="requestToDeny" header="Deny Request '{{requestToDeny.title}}''" [(visible)]="denyDisplay" [draggable]="false">
<span>Please enter a rejection reason, the user will be notified of this:</span>
<textarea [(ngModel)]="rejectionReason" class="form-control-custom form-control"></textarea>
<p-footer>
<button type="button" (click)="denyRequest();" label="Reject" class="btn btn-success">Deny</button>
<button type="button"(click)="denyDisplay=false" label="Close" class="btn btn-danger">Close</button>
</p-footer>
</p-dialog>

@ -11,6 +11,10 @@ export class TvRequestChildrenComponent {
@Input() public childRequests: IChildRequests[];
@Input() public isAdmin: boolean;
@Input() public currentUser: string;
public denyDisplay: boolean;
public requestToDeny: IChildRequests;
public rejectionReason: string;
@Output() public requestDeleted = new EventEmitter<number>();
@ -57,20 +61,26 @@ export class TvRequestChildrenComponent {
public deny(request: IChildRequests) {
request.denied = true;
this.requestToDeny = request;
this.denyDisplay = true;
request.seasonRequests.forEach((season) => {
season.episodes.forEach((ep) => {
ep.approved = false;
});
});
this.requestService.denyChild({ id: request.id })
}
public denyRequest() {
this.requestService.denyChild({ id: this.requestToDeny.id, reason: this.rejectionReason })
.subscribe(x => {
this.denyDisplay = false;
if (x.result) {
this.notificationService.success(
`Request has been denied successfully`);
} else {
this.notificationService.warning("Request Denied", x.message ? x.message : x.errorMessage);
request.approved = false;
this.requestToDeny.approved = false;
}
});
}

@ -1,11 +1,11 @@
<!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab">
<div class="input-group">
<div class="input-group search-bar-background">
<input id="search" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons"
(keyup)="search($event)">
<div class="input-group-addon right-radius">
<div class="btn-group">
<div class="btn-group" role="group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ 'Search.Suggestions' | translate }}
<i class="fa fa-chevron-down"></i>
@ -16,10 +16,43 @@
<li><a (click)="topRatedMovies()" [translate]="'Search.Movies.TopRatedMovies'"></a></li>
<li><a (click)="nowPlayingMovies()" [translate]="'Search.Movies.NowPlayingMovies'"></a></li>
</ul>
<button class="btn btn-sm btn-primary-outline" (click)="refineOpen()">
{{ 'Search.Refine' | translate }}
<i class="fa" [ngClass]="{'fa-chevron-down': !refineSearchEnabled, 'fa-chevron-up': refineSearchEnabled}"></i>
</button>
</div>
<i class="fa fa-search"></i>
</div>
</div>
<!-- Refine search options -->
<div class="row top-spacing form-group vcenter" *ngIf="refineSearchEnabled">
<div class="col-md-1">
<div class="form-group">
<label class="control-label">Year</label>
<input [(ngModel)]="searchYear" class="form-control form-control-custom refine-option">
</div>
</div>
<!-- <label for="name" class="col-xs-2 col-md-1">Language:</label> -->
<div class="col-md-2">
<div class="form-group">
<label for="select" class="control-label">Language</label>
<div id="profiles">
<select [(ngModel)]="selectedLanguage" class="form-control form-control-custom refine-option"
id="select">
<option *ngFor="let lang of langauges" value="{{lang.code}}">{{lang.nativeName}}</option>
</select>
</div>
</div>
</div>
<div class="col-md-9">
<button class="btn pull-right btn-success-outline" (click)="applyRefinedSearch()">Apply</button>
</div>
</div>
<remaining-requests [movie]="true" [quotaRefreshEvents]="movieRequested.asObservable()" #remainingFilms></remaining-requests>
@ -111,11 +144,15 @@
<br />
<div *ngIf="result.available">
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnPlex' | translate}}</a>
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline" href="{{result.embyUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnEmby' | translate}}</a>
<a *ngIf="result.plexUrl" style="text-align: right" class="btn btn-sm btn-success-outline" href="{{result.plexUrl}}"
target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnPlex' | translate}}</a>
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline"
href="{{result.embyUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
</div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> {{'Requests.ReportIssue' | translate}}
<span class="caret"></span>
</button>

@ -6,12 +6,15 @@ import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchMovieResult } from "../interfaces";
import { NotificationService, RequestService, SearchService } from "../services";
import { IIssueCategory, ILanguageRefine, IRequestEngineResult, ISearchMovieResult } from "../interfaces";
import { NotificationService, RequestService, SearchService, SettingsService } from "../services";
import * as languageData from "../../other/iso-lang.json";
@Component({
selector: "movie-search",
templateUrl: "./moviesearch.component.html",
styleUrls: ["./search.component.scss"],
})
export class MovieSearchComponent implements OnInit {
@ -22,6 +25,10 @@ export class MovieSearchComponent implements OnInit {
public result: IRequestEngineResult;
public searchApplied = false;
public refineSearchEnabled = false;
public searchYear?: number;
public selectedLanguage: string;
public langauges: ILanguageRefine[];
@Input() public issueCategories: IIssueCategory[];
@Input() public issuesEnabled: boolean;
@ -36,24 +43,14 @@ export class MovieSearchComponent implements OnInit {
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) {
private readonly platformLocation: PlatformLocation, private settingsService: SettingsService) {
this.langauges = <ILanguageRefine[]><any>languageData;
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
distinctUntilChanged(), // only emit if value is different from previous value
).subscribe(x => {
this.searchText = x as string;
if (this.searchText === "") {
this.clearResults();
return;
}
this.searchService.searchMovie(this.searchText)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
this.runSearch();
});
this.defaultPoster = "../../../images/default_movie_poster.png";
const base = this.platformLocation.getBaseHrefFromDOM();
@ -70,7 +67,7 @@ export class MovieSearchComponent implements OnInit {
result: false,
errorMessage: "",
};
this.settingsService.getDefaultLanguage().subscribe(x => this.selectedLanguage = x);
this.popularMovies();
}
@ -87,7 +84,8 @@ export class MovieSearchComponent implements OnInit {
}
try {
this.requestService.requestMovie({ theMovieDbId: searchResult.id })
const language = this.selectedLanguage && this.selectedLanguage.length > 0 ? this.selectedLanguage : "en";
this.requestService.requestMovie({ theMovieDbId: searchResult.id, languageCode: language })
.subscribe(x => {
this.result = x;
if (this.result.result) {
@ -161,7 +159,8 @@ export class MovieSearchComponent implements OnInit {
public similarMovies(theMovieDbId: number) {
this.clearResults();
this.searchService.similarMovies(theMovieDbId)
const lang = this.selectedLanguage && this.selectedLanguage.length > 0 ? this.selectedLanguage : "";
this.searchService.similarMovies(theMovieDbId, lang)
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
@ -184,6 +183,17 @@ export class MovieSearchComponent implements OnInit {
});
}
public refineOpen() {
this.refineSearchEnabled = !this.refineSearchEnabled;
if (!this.refineSearchEnabled) {
this.searchYear = undefined;
}
}
public applyRefinedSearch() {
this.runSearch();
}
private getExtraInfo() {
this.movieResults.forEach((val, index) => {
@ -194,10 +204,18 @@ export class MovieSearchComponent implements OnInit {
}
val.background = this.sanitizer.bypassSecurityTrustStyle
("url(" + "https://image.tmdb.org/t/p/w1280" + val.backdropPath + ")");
this.searchService.getMovieInformation(val.id)
if (this.applyRefinedSearch) {
this.searchService.getMovieInformationWithRefined(val.id, this.selectedLanguage)
.subscribe(m => {
this.updateItem(val, m);
});
} else {
this.searchService.getMovieInformation(val.id)
.subscribe(m => {
this.updateItem(val, m);
});
}
});
}
@ -214,4 +232,30 @@ export class MovieSearchComponent implements OnInit {
this.movieResults = [];
this.searchApplied = false;
}
private runSearch() {
if (this.searchText === "") {
this.clearResults();
return;
}
if (this.refineOpen) {
this.searchService.searchMovieWithRefined(this.searchText, this.searchYear, this.selectedLanguage)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
} else {
this.searchService.searchMovie(this.searchText)
.subscribe(x => {
this.movieResults = x;
this.searchApplied = true;
// Now let's load some extra info including IMDB Id
// This way the search is fast at displaying results.
this.getExtraInfo();
});
}
}
}

@ -65,7 +65,7 @@ export class MovieSearchGridComponent implements OnInit {
}
try {
this.requestService.requestMovie({ theMovieDbId: searchResult.id })
this.requestService.requestMovie({ theMovieDbId: searchResult.id, languageCode: "en" })
.subscribe(x => {
this.result = x;

@ -1,14 +1,11 @@
import { PlatformLocation } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { TranslateService } from "@ngx-translate/core";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { AuthService } from "../../auth/auth.service";
import { IIssueCategory, IRequestEngineResult, ISearchMovieResult } from "../../interfaces";
import { IIssueCategory, IRequestEngineResult } from "../../interfaces";
import { ISearchAlbumResult, ISearchArtistResult } from "../../interfaces/ISearchMusicResult";
import { NotificationService, RequestService, SearchService } from "../../services";
import { SearchService } from "../../services";
@Component({
selector: "music-search",
@ -35,10 +32,8 @@ export class MusicSearchComponent implements OnInit {
public defaultPoster: string;
constructor(
private searchService: SearchService, private requestService: RequestService,
private notificationService: NotificationService, private authService: AuthService,
private readonly translate: TranslateService, private sanitizer: DomSanitizer,
private readonly platformLocation: PlatformLocation) {
private searchService: SearchService, private sanitizer: DomSanitizer,
private platformLocation: PlatformLocation) {
this.searchChanged.pipe(
debounceTime(600), // Wait Xms after the last event before emitting last event
@ -110,45 +105,6 @@ export class MusicSearchComponent implements OnInit {
this.searchChanged.next(`lidarr:${artistId}`);
}
public request(searchResult: ISearchMovieResult) {
searchResult.requested = true;
searchResult.requestProcessing = true;
searchResult.showSubscribe = false;
if (this.authService.hasRole("admin") || this.authService.hasRole("AutoApproveMovie")) {
searchResult.approved = true;
}
try {
this.requestService.requestMovie({ theMovieDbId: searchResult.id })
.subscribe(x => {
this.result = x;
if (this.result.result) {
this.translate.get("Search.RequestAdded", { title: searchResult.title }).subscribe(x => {
this.notificationService.success(x);
searchResult.processed = true;
});
} else {
if (this.result.errorMessage && this.result.message) {
this.notificationService.warning("Request Added", `${this.result.message} - ${this.result.errorMessage}`);
} else {
this.notificationService.warning("Request Added", this.result.message ? this.result.message : this.result.errorMessage);
}
searchResult.requested = false;
searchResult.approved = false;
searchResult.processed = false;
searchResult.requestProcessing = false;
}
});
} catch (e) {
searchResult.processed = false;
searchResult.requestProcessing = false;
this.notificationService.error(e);
}
}
public viewAlbumsForArtist(albums: ISearchAlbumResult[]) {
this.clearArtistResults();
this.searchAlbum = true;

@ -0,0 +1,30 @@
@media (max-width: 978px) {
.top-spacing {
padding-top: 5%
}
.form-control-search {
width: 77%;
}
}
@media (min-width: 979px) {
.top-spacing {
padding-top: 2%
}
.form-control-search {
width: 90%;
}
}
.search-bar-background {
background-color: #333333;
}
.vcenter {
display: flex;
align-items: center;
}
.refine-option {
box-shadow: inset 0 1px 5px rgba(0,0,0,1.0);
}

@ -5,8 +5,8 @@ import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { TreeNode } from "primeng/primeng";
import { FilterType, IAlbumRequest, IAlbumRequestModel, IAlbumUpdateModel, IChildRequests, IFilter, IMovieRequestModel, IMovieRequests,
IMovieUpdateModel, IRequestEngineResult, IRequestsViewModel, ITvRequests, ITvUpdateModel, OrderType } from "../interfaces";
import { FilterType, IAlbumRequest, IAlbumRequestModel, IAlbumUpdateModel, IChildRequests, IDenyAlbumModel, IDenyMovieModel, IFilter,
IMovieRequestModel, IMovieRequests, IMovieUpdateModel, IRequestEngineResult, IRequestsViewModel, ITvDenyModel, ITvRequests, ITvUpdateModel, OrderType } from "../interfaces";
import { ITvRequestViewModel } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
@ -50,7 +50,7 @@ export class RequestService extends ServiceHelpers {
return this.http.post<IRequestEngineResult>(`${this.url}Movie/Approve`, JSON.stringify(movie), {headers: this.headers});
}
public denyMovie(movie: IMovieUpdateModel): Observable<IRequestEngineResult> {
public denyMovie(movie: IDenyMovieModel): Observable<IRequestEngineResult> {
return this.http.put<IRequestEngineResult>(`${this.url}Movie/Deny`, JSON.stringify(movie), {headers: this.headers});
}
@ -118,7 +118,7 @@ export class RequestService extends ServiceHelpers {
return this.http.put<IChildRequests>(`${this.url}tv/child`, JSON.stringify(child), {headers: this.headers});
}
public denyChild(child: ITvUpdateModel): Observable<IRequestEngineResult> {
public denyChild(child: ITvDenyModel): Observable<IRequestEngineResult> {
return this.http.put<IRequestEngineResult>(`${this.url}tv/deny`, JSON.stringify(child), {headers: this.headers});
}
@ -161,7 +161,7 @@ export class RequestService extends ServiceHelpers {
return this.http.post<IRequestEngineResult>(`${this.url}music/Approve`, JSON.stringify(Album), {headers: this.headers});
}
public denyAlbum(Album: IAlbumUpdateModel): Observable<IRequestEngineResult> {
public denyAlbum(Album: IDenyAlbumModel): Observable<IRequestEngineResult> {
return this.http.put<IRequestEngineResult>(`${this.url}music/Deny`, JSON.stringify(Album), {headers: this.headers});
}

@ -18,10 +18,15 @@ export class SearchService extends ServiceHelpers {
// Movies
public searchMovie(searchTerm: string): Observable<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/` + searchTerm);
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/${searchTerm}`);
}
public similarMovies(theMovieDbId: number): Observable<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/${theMovieDbId}/similar`);
public searchMovieWithRefined(searchTerm: string, year: number | undefined, langCode: string): Observable<ISearchMovieResult[]> {
return this.http.post<ISearchMovieResult[]>(`${this.url}/Movie/`, { searchTerm, year, languageCode: langCode });
}
public similarMovies(theMovieDbId: number, langCode: string): Observable<ISearchMovieResult[]> {
return this.http.post<ISearchMovieResult[]>(`${this.url}/Movie/similar`, {theMovieDbId, languageCode: langCode});
}
public popularMovies(): Observable<ISearchMovieResult[]> {
@ -40,34 +45,38 @@ export class SearchService extends ServiceHelpers {
return this.http.get<ISearchMovieResult>(`${this.url}/Movie/info/${theMovieDbId}`);
}
public getMovieInformationWithRefined(theMovieDbId: number, langCode: string): Observable<ISearchMovieResult> {
return this.http.post<ISearchMovieResult>(`${this.url}/Movie/info`, { theMovieDbId, languageCode: langCode });
}
// TV
public searchTv(searchTerm: string): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/${searchTerm}`, {headers: this.headers});
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/${searchTerm}`, { headers: this.headers });
}
public searchTvTreeNode(searchTerm: string): Observable<TreeNode[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/${searchTerm}/tree`, {headers: this.headers});
return this.http.get<TreeNode[]>(`${this.url}/Tv/${searchTerm}/tree`, { headers: this.headers });
}
public getShowInformationTreeNode(theTvDbId: number): Observable<TreeNode> {
return this.http.get<TreeNode>(`${this.url}/Tv/info/${theTvDbId}/Tree`, {headers: this.headers});
return this.http.get<TreeNode>(`${this.url}/Tv/info/${theTvDbId}/Tree`, { headers: this.headers });
}
public getShowInformation(theTvDbId: number): Observable<ISearchTvResult> {
return this.http.get<ISearchTvResult>(`${this.url}/Tv/info/${theTvDbId}`, {headers: this.headers});
return this.http.get<ISearchTvResult>(`${this.url}/Tv/info/${theTvDbId}`, { headers: this.headers });
}
public popularTv(): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/popular`, {headers: this.headers});
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/popular`, { headers: this.headers });
}
public mostWatchedTv(): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/mostwatched`, {headers: this.headers});
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/mostwatched`, { headers: this.headers });
}
public anticipatedTv(): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/anticipated`, {headers: this.headers});
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/anticipated`, { headers: this.headers });
}
public trendingTv(): Observable<ISearchTvResult[]> {
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/trending`, {headers: this.headers});
return this.http.get<ISearchTvResult[]>(`${this.url}/Tv/trending`, { headers: this.headers });
}
// Music
public searchArtist(searchTerm: string): Observable<ISearchArtistResult[]> {

@ -10,6 +10,7 @@ import {
ICronTestModel,
ICronViewModelBody,
ICustomizationSettings,
ICustomPage,
IDiscordNotifcationSettings,
IDogNzbSettings,
IEmailNotificationSettings,
@ -52,6 +53,10 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IOmbiSettings>(`${this.url}/Ombi/`, {headers: this.headers});
}
public getDefaultLanguage(): Observable<string> {
return this.http.get<string>(`${this.url}/defaultlanguage/`, {headers: this.headers});
}
public saveOmbi(settings: IOmbiSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/Ombi/`, JSON.stringify(settings), {headers: this.headers});
}
@ -108,6 +113,14 @@ export class SettingsService extends ServiceHelpers {
return this.http.get<IAuthenticationSettings>(`${this.url}/Authentication`, {headers: this.headers});
}
public getCustomPage(): Observable<ICustomPage> {
return this.http.get<ICustomPage>(`${this.url}/CustomPage`, {headers: this.headers});
}
public saveCustomPage(model: ICustomPage): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/CustomPage`, model, {headers: this.headers});
}
public getClientId(): Observable<string> {
return this.http.get<string>(`${this.url}/clientid`, {headers: this.headers});
}

@ -21,8 +21,8 @@
<div class="form-group">
<label for="applicationurl" class="control-label">Application URL</label>
<div>
<input type="text" [(ngModel)]="settings.applicationUrl" class="form-control form-control-custom " id="applicationurl" name="applicationurl"
placeholder="http://ombi.io/" value="{{settings.applicationUrl}}">
<input type="text" [(ngModel)]="settings.applicationUrl" class="form-control form-control-custom " id="applicationurl"
name="applicationurl" placeholder="http://ombi.io/" value="{{settings.applicationUrl}}">
</div>
</div>
@ -36,8 +36,8 @@
<div class="form-group">
<label for="logo" class="control-label">Custom Logo</label>
<div>
<input type="text" [(ngModel)]="settings.logo" class="form-control form-control-custom " id="logo" name="logo" value="{{settings.logo}}"
tooltipPosition="top" pTooltip="Use a URL e.g. www.google.com/logo.png">
<input type="text" [(ngModel)]="settings.logo" class="form-control form-control-custom " id="logo" name="logo"
value="{{settings.logo}}" tooltipPosition="top" pTooltip="Use a URL e.g. www.google.com/logo.png">
</div>
</div>
<div *ngIf="settings.logo" class="form-group">
@ -50,14 +50,16 @@
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enableCustomDonations" name="enableCustomDonations" [(ngModel)]="settings.enableCustomDonations">
<label for="enableCustomDonations" tooltipPosition="top" pTooltip="Enable to show a custom donation link in the navigation bar">Enable custom donation link</label>
<label for="enableCustomDonations" tooltipPosition="top" pTooltip="Enable to show a custom donation link in the navigation bar">Enable
custom donation link</label>
</div>
</div>
<div class="form-group" *ngIf="settings.enableCustomDonations">
<label for="customDonation" class="control-label">Custom Donation URL</label>
<div>
<input [disabled]="!settings.enableCustomDonations" type="text" [(ngModel)]="settings.customDonationUrl" class="form-control form-control-custom " name="customDonation" value="{{settings.customDonationUrl}}"
<input [disabled]="!settings.enableCustomDonations" type="text" [(ngModel)]="settings.customDonationUrl"
class="form-control form-control-custom " name="customDonation" value="{{settings.customDonationUrl}}"
tooltipPosition="top" pTooltip="A link to a Paypal address, or your custom donation url.">
</div>
</div>
@ -65,10 +67,19 @@
<div class="form-group" *ngIf="settings.enableCustomDonations">
<label for="customDonationMessage" class="control-label">Donation Button Message</label>
<div>
<input [disabled]="!settings.enableCustomDonations" type="text" [(ngModel)]="settings.customDonationMessage" class="form-control form-control-custom " name="customDonationMessage" value="{{settings.customDonationMessage}}"
<input [disabled]="!settings.enableCustomDonations" type="text" [(ngModel)]="settings.customDonationMessage"
class="form-control form-control-custom " name="customDonationMessage" value="{{settings.customDonationMessage}}"
tooltipPosition="top" pTooltip="Set a custom message to be displayed in the navigation bar.">
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="useCustomPage" name="useCustomPage" [(ngModel)]="settings.useCustomPage">
<label for="useCustomPage" tooltipPosition="top" pTooltip="Enabled a custom page where you can fully edit">Use
Custom Page</label>
</div>
</div>
@ -85,9 +96,8 @@
<label for="customCss" class="control-label">Custom CSS</label>
</div>
<div class="form-group language-css" pCode>
<textarea rows="25" type="text"
pTooltip="Enter your custom styles here" tooltipPosition="top"
class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="settings.customCss"> {{settings.customCss}} </textarea>
<textarea rows="25" type="text" pTooltip="Enter your custom styles here" tooltipPosition="top" class="form-control-custom form-control "
id="themeContent" name="themeContent" [(ngModel)]="settings.customCss"> {{settings.customCss}} </textarea>
</div>
</div>
</div>

@ -105,6 +105,13 @@
<small *ngIf="form.get('issuesPurge').hasError('required')" class="error-text">The Issues Purge is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('issuesPurge')?.value)">Test</button>
</div>
<div class="form-group">
<label for="userImporter" class="control-label">Media Data Refresh</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('mediaDatabaseRefresh').hasError('required')}" id="mediaDatabaseRefresh" name="mediaDatabaseRefresh" formControlName="mediaDatabaseRefresh">
<small *ngIf="form.get('mediaDatabaseRefresh').hasError('required')" class="error-text">The Media Database Refresh is required</small>
<button type="button" class="btn btn-sm btn-primary-outline" (click)="testCron(form.get('mediaDatabaseRefresh')?.value)">Test</button>
</div>
</div>
<div class="form-group">
<div>

@ -37,6 +37,7 @@ export class JobsComponent implements OnInit {
lidarrArtistSync: [x.lidarrArtistSync, Validators.required],
issuesPurge: [x.issuesPurge, Validators.required],
retryRequests: [x.retryRequests, Validators.required],
mediaDatabaseRefresh: [x.mediaDatabaseRefresh, Validators.required],
});
});
}

@ -22,64 +22,80 @@
</div>
</div>-->
<div class="col-md-6">
<div class="form-group">
<label for="baseUrl" class="control-label">Base Url</label>
<div>
<input type="text" class="form-control form-control-custom" id="baseUrl" name="baseUrl" formControlName="baseUrl">
<div class="form-group">
<label for="baseUrl" class="control-label">Base Url</label>
<div>
<input type="text" class="form-control form-control-custom" id="baseUrl" name="baseUrl"
formControlName="baseUrl">
</div>
</div>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">Api Key</label>
<div class="input-group">
<input type="text" class="form-control form-control-custom" id="ApiKey" name="ApiKey" formControlName="apiKey" readonly="readonly" #apiKey>
<div class="form-group">
<label for="ApiKey" class="control-label">Api Key</label>
<div class="input-group">
<input type="text" class="form-control form-control-custom" id="ApiKey" name="ApiKey"
formControlName="apiKey" readonly="readonly" #apiKey>
<div class="input-group-addon">
<div (click)="refreshApiKey()" id="refreshKey" class="fa fa-refresh" title="Reset API Key" pTooltip="This will invalidate the old API key" ></div>
</div>
<div class="input-group-addon">
<div (click)="refreshApiKey()" id="refreshKey" class="fa fa-refresh" title="Reset API Key"
pTooltip="This will invalidate the old API key"></div>
</div>
<div class="input-group-addon">
<div ngxClipboard [ngxClipboard]="apiKey" class="fa fa-clipboard" (cbOnSuccess)="successfullyCopied()"></div>
<div class="input-group-addon">
<div ngxClipboard [ngxClipboard]="apiKey" class="fa fa-clipboard" (cbOnSuccess)="successfullyCopied()"></div>
</div>
</div>
</div>
</div>
<br/>
<br />
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="doNotSendNotificationsForAutoApprove" name="doNotSendNotificationsForAutoApprove" formControlName="doNotSendNotificationsForAutoApprove">
<label for="doNotSendNotificationsForAutoApprove">Do not send Notifications if a User has the Auto Approve permission</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="doNotSendNotificationsForAutoApprove" name="doNotSendNotificationsForAutoApprove"
formControlName="doNotSendNotificationsForAutoApprove">
<label for="doNotSendNotificationsForAutoApprove">Do not send Notifications if a User has the Auto
Approve permission</label>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="hideRequestsUsers" name="hideRequestsUsers" formControlName="hideRequestsUsers">
<label for="hideRequestsUsers">Hide requests from other users</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="hideRequestsUsers" name="hideRequestsUsers" formControlName="hideRequestsUsers">
<label for="hideRequestsUsers">Hide requests from other users</label>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="ignoreCertificateErrors" name="ignoreCertificateErrors" formControlName="ignoreCertificateErrors">
<label for="ignoreCertificateErrors" tooltipPosition="top" pTooltip="Enable if you are having connectivity problems over SSL">Ignore any certificate errors</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="ignoreCertificateErrors" name="ignoreCertificateErrors" formControlName="ignoreCertificateErrors">
<label for="ignoreCertificateErrors" tooltipPosition="top" pTooltip="Enable if you are having connectivity problems over SSL">Ignore
any certificate errors</label>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="CollectAnalyticData" name="CollectAnalyticData" formControlName="collectAnalyticData">
<label for="CollectAnalyticData">Allow us to collect anonymous analytical data e.g. browser used</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="CollectAnalyticData" name="CollectAnalyticData" formControlName="collectAnalyticData">
<label for="CollectAnalyticData" tooltipPosition="top" pTooltip="This will allow us to have a better understanding of the userbase so we know what we should be supporting">Allow
us to collect anonymous analytical data e.g. browser used</label>
</div>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
<div class="form-group" *ngIf="langauges">
<label for="select" class="control-label">Language</label>
<div id="profiles">
<select formControlName="defaultLanguageCode" class="form-control form-control-custom" id="select">
<option *ngFor="let lang of langauges" value="{{lang.code}}">{{lang.nativeName}}</option>
</select>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</div>
</div>
</form>
</fieldset>
</fieldset>

@ -1,16 +1,19 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { IOmbiSettings } from "../../interfaces";
import { ILanguageRefine, IOmbiSettings } from "../../interfaces";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
import * as languageData from "../../../other/iso-lang.json";
@Component({
templateUrl: "./ombi.component.html",
})
export class OmbiComponent implements OnInit {
public form: FormGroup;
public langauges: ILanguageRefine[];
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
@ -25,8 +28,10 @@ export class OmbiComponent implements OnInit {
baseUrl: [x.baseUrl],
doNotSendNotificationsForAutoApprove: [x.doNotSendNotificationsForAutoApprove],
hideRequestsUsers: [x.hideRequestsUsers],
defaultLanguageCode: [x.defaultLanguageCode],
});
});
this.langauges = <ILanguageRefine[]><any>languageData;
}
public refreshApiKey() {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save