Merge develop into recentlyadded

pull/2089/head
Jamie 6 years ago
commit b55efd60c5

@ -2,8 +2,118 @@
## (unreleased) ## (unreleased)
### **Fixes**
- Small memory improvements in the Plex Sync. [Jamie]
- Fixed the sort issue on the user Management page. Also added sorting to the Movie Requests page. [tidusjar]
- Downgraded the angular2-jwt library since it has a bug in it. #2064. [tidusjar]
- Fixed an issue when Plex decideds to reuse the Plex Key for a different media item... #2038. [tidusjar]
- Fixed an issue where we might show the Imdb link when we do not have a imdbid #1797. [tidusjar]
- Fixed the issue where we can no longer select Pending Approval in the filters #2057. [tidusjar]
- Fixed the API key not working when attempting to get requests #2058. [tidusjar]
- Fixed #2056. [tidusjar]
- Experimental, set the Webpack base root to the ombi base path if we have it. This should hopefully fix the reverse proxy issues. [Jamie]
## v3.0.3000 (2018-03-09)
### **New Features** ### **New Features**
- Added the ability to override root and quality options in Sonarr (#2049) [Jamie]
- Added Pending Approval into the filters list. [tidusjar]
- Added the ability to hide requests that have not been made by that user (#2052) [Jamie]
- Update README.md. [Jamie]
- Update README.md. [Louis Laureys]
### **Fixes**
- Fixed #2042. [Jamie]
## v3.0.0 (2018-03-04)
### **New Features**
- Update build.cake. [Jamie]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Added a similar button to the movie searches. Makes movie discoverablility easier. [tidusjar]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update ISSUE_TEMPLATE.md. [Jamie]
- Update appveyor.yml. [Jamie]
- Update ISSUE_TEMPLATE.md. [PotatoQuality]
- Update ISSUE_TEMPLATE.md. [PotatoQuality]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update README.md. [PotatoQuality]
- Change the default templates to use {IssueUser} [Jamie]
- Changed the base url validation. [tidusjar]
- Added bulk editing (#1941) [Jamie]
- Change the poster size to w300 #1932. [Jamie]
- Added a default user agent on all API calls. [tidusjar]
- Update request.service.ts. [Jamie]
- Added a filter onto the movies requests page for some inital feedback. [Jamie]
- Added ordering to the User Management screen. [Jamie]
- Update README.md. [Jamie]
- Added custom donation url (#1902) [m4tta]
- Changed the url scheme to make it easier to parse. [Jamie]
- Added Norwegian to the translation code, forgot to check this in. [Jamie]
- Added Norwegian to the language dropdown. [Jamie]
- Added the stuff needed for omBlur. [tidusjar]
- Update README.md (#1872) [xnaas]
- Update README.md. [Jamie]
- Update plex.component.html. [Jamie]
- Change plus to list in menu (#1855) [Louis Laureys]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Added user request limits, We can now set the limit for a user. [tidusjar] - Added user request limits, We can now set the limit for a user. [tidusjar]
- Updated the UI JWT framework. [Jamie] - Updated the UI JWT framework. [Jamie]
@ -284,6 +394,270 @@
### **Fixes** ### **Fixes**
- New translations en.json (Norwegian) (#2020) [Jamie]
- Publish 32bit build of windows. [tidusjar]
- Fixing incorrect filter translation targets (#1987) [Jono Cairns]
- New Crowdin translations (#2017) [Jamie]
- Fixed #1997. [tidusjar]
- We now show the digital release date in the search if available #1962. [tidusjar]
- Css fixes (#2014) [Louis Laureys]
- API improvements. [Jamie]
- Fix #1599 (#2008) [Louis Laureys]
- Issue button fix (#2006) [Louis Laureys]
- Fixed #1886 #1865. [Jamie]
- Fixed the outstanding issue on #1995. [Jamie]
- Fixed an issue for #1951. [tidusjar]
- Try and fuzzy match the title and release if we cannot get the tvdb id or imdbid (depends on the media agents in Plex) #1951. [tidusjar]
- Fixed #1989 #1719. [Jamie]
- Small changes that might fix #1985 but doubt it. [Jamie]
- Should fix #1975. [tidusjar]
- Fixed #1789. [tidusjar]
- Fixed #1968. [tidusjar]
- Fixed #1978. [tidusjar]
- Fixed #1954. [tidusjar]
- Small changes to the auto updater, let's see how this works. [Jamie]
- Fixed build. [Jamie]
- Fixed the update check for the master build. [Jamie]
- Removed accidently merged files. [Jamie]
- Create CODE_OF_CONDUCT.md. [Jamie]
- Windows installation guide link update. [PotatoQuality]
- Fixed the issue comment issue #1914 also added another variable for issues {IssueUser} which is the user that reported the issue. [Jamie]
- Fix #1914. [tidusjar]
- Fixed #1914. [tidusjar]
- Fixed build and added logging. [TidusJar]
- New Crowdin translations (#1934) [Jamie]
- Potential fix for #1942. [Jamie]
- Quick change to the Emby Availability rule to make it in line slightly with the Plex one. #1950. [Jamie]
- Turn off mobile notifications. [tidusjar]
- FIXED PLEX!!!!! [tidusjar]
- Batch the PlexContentSync and increase the plex episode batch size. [tidusjar]
- Fixed the migration issue, it's too difficult to migrate the tables. [tidusjar]
- Fixed #1942. [tidusjar]
- Fixed checkboxes style. [Jamie]
- These are not the droids you are looking for. [Jamie]
- Fixed the wrong translation and see if we can VACUUM the db. [tidusjar]
- More translations and added a check on the baseurl to ensure it starts with a '/' [Jamie]
- More translations. [Jamie]
- Fixed #1878 and added a Request all button when selecting episodes. [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Italian) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Italian) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Italian) [Jamie]
- New translations en.json (German) [Jamie]
- New translations en.json (Dutch) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Danish) [Jamie]
- New translations en.json (Swedish) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- Working on the movie matching. Stop dupes #1869. [tidusjar]
- Delete plex episodes on every run due to a bug, need to spend quite a bit of time on this. [tidusjar]
- Fixed the issue where we were always adding emby episodes. Also fixed #1933. [tidusjar]
- New Crowdin translations (#1906) [Jamie]
- Add plain password for emby login (#1925) [dorian ALKOUM]
- Fixed #1924. [Jamie]
- Fixed the issue where I knocked out the ordering of notifications, oops. [tidusjar]
- #1914 for the issue resolved notification. [Jamie]
- #1916. [Jamie]
- Remove the placeholder. [Jamie]
- Feature arm (#1909) [Jamie]
- New Crowdin translations (#1897) [Jamie]
- Fix logo cut off on login screen (#1896) [Louis Laureys]
- E-Mails: Only add poster table row if img is set (#1899) [Louis Laureys]
- New Crowdin translations (#1884) [Jamie]
- Fix mobile layout (#1888) [Louis Laureys]
- Smal changes to the api. [tidusjar]
- OmBlur. [tidusjar]
- Hide the password field if it's not needed #1815. [Jamie]
- Should fix #1885. [Jamie]
- Make user management table responsive (#1882) [Louis Laureys]
- Fixed some stuff for omBlur. [Jamie]
- Some work... No one take a look at this, it's a suprise. [Jamie]
- New Crowdin translations (#1858) [Jamie]
- When requesting Anime, we now mark it correctly as Anime in Sonarr. [tidusjar]
- Fixed #1879 and added the spans. [tidusjar]
- Some work on the auto updater #1460. [tidusjar]
- Removed the potential locking. [tidusjar]
- Fixed #1863. [tidusjar]
- Moved the update check code from the External azure service into Ombi at /api/v1/update/BRANCH. [Jamie]
- Fixed the UI erroring out, also dont show tv with no externals. [tidusjar]
- More memory management and improvements. [tidusjar]
- These are not needed, added accidentally (#1860) [Louis Laureys]
- Some memory management improvements. [tidusjar]
- Fixed #1857. [tidusjar]
- Delete old v2 ombi from v3 branch. [tidusjar]
- New Crowdin translations (#1840) [Jamie]
- Better login backgrounds! (#1852) [Louis Laureys]
- Fixed #1851. [tidusjar]
- Fixed #1826. [tidusjar]
- Redo change #1848. [tidusjar]
- Fix the issue for welcome emails not sending. [tidusjar]
- Fix typo (#1845) [Kyle Lucy]
- Fix user mentions in Slack notifications (#1846) [Aljosa Asanovic]
- If Radarr/Sonarr has noticed that the media is available, then mark it as available in the UI. [Jamie]
- Fixed #1835. [Jamie]
- Enable Multi MIME and add alt tags to images (#1838) [Louis Laureys]
- New Crowdin translations (#1816) [Jamie]
- Fixed #1832. [tidusjar]
- Switch to use a single HTTPClient rather than a new one every request !dev. [tidusjar]
- Fix non-admin rights (#1820) [Rob Gökemeijer]
- Fix duplicated "Requests" element ID on new Issues link (#1817) [Shoghi Cervantes]
- Add the Issue Reporting functionality (#1811) [Jamie]
- Removed the forum. [tidusjar]
- #1659 Made the option to ignore notifcations for auto approve. [Jamie] - #1659 Made the option to ignore notifcations for auto approve. [Jamie]
- New Crowdin translations (#1806) [Jamie] - New Crowdin translations (#1806) [Jamie]

@ -9,17 +9,16 @@ ____
[![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi) [![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi)
[![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet) [![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet)
[![Patreon](https://www.ombi.io/img/patreondonate.svg)](https://patreon.com/tidusjar/Ombi)
[![Paypal](https://www.ombi.io/img/paypaldonate.svg)](https://paypal.me/PlexRequestsNet)
___ ___
[![Report a bug](http://i.imgur.com/xSpw482.png)](https://forums.ombi.io/viewforum.php?f=10) [![Feature request](http://i.imgur.com/mFO0OuX.png)](https://forums.ombi.io/posting.php?mode=post&f=20) [![Report a bug](http://i.imgur.com/xSpw482.png)](https://forums.ombi.io/viewforum.php?f=10) [![Feature request](http://i.imgur.com/mFO0OuX.png)](https://forums.ombi.io/posting.php?mode=post&f=20)
| Service | Master (V2) | Open Beta (V3 - Recommended) |
| Service | Stable | Develop |
|----------|:---------------------------:|:----------------------------:| |----------|:---------------------------:|:----------------------------:|
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/DotNetCore?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/DotNetCore) | | AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/develop?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop) |
| Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/DotNetCore/artifacts) | | Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop/artifacts) |
# Features # Features
Here are some of the features Ombi V3 has: Here are some of the features Ombi V3 has:
* Now working without crashes on Linux. * Now working without crashes on Linux.
@ -72,10 +71,9 @@ We are planning to bring back these features in V3 but for now you can find a li
| Login page | Yes (brand new) | Yes | | Login page | Yes (brand new) | Yes |
| Custom Notification Messages | Yes | No | | Custom Notification Messages | Yes | No |
| Sending newsletters | Planned | Yes | | Sending newsletters | Planned | Yes |
| Send a Mass Email | Planned | Yes | | Send a Mass Email | Yes | Yes |
| SickRage | Yes | Yes | | SickRage | Yes | Yes |
| CouchPotato | Yes | Yes | | CouchPotato | Yes | Yes |
| Watcher | Planned | Yes |
| DogNzb | Yes | No | | DogNzb | Yes | No |
| Issues | Yes | Yes | | Issues | Yes | Yes |
| Headphones | No (support dropped) | Yes | | Headphones | No (support dropped) | Yes |
@ -94,8 +92,9 @@ Search the existing requests to see if your suggestion has already been submitte
# Installation # Installation
[Click Here](https://github.com/tidusjar/Ombi/wiki/Installation) [Installation Guide](https://github.com/tidusjar/Ombi/wiki/Installation)
[Here for Reverse Proxy Config Examples](https://github.com/tidusjar/Ombi/wiki/Reverse-Proxy-Examples) [Here for Reverse Proxy Config Examples](https://github.com/tidusjar/Ombi/wiki/Reverse-Proxy-Examples)
[PlexGuide.com - Ombi Deployment & 101 Demonstration!](https://www.youtube.com/watch?v=QPNlqqkjNJw&feature=youtu.be)
# Contributors # Contributors

@ -25,6 +25,12 @@ after_build:
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm.tar.gz" appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm.tar.gz"
appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\windows-32bit.zip"
# appveyor PushArtifact "%APPVEYOR_BUILD_FOLDER%\src\Ombi\bin\Release\netcoreapp2.0\linux-arm64.tar.gz"

@ -3,7 +3,7 @@
#addin "Cake.Gulp" #addin "Cake.Gulp"
#addin "Cake.Npm" #addin "Cake.Npm"
#addin "SharpZipLib" #addin "SharpZipLib"
#addin "Cake.Compression" #addin nuget:?package=Cake.Compression&version=0.1.4
#addin "Cake.Incubator" #addin "Cake.Incubator"
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@ -26,26 +26,29 @@ var csProj = "./src/Ombi/Ombi.csproj"; // Path to the project.csproj
var solutionFile = "Ombi.sln"; // Solution file if needed var solutionFile = "Ombi.sln"; // Solution file if needed
GitVersion versionInfo = null; GitVersion versionInfo = null;
var frameworkVer = "netcoreapp2.0";
var buildSettings = new DotNetCoreBuildSettings var buildSettings = new DotNetCoreBuildSettings
{ {
Framework = "netcoreapp2.0", Framework = frameworkVer,
Configuration = "Release", Configuration = "Release",
OutputDirectory = Directory(buildDir), OutputDirectory = Directory(buildDir),
}; };
var publishSettings = new DotNetCorePublishSettings var publishSettings = new DotNetCorePublishSettings
{ {
Framework = "netcoreapp2.0", Framework = frameworkVer,
Configuration = "Release", Configuration = "Release",
OutputDirectory = Directory(buildDir), OutputDirectory = Directory(buildDir),
}; };
var artifactsFolder = buildDir + "/netcoreapp2.0/"; var artifactsFolder = buildDir + "/"+frameworkVer+"/";
var windowsArtifactsFolder = artifactsFolder + "win10-x64/published"; var windowsArtifactsFolder = artifactsFolder + "win10-x64/published";
var windows32BitArtifactsFolder = artifactsFolder + "win10-x32/published"; var windows32BitArtifactsFolder = artifactsFolder + "win10-x86/published";
var osxArtifactsFolder = artifactsFolder + "osx-x64/published"; var osxArtifactsFolder = artifactsFolder + "osx-x64/published";
var linuxArtifactsFolder = artifactsFolder + "linux-x64/published"; var linuxArtifactsFolder = artifactsFolder + "linux-x64/published";
var linuxArmArtifactsFolder = artifactsFolder + "linux-arm/published"; var linuxArmArtifactsFolder = artifactsFolder + "linux-arm/published";
var linuxArm64BitArtifactsFolder = artifactsFolder + "linux-arm64/published";
@ -104,6 +107,10 @@ Task("SetVersionInfo")
{ {
fullVer = fullVer.Replace("_",""); fullVer = fullVer.Replace("_","");
} }
if(fullVer.Contains("/"))
{
fullVer = fullVer.Replace("/","");
}
buildSettings.ArgumentCustomization = args => args.Append("/p:SemVer=" + versionInfo.AssemblySemVer); buildSettings.ArgumentCustomization = args => args.Append("/p:SemVer=" + versionInfo.AssemblySemVer);
buildSettings.ArgumentCustomization = args => args.Append("/p:FullVer=" + fullVer); buildSettings.ArgumentCustomization = args => args.Append("/p:FullVer=" + fullVer);
@ -161,35 +168,38 @@ Task("Package")
GZipCompress(osxArtifactsFolder, artifactsFolder + "osx.tar.gz"); GZipCompress(osxArtifactsFolder, artifactsFolder + "osx.tar.gz");
GZipCompress(linuxArtifactsFolder, artifactsFolder + "linux.tar.gz"); GZipCompress(linuxArtifactsFolder, artifactsFolder + "linux.tar.gz");
GZipCompress(linuxArmArtifactsFolder, artifactsFolder + "linux-arm.tar.gz"); GZipCompress(linuxArmArtifactsFolder, artifactsFolder + "linux-arm.tar.gz");
//GZipCompress(linuxArm64BitArtifactsFolder, artifactsFolder + "linux-arm64.tar.gz");
}); });
Task("Publish") Task("Publish")
.IsDependentOn("PrePublish") .IsDependentOn("PrePublish")
.IsDependentOn("Publish-Windows") .IsDependentOn("Publish-Windows")
.IsDependentOn("Publish-Windows-32bit")
.IsDependentOn("Publish-OSX") .IsDependentOn("Publish-OSX")
.IsDependentOn("Publish-Linux") .IsDependentOn("Publish-Linux")
.IsDependentOn("Publish-Linux-ARM") .IsDependentOn("Publish-Linux-ARM")
//.IsDependentOn("Publish-Linux-ARM-64Bit")
.IsDependentOn("Package"); .IsDependentOn("Package");
Task("Publish-Windows") Task("Publish-Windows")
.Does(() => .Does(() =>
{ {
publishSettings.Runtime = "win10-x64"; publishSettings.Runtime = "win10-x64";
publishSettings.OutputDirectory = Directory(buildDir) + Directory("netcoreapp2.0/win10-x64/published"); publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer +"/win10-x64/published");
DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings); DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings);
CopyFile(buildDir + "/netcoreapp2.0/win10-x64/Swagger.xml", buildDir + "/netcoreapp2.0/win10-x64/published/Swagger.xml"); CopyFile(buildDir + "/"+frameworkVer+"/win10-x64/Swagger.xml", buildDir + "/"+frameworkVer+"/win10-x64/published/Swagger.xml");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings); DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
}); });
Task("Publish-Windows-32bit") Task("Publish-Windows-32bit")
.Does(() => .Does(() =>
{ {
publishSettings.Runtime = "win10-x32"; publishSettings.Runtime = "win10-x86";
publishSettings.OutputDirectory = Directory(buildDir) + Directory("netcoreapp2.0/win10-x32/published"); publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer+"/win10-x86/published");
DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings); DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings);
CopyFile(buildDir + "/netcoreapp2.0/win10-x32/Swagger.xml", buildDir + "/netcoreapp2.0/win10-x32/published/Swagger.xml"); CopyFile(buildDir + "/"+frameworkVer+"/win10-x86/Swagger.xml", buildDir + "/"+frameworkVer+"/win10-x86/published/Swagger.xml");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings); DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
}); });
@ -197,10 +207,10 @@ Task("Publish-OSX")
.Does(() => .Does(() =>
{ {
publishSettings.Runtime = "osx-x64"; publishSettings.Runtime = "osx-x64";
publishSettings.OutputDirectory = Directory(buildDir) + Directory("netcoreapp2.0/osx-x64/published"); publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer+"/osx-x64/published");
DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings); DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings);
CopyFile(buildDir + "/netcoreapp2.0/osx-x64/Swagger.xml", buildDir + "/netcoreapp2.0/osx-x64/published/Swagger.xml"); CopyFile(buildDir + "/"+frameworkVer+"/osx-x64/Swagger.xml", buildDir + "/"+frameworkVer+"/osx-x64/published/Swagger.xml");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings); DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
}); });
@ -208,10 +218,10 @@ Task("Publish-Linux")
.Does(() => .Does(() =>
{ {
publishSettings.Runtime = "linux-x64"; publishSettings.Runtime = "linux-x64";
publishSettings.OutputDirectory = Directory(buildDir) + Directory("netcoreapp2.0/linux-x64/published"); publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer+"/linux-x64/published");
DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings); DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings);
CopyFile(buildDir + "/netcoreapp2.0/linux-x64/Swagger.xml", buildDir + "/netcoreapp2.0/linux-x64/published/Swagger.xml"); CopyFile(buildDir + "/"+frameworkVer+"/linux-x64/Swagger.xml", buildDir + "/"+frameworkVer+"/linux-x64/published/Swagger.xml");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings); DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
}); });
@ -219,12 +229,25 @@ Task("Publish-Linux-ARM")
.Does(() => .Does(() =>
{ {
publishSettings.Runtime = "linux-arm"; publishSettings.Runtime = "linux-arm";
publishSettings.OutputDirectory = Directory(buildDir) + Directory("netcoreapp2.0/linux-arm/published"); publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer+"/linux-arm/published");
DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings);
CopyFile(
buildDir + "/"+frameworkVer+"/linux-arm/Swagger.xml",
buildDir + "/"+frameworkVer+"/linux-arm/published/Swagger.xml");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
});
Task("Publish-Linux-ARM-64Bit")
.Does(() =>
{
publishSettings.Runtime = "linux-arm64";
publishSettings.OutputDirectory = Directory(buildDir) + Directory(frameworkVer+"/linux-arm64/published");
DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings); DotNetCorePublish("./src/Ombi/Ombi.csproj", publishSettings);
CopyFile( CopyFile(
buildDir + "/netcoreapp2.0/linux-arm/Swagger.xml", buildDir + "/"+frameworkVer+"/linux-arm64/Swagger.xml",
buildDir + "/netcoreapp2.0/linux-arm/published/Swagger.xml"); buildDir + "/"+frameworkVer+"/linux-arm64/published/Swagger.xml");
DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings); DotNetCorePublish("./src/Ombi.Updater/Ombi.Updater.csproj", publishSettings);
}); });

@ -23,7 +23,7 @@ namespace Ombi.Api.Plex.Models
public int leafCount { get; set; } public int leafCount { get; set; }
public int viewedLeafCount { get; set; } public int viewedLeafCount { get; set; }
public int childCount { get; set; } public int childCount { get; set; }
public int addedAt { get; set; } public long addedAt { get; set; }
public int updatedAt { get; set; } public int updatedAt { get; set; }
public Genre[] Genre { get; set; } public Genre[] Genre { get; set; }
//public Role[] Role { get; set; } //public Role[] Role { get; set; }

@ -0,0 +1,9 @@
using System;
namespace Ombi.Api.Radarr
{
public class CommandResult
{
public string name { get; set; }
}
}

@ -10,6 +10,9 @@ namespace Ombi.Api.Radarr
Task<List<RadarrProfile>> GetProfiles(string apiKey, string baseUrl); Task<List<RadarrProfile>> GetProfiles(string apiKey, string baseUrl);
Task<List<RadarrRootFolder>> GetRootFolders(string apiKey, string baseUrl); Task<List<RadarrRootFolder>> GetRootFolders(string apiKey, string baseUrl);
Task<SystemStatus> SystemStatus(string apiKey, string baseUrl); Task<SystemStatus> SystemStatus(string apiKey, string baseUrl);
Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl);
Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl);
Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl);
Task<RadarrAddMovieResponse> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability); Task<RadarrAddMovieResponse> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath,string apiKey, string baseUrl, bool searchNow, string minimumAvailability);
} }
} }

@ -17,10 +17,7 @@ namespace Ombi.Api.Radarr.Models
public bool monitored { get; set; } public bool monitored { get; set; }
public int tmdbId { get; set; } public int tmdbId { get; set; }
public List<string> images { get; set; } public List<string> images { get; set; }
public string cleanTitle { get; set; }
public string imdbId { get; set; }
public string titleSlug { get; set; } public string titleSlug { get; set; }
public int id { get; set; }
public int year { get; set; } public int year { get; set; }
public string minimumAvailability { get; set; } public string minimumAvailability { get; set; }
} }

@ -3,19 +3,10 @@
public class RadarrError public class RadarrError
{ {
public string message { get; set; } public string message { get; set; }
public string description { get; set; }
} }
public class RadarrErrorResponse public class RadarrErrorResponse
{ {
public string propertyName { get; set; }
public string errorMessage { get; set; } public string errorMessage { get; set; }
public object attemptedValue { get; set; }
public FormattedMessagePlaceholderValues formattedMessagePlaceholderValues { get; set; }
}
public class FormattedMessagePlaceholderValues
{
public string propertyName { get; set; }
public object propertyValue { get; set; }
} }
} }

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -53,6 +52,23 @@ namespace Ombi.Api.Radarr
return await Api.Request<List<MovieResponse>>(request); return await Api.Request<List<MovieResponse>>(request);
} }
public async Task<MovieResponse> GetMovie(int id, string apiKey, string baseUrl)
{
var request = new Request($"/api/movie/{id}", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return await Api.Request<MovieResponse>(request);
}
public async Task<MovieResponse> UpdateMovie(MovieResponse movie, string apiKey, string baseUrl)
{
var request = new Request($"/api/movie/", baseUrl, HttpMethod.Put);
AddHeaders(request, apiKey);
request.AddJsonBody(movie);
return await Api.Request<MovieResponse>(request);
}
public async Task<RadarrAddMovieResponse> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability) public async Task<RadarrAddMovieResponse> AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, string baseUrl, bool searchNow, string minimumAvailability)
{ {
var request = new Request("/api/movie", baseUrl, HttpMethod.Post); var request = new Request("/api/movie", baseUrl, HttpMethod.Post);
@ -66,7 +82,7 @@ namespace Ombi.Api.Radarr
titleSlug = title, titleSlug = title,
monitored = true, monitored = true,
year = year, year = year,
minimumAvailability = minimumAvailability, minimumAvailability = minimumAvailability
}; };
if (searchNow) if (searchNow)
@ -81,9 +97,9 @@ namespace Ombi.Api.Radarr
request.AddHeader("X-Api-Key", apiKey); request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options); request.AddJsonBody(options);
var response = await Api.RequestContent(request);
try try
{ {
var response = await Api.RequestContent(request);
if (response.Contains("\"message\":")) if (response.Contains("\"message\":"))
{ {
var error = JsonConvert.DeserializeObject<RadarrError>(response); var error = JsonConvert.DeserializeObject<RadarrError>(response);
@ -98,11 +114,24 @@ namespace Ombi.Api.Radarr
} }
catch (JsonSerializationException jse) catch (JsonSerializationException jse)
{ {
Logger.LogError(LoggingEvents.RadarrApi, jse, "Error When adding movie to Radarr"); Logger.LogError(LoggingEvents.RadarrApi, jse, "Error When adding movie to Radarr, Reponse: {0}", response);
} }
return null; return null;
} }
public async Task<bool> MovieSearch(int[] movieIds, string apiKey, string baseUrl)
{
var result = await Command(apiKey, baseUrl, new { name = "MoviesSearch", movieIds });
return result != null;
}
private async Task<CommandResult> Command(string apiKey, string baseUrl, object body)
{
var request = new Request($"/api/Command/", baseUrl, HttpMethod.Post);
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(body);
return await Api.Request<CommandResult>(request);
}
/// <summary> /// <summary>
/// Adds the required headers and also the authorization header /// Adds the required headers and also the authorization header

@ -8,11 +8,6 @@ namespace Ombi.Api.Sonarr.Models
public class CommandResult public class CommandResult
{ {
public string name { get; set; } public string name { get; set; }
public DateTime startedOn { get; set; }
public DateTime stateChangeTime { get; set; }
public bool sendUpdatesToClient { get; set; }
public string state { get; set; }
public int id { get; set; }
} }
} }

@ -66,8 +66,10 @@ namespace Ombi.Api.Sonarr
var request = new Request($"/api/series/{id}", baseUrl, HttpMethod.Get); var request = new Request($"/api/series/{id}", baseUrl, HttpMethod.Get);
request.AddHeader("X-Api-Key", apiKey); request.AddHeader("X-Api-Key", apiKey);
var result = await Api.Request<SonarrSeries>(request); var result = await Api.Request<SonarrSeries>(request);
result.seasons.ToList().RemoveAt(0); if (result?.seasons?.Length > 0)
{
result?.seasons?.ToList().RemoveAt(0);
}
return result; return result;
} }

@ -110,7 +110,8 @@ namespace Ombi.Core.Authentication
/// <returns></returns> /// <returns></returns>
private async Task<bool> CheckPlexPasswordAsync(OmbiUser user, string password) private async Task<bool> CheckPlexPasswordAsync(OmbiUser user, string password)
{ {
var result = await _plexApi.SignIn(new UserRequest { password = password, login = user.UserName }); var login = user.EmailLogin ? user.Email : user.UserName;
var result = await _plexApi.SignIn(new UserRequest { password = password, login = login });
if (result.user?.authentication_token != null) if (result.user?.authentication_token != null)
{ {
return true; return true;

@ -14,6 +14,8 @@ using Ombi.Store.Repository.Requests;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -24,14 +26,18 @@ namespace Ombi.Core.Engine
private Dictionary<int, TvRequests> _dbTv; private Dictionary<int, TvRequests> _dbTv;
protected BaseMediaEngine(IPrincipal identity, IRequestServiceMain requestService, protected BaseMediaEngine(IPrincipal identity, IRequestServiceMain requestService,
IRuleEvaluator rules, OmbiUserManager um) : base(identity, um, rules) IRuleEvaluator rules, OmbiUserManager um, ICacheService cache, ISettingsService<OmbiSettings> ombiSettings) : base(identity, um, rules)
{ {
RequestService = requestService; RequestService = requestService;
Cache = cache;
OmbiSettings = ombiSettings;
} }
protected IRequestServiceMain RequestService { get; } protected IRequestServiceMain RequestService { get; }
protected IMovieRequestRepository MovieRepository => RequestService.MovieRequestService; protected IMovieRequestRepository MovieRepository => RequestService.MovieRequestService;
protected ITvRequestRepository TvRepository => RequestService.TvRequestService; protected ITvRequestRepository TvRepository => RequestService.TvRequestService;
protected readonly ICacheService Cache;
protected readonly ISettingsService<OmbiSettings> OmbiSettings;
protected async Task<Dictionary<int, MovieRequests>> GetMovieRequests() protected async Task<Dictionary<int, MovieRequests>> GetMovieRequests()
{ {
@ -99,5 +105,30 @@ namespace Ombi.Core.Engine
Pending = pendingMovies + pendingTv Pending = pendingMovies + pendingTv
}; };
} }
protected async Task<HideResult> HideFromOtherUsers()
{
if (await IsInRole(OmbiRoles.Admin) || await IsInRole(OmbiRoles.PowerUser))
{
return new HideResult();
}
var settings = await Cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await OmbiSettings.GetSettingsAsync());
var result = new HideResult
{
Hide = settings.HideRequestsUsers
};
if (settings.HideRequestsUsers)
{
var user = await GetUser();
result.UserId = user.Id;
}
return result;
}
public class HideResult
{
public bool Hide { get; set; }
public string UserId { get; set; }
}
} }
} }

@ -1,4 +1,5 @@
using Ombi.Core.Rule; using System;
using Ombi.Core.Rule;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Helpers;
namespace Ombi.Core.Engine.Interfaces namespace Ombi.Core.Engine.Interfaces
{ {
@ -30,6 +32,13 @@ namespace Ombi.Core.Engine.Interfaces
private OmbiUser _user; private OmbiUser _user;
protected async Task<OmbiUser> GetUser() protected async Task<OmbiUser> GetUser()
{ {
if (IsApiUser)
{
return new OmbiUser
{
UserName = Username,
};
}
return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == Username)); return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == Username));
} }
@ -40,6 +49,10 @@ namespace Ombi.Core.Engine.Interfaces
protected async Task<bool> IsInRole(string roleName) protected async Task<bool> IsInRole(string roleName)
{ {
if (IsApiUser && roleName != OmbiRoles.Disabled)
{
return true;
}
return await UserManager.IsInRoleAsync(await GetUser(), roleName); return await UserManager.IsInRoleAsync(await GetUser(), roleName);
} }
@ -59,5 +72,7 @@ namespace Ombi.Core.Engine.Interfaces
var ruleResults = await Rules.StartSpecificRules(model, rule); var ruleResults = await Rules.StartSpecificRules(model, rule);
return ruleResults; return ruleResults;
} }
private bool IsApiUser => Username.Equals("Api", StringComparison.CurrentCultureIgnoreCase);
} }
} }

@ -17,5 +17,7 @@ namespace Ombi.Core
Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies(); Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies();
Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId); Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId);
Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId);
} }
} }

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

@ -10,9 +10,13 @@ namespace Ombi.Core.Engine.Interfaces
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> SearchTreeNode(string searchTerm); Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> SearchTreeNode(string searchTerm);
Task<TreeNode<SearchTvShowViewModel>> GetShowInformationTreeNode(int tvdbid); Task<TreeNode<SearchTvShowViewModel>> GetShowInformationTreeNode(int tvdbid);
Task<SearchTvShowViewModel> GetShowInformation(int tvdbid); Task<SearchTvShowViewModel> GetShowInformation(int tvdbid);
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Popular(); Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTree();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Anticipated(); Task<IEnumerable<SearchTvShowViewModel>> Popular();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatches(); Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTree();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Trending(); Task<IEnumerable<SearchTvShowViewModel>> Anticipated();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchesTree();
Task<IEnumerable<SearchTvShowViewModel>> MostWatches();
Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree();
Task<IEnumerable<SearchTvShowViewModel>> Trending();
} }
} }

@ -1,6 +1,5 @@
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Search;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using System; using System;
@ -11,9 +10,12 @@ using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -23,7 +25,7 @@ namespace Ombi.Core.Engine
{ {
public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user, public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log, INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log,
OmbiUserManager manager, IRepository<RequestLog> rl) : base(user, requestService, r, manager) OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache, ISettingsService<OmbiSettings> ombiSettings) : base(user, requestService, r, manager, cache, ombiSettings)
{ {
MovieApi = movieApi; MovieApi = movieApi;
NotificationHelper = helper; NotificationHelper = helper;
@ -45,7 +47,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<RequestEngineResult> RequestMovie(MovieRequestViewModel model) public async Task<RequestEngineResult> RequestMovie(MovieRequestViewModel model)
{ {
var movieInfo = await MovieApi.GetMovieInformation(model.TheMovieDbId); var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(model.TheMovieDbId);
if (movieInfo == null || movieInfo.Id == 0) if (movieInfo == null || movieInfo.Id == 0)
{ {
return new RequestEngineResult return new RequestEngineResult
@ -78,6 +80,9 @@ namespace Ombi.Core.Engine
Background = movieInfo.BackdropPath Background = movieInfo.BackdropPath
}; };
var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
requestModel.DigitalReleaseDate = usDates?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
var ruleResults = (await RunRequestRules(requestModel)).ToList(); var ruleResults = (await RunRequestRules(requestModel)).ToList();
if (ruleResults.Any(x => !x.Success)) if (ruleResults.Any(x => !x.Success))
{ {
@ -106,7 +111,7 @@ namespace Ombi.Core.Engine
return requestEngineResult; return requestEngineResult;
} }
// If there are no providers then it's successful but movie has not been sent // If there are no providers then it's successful but movie has not been sent
} }
@ -122,7 +127,16 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<MovieRequests>> GetRequests(int count, int position) public async Task<IEnumerable<MovieRequests>> GetRequests(int count, int position)
{ {
var allRequests = await MovieRepository.GetWithUser().Skip(position).Take(count).ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<MovieRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).Skip(position).Take(count).ToListAsync();
}
else
{
allRequests = await MovieRepository.GetWithUser().Skip(position).Take(count).ToListAsync();
}
allRequests.ForEach(x => allRequests.ForEach(x =>
{ {
x.PosterPath = PosterPathHelper.FixPosterPath(x.PosterPath); x.PosterPath = PosterPathHelper.FixPosterPath(x.PosterPath);
@ -136,7 +150,16 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<MovieRequests>> GetRequests() public async Task<IEnumerable<MovieRequests>> GetRequests()
{ {
var allRequests = await MovieRepository.GetWithUser().ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<MovieRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MovieRepository.GetWithUser().ToListAsync();
}
return allRequests; return allRequests;
} }
@ -147,7 +170,16 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<MovieRequests>> SearchMovieRequest(string search) public async Task<IEnumerable<MovieRequests>> SearchMovieRequest(string search)
{ {
var allRequests = await MovieRepository.GetWithUser().ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<MovieRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).ToListAsync();
}
else
{
allRequests = await MovieRepository.GetWithUser().ToListAsync();
}
var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList(); var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList();
results.ForEach(x => results.ForEach(x =>
{ {
@ -155,7 +187,7 @@ namespace Ombi.Core.Engine
}); });
return results; return results;
} }
public async Task<RequestEngineResult> ApproveMovieById(int requestId) public async Task<RequestEngineResult> ApproveMovieById(int requestId)
{ {
var request = await MovieRepository.Find(requestId); var request = await MovieRepository.Find(requestId);
@ -175,7 +207,7 @@ namespace Ombi.Core.Engine
request.Denied = true; request.Denied = true;
// We are denying a request // We are denying a request
NotificationHelper.Notify(request, NotificationType.RequestDeclined); NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MovieRepository.Update(request); await MovieRepository.Update(request);
return new RequestEngineResult return new RequestEngineResult
{ {
@ -335,9 +367,10 @@ namespace Ombi.Core.Engine
return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!" }; return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!" };
} }
public IEnumerable<MovieRequests> Filter(FilterViewModel vm) public async Task<IEnumerable<MovieRequests>> Filter(FilterViewModel vm)
{ {
var requests = MovieRepository.GetWithUser(); var shouldHide = await HideFromOtherUsers();
var requests = shouldHide.Hide ? MovieRepository.GetWithUser(shouldHide.UserId) : MovieRepository.GetWithUser();
switch (vm.AvailabilityFilter) switch (vm.AvailabilityFilter)
{ {
case FilterType.None: case FilterType.None:

@ -12,26 +12,26 @@ using System.Threading.Tasks;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
public class MovieSearchEngine : BaseMediaEngine, IMovieEngine public class MovieSearchEngine : BaseMediaEngine, IMovieEngine
{ {
public MovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, public MovieSearchEngine(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem) ILogger<MovieSearchEngine> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s)
: base(identity, service, r, um) : base(identity, service, r, um, mem, s)
{ {
MovieApi = movApi; MovieApi = movApi;
Mapper = mapper; Mapper = mapper;
Logger = logger; Logger = logger;
MemCache = mem;
} }
private IMovieDbApi MovieApi { get; } private IMovieDbApi MovieApi { get; }
private IMapper Mapper { get; } private IMapper Mapper { get; }
private ILogger<MovieSearchEngine> Logger { get; } private ILogger<MovieSearchEngine> Logger { get; }
private ICacheService MemCache { get; }
/// <summary> /// <summary>
/// Lookups the imdb information. /// Lookups the imdb information.
@ -40,7 +40,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId) public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId)
{ {
var movieInfo = await MovieApi.GetMovieInformationWithVideo(theMovieDbId); var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId);
var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo); var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo);
return await ProcessSingleMovie(viewMovie, true); return await ProcessSingleMovie(viewMovie, true);
@ -63,13 +63,29 @@ namespace Ombi.Core.Engine
return null; return null;
} }
/// <summary>
/// Get similar movies to the id passed in
/// </summary>
/// <param name="theMovieDbId"></param>
/// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> SimilarMovies(int theMovieDbId)
{
var result = await MovieApi.SimilarMovies(theMovieDbId);
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
return await TransformMovieResultsToResponse(result.Take(10)); // Take 10 to stop us overloading the API
}
return null;
}
/// <summary> /// <summary>
/// Gets popular movies. /// Gets popular movies.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies() public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => await MovieApi.PopularMovies(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -84,7 +100,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies() public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => await MovieApi.TopRated(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -99,7 +115,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies() public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => await MovieApi.Upcoming(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -114,7 +130,7 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies() public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => await MovieApi.NowPlaying(), DateTime.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -141,6 +157,8 @@ namespace Ombi.Core.Engine
var showInfo = await MovieApi.GetMovieInformation(viewMovie.Id); var showInfo = await MovieApi.GetMovieInformation(viewMovie.Id);
viewMovie.Id = showInfo.Id; // TheMovieDbId viewMovie.Id = showInfo.Id; // TheMovieDbId
viewMovie.ImdbId = showInfo.ImdbId; viewMovie.ImdbId = showInfo.ImdbId;
var usDates = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
viewMovie.DigitalReleaseDate = usDates?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
} }
viewMovie.TheMovieDbId = viewMovie.Id.ToString(); viewMovie.TheMovieDbId = viewMovie.Id.ToString();

@ -17,6 +17,8 @@ using Ombi.Core.Helpers;
using Ombi.Core.Rule; using Ombi.Core.Rule;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Senders; using Ombi.Core.Senders;
using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -26,7 +28,7 @@ namespace Ombi.Core.Engine
{ {
public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user, public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl) : base(user, requestService, rule, manager) ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache) : base(user, requestService, rule, manager, cache, settings)
{ {
TvApi = tvApi; TvApi = tvApi;
NotificationHelper = helper; NotificationHelper = helper;
@ -128,45 +130,136 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position) public async Task<IEnumerable<TvRequests>> GetRequests(int count, int position)
{ {
var allRequests = await TvRepository.Get() var shouldHide = await HideFromOtherUsers();
.Include(x => x.ChildRequests) List<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.Get(shouldHide.UserId)
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync();
// Filter out children
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = await TvRepository.Get()
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes) .ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync(); .Skip(position).Take(count).ToListAsync();
}
return allRequests; return allRequests;
} }
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetRequestsTreeNode(int count, int position) public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> GetRequestsTreeNode(int count, int position)
{ {
var allRequests = await TvRepository.Get() var shouldHide = await HideFromOtherUsers();
.Include(x => x.ChildRequests) List<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.Get(shouldHide.UserId)
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.SeasonRequests)
.ThenInclude(x=>x.Episodes) .ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync(); .Skip(position).Take(count).ToListAsync();
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = await TvRepository.Get()
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Skip(position).Take(count).ToListAsync();
}
return ParseIntoTreeNode(allRequests); return ParseIntoTreeNode(allRequests);
} }
public async Task<IEnumerable<TvRequests>> GetRequests() public async Task<IEnumerable<TvRequests>> GetRequests()
{ {
var allRequests = TvRepository.Get(); var shouldHide = await HideFromOtherUsers();
IQueryable<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = TvRepository.Get(shouldHide.UserId);
FilterChildren(allRequests, shouldHide);
}
else
{
allRequests = TvRepository.Get();
}
return await allRequests.ToListAsync(); return await allRequests.ToListAsync();
} }
private static void FilterChildren(IEnumerable<TvRequests> allRequests, HideResult shouldHide)
{
// Filter out children
foreach (var t in allRequests)
{
for (var j = 0; j < t.ChildRequests.Count; j++)
{
var child = t.ChildRequests[j];
if (child.RequestedUserId != shouldHide.UserId)
{
t.ChildRequests.RemoveAt(j);
j--;
}
}
}
}
public async Task<IEnumerable<ChildRequests>> GetAllChldren(int tvId) public async Task<IEnumerable<ChildRequests>> GetAllChldren(int tvId)
{ {
return await TvRepository.GetChild().Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync(); var shouldHide = await HideFromOtherUsers();
List<ChildRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = await TvRepository.GetChild(shouldHide.UserId).Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync();
}
else
{
allRequests = await TvRepository.GetChild().Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync();
}
return allRequests;
} }
public async Task<IEnumerable<TvRequests>> SearchTvRequest(string search) public async Task<IEnumerable<TvRequests>> SearchTvRequest(string search)
{ {
var allRequests = TvRepository.Get(); var shouldHide = await HideFromOtherUsers();
IQueryable<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = TvRepository.Get(shouldHide.UserId);
}
else
{
allRequests = TvRepository.Get();
}
var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync();
return results; return results;
} }
public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search) public async Task<IEnumerable<TreeNode<TvRequests, List<ChildRequests>>>> SearchTvRequestTree(string search)
{ {
var allRequests = TvRepository.Get(); var shouldHide = await HideFromOtherUsers();
IQueryable<TvRequests> allRequests;
if (shouldHide.Hide)
{
allRequests = TvRepository.Get(shouldHide.UserId);
}
else
{
allRequests = TvRepository.Get();
}
var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync();
return ParseIntoTreeNode(results); return ParseIntoTreeNode(results);
} }
@ -177,6 +270,13 @@ namespace Ombi.Core.Engine
var allRequests = TvRepository.Get(); var allRequests = TvRepository.Get();
var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id); var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id);
results.TvDbId = request.TvDbId;
results.ImdbId = request.ImdbId;
results.Overview = request.Overview;
results.PosterPath = PosterPathHelper.FixPosterPath(request.PosterPath);
results.QualityOverride = request.QualityOverride;
results.RootFolder = request.RootFolder;
await TvRepository.Update(results); await TvRepository.Update(results);
return results; return results;
} }
@ -395,7 +495,7 @@ namespace Ombi.Core.Engine
var result = await TvSender.Send(model); var result = await TvSender.Send(model);
if (result.Success) if (result.Success)
{ {
return new RequestEngineResult {Result = true}; return new RequestEngineResult { Result = true };
} }
return new RequestEngineResult return new RequestEngineResult
{ {
@ -411,7 +511,7 @@ namespace Ombi.Core.Engine
RequestType = RequestType.TvShow, RequestType = RequestType.TvShow,
}); });
return new RequestEngineResult {Result = true}; return new RequestEngineResult { Result = true };
} }
} }
} }

@ -19,6 +19,7 @@ using Ombi.Store.Repository.Requests;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Settings.Settings.Models;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -26,8 +27,8 @@ namespace Ombi.Core.Engine
{ {
public TvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService<PlexSettings> plexSettings, public TvSearchEngine(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ISettingsService<PlexSettings> plexSettings,
ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ISettingsService<EmbySettings> embySettings, IPlexContentRepository repo, IEmbyContentRepository embyRepo, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um,
ICacheService memCache) ICacheService memCache, ISettingsService<OmbiSettings> s)
: base(identity, service, r, um) : base(identity, service, r, um, memCache, s)
{ {
TvMazeApi = tvMaze; TvMazeApi = tvMaze;
Mapper = mapper; Mapper = mapper;
@ -36,7 +37,6 @@ namespace Ombi.Core.Engine
PlexContentRepo = repo; PlexContentRepo = repo;
TraktApi = trakt; TraktApi = trakt;
EmbyContentRepo = embyRepo; EmbyContentRepo = embyRepo;
MemCache = memCache;
} }
private ITvMazeApi TvMazeApi { get; } private ITvMazeApi TvMazeApi { get; }
@ -46,7 +46,6 @@ namespace Ombi.Core.Engine
private IPlexContentRepository PlexContentRepo { get; } private IPlexContentRepository PlexContentRepo { get; }
private IEmbyContentRepository EmbyContentRepo { get; } private IEmbyContentRepository EmbyContentRepo { get; }
private ITraktApi TraktApi { get; } private ITraktApi TraktApi { get; }
private ICacheService MemCache { get; }
public async Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm) public async Task<IEnumerable<SearchTvShowViewModel>> Search(string searchTerm)
{ {
@ -122,33 +121,61 @@ namespace Ombi.Core.Engine
return ParseIntoTreeNode(result); return ParseIntoTreeNode(result);
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Popular() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> PopularTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Anticipated() public async Task<IEnumerable<SearchTvShowViewModel>> Popular()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(), DateTime.Now.AddHours(12));
var processed= await ProcessResults(result); var processed = await ProcessResults(result);
return processed;
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> AnticipatedTree()
{
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated()
{
var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed;
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatches() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> MostWatchesTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> MostWatches()
{
var result = await Cache.GetOrAdd(CacheKeys.MostWatchesTv, async () => await TraktApi.GetMostWatchesShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed;
}
public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> Trending() public async Task<IEnumerable<TreeNode<SearchTvShowViewModel>>> TrendingTree()
{ {
var result = await MemCache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12)); var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result); var processed = await ProcessResults(result);
return processed.Select(ParseIntoTreeNode).ToList(); return processed.Select(ParseIntoTreeNode).ToList();
} }
public async Task<IEnumerable<SearchTvShowViewModel>> Trending()
{
var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(), DateTime.Now.AddHours(12));
var processed = await ProcessResults(result);
return processed;
}
private static TreeNode<SearchTvShowViewModel> ParseIntoTreeNode(SearchTvShowViewModel result) private static TreeNode<SearchTvShowViewModel> ParseIntoTreeNode(SearchTvShowViewModel result)
{ {
return new TreeNode<SearchTvShowViewModel> return new TreeNode<SearchTvShowViewModel>

@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Net.Mail;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Ombi.Core.Helpers namespace Ombi.Core.Helpers
@ -31,12 +32,11 @@ namespace Ombi.Core.Helpers
// Return true if strIn is in valid e-mail format. // Return true if strIn is in valid e-mail format.
try try
{ {
return Regex.IsMatch(strIn, // ReSharper disable once ObjectCreationAsStatement
@"^(?("")("".+?(?<!\\)""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))" + new MailAddress(strIn);
@"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9][\-a-z0-9]{0,22}[a-z0-9]))$", return true;
RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));
} }
catch (RegexMatchTimeoutException) catch (FormatException)
{ {
return false; return false;
} }

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ombi.Core.Models;
namespace Ombi.Core.Senders
{
public interface IMassEmailSender
{
Task<bool> SendMassEmail(MassEmailModel model);
}
}

@ -0,0 +1,40 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2018 Jamie Rees
// File: MassEmailModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using Ombi.Store.Entities;
namespace Ombi.Core.Models
{
public class MassEmailModel
{
public string Subject { get; set; }
public string Body { get; set; }
public List<OmbiUser> Users { get; set; }
}
}

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
namespace Ombi.Core.Models.Search namespace Ombi.Core.Models.Search
@ -25,5 +26,7 @@ namespace Ombi.Core.Models.Search
public int RootPathOverride { get; set; } public int RootPathOverride { get; set; }
public int QualityOverride { get; set; } public int QualityOverride { get; set; }
public override RequestType Type => RequestType.Movie; public override RequestType Type => RequestType.Movie;
public ReleaseDatesDto ReleaseDates { get; set; }
public DateTime? DigitalReleaseDate { get; set; }
} }
} }

@ -0,0 +1,95 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2018 Jamie Rees
// File: MassEmailSender.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Authentication;
using Ombi.Core.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Senders
{
public class MassEmailSender : IMassEmailSender
{
public MassEmailSender(IEmailProvider emailProvider, ISettingsService<CustomizationSettings> custom, ISettingsService<EmailNotificationSettings> email,
ILogger<MassEmailSender> log, OmbiUserManager manager)
{
_email = emailProvider;
_customizationService = custom;
_emailService = email;
_log = log;
_userManager = manager;
}
private readonly IEmailProvider _email;
private readonly ISettingsService<CustomizationSettings> _customizationService;
private readonly ISettingsService<EmailNotificationSettings> _emailService;
private readonly ILogger<MassEmailSender> _log;
private readonly OmbiUserManager _userManager;
public async Task<bool> SendMassEmail(MassEmailModel model)
{
var customization = await _customizationService.GetSettingsAsync();
var email = await _emailService.GetSettingsAsync();
var messagesSent = new List<Task>();
foreach (var user in model.Users)
{
var fullUser = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == user.Id);
if (!fullUser.Email.HasValue())
{
_log.LogInformation("User {0} has no email, cannot send mass email to this user", fullUser.UserName);
continue;
}
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.Setup(fullUser, customization);
var template = new NotificationTemplates() { Message = model.Body, Subject = model.Subject };
var content = resolver.ParseMessage(template, curlys);
var msg = new NotificationMessage
{
Message = content.Message,
To = fullUser.Email,
Subject = content.Subject
};
messagesSent.Add(_email.SendAdHoc(msg, email));
_log.LogInformation("Sent mass email to user {0} @ {1}", fullUser.UserName, fullUser.Email);
}
await Task.WhenAll(messagesSent);
return true;
}
}
}

@ -48,7 +48,7 @@ namespace Ombi.Core.Senders
var dogSettings = await DogNzbSettings.GetSettingsAsync(); var dogSettings = await DogNzbSettings.GetSettingsAsync();
if (dogSettings.Enabled) if (dogSettings.Enabled)
{ {
await SendToDogNzb(model,dogSettings); await SendToDogNzb(model, dogSettings);
return new SenderResult return new SenderResult
{ {
Success = true, Success = true,
@ -95,18 +95,40 @@ namespace Ombi.Core.Senders
} }
var rootFolderPath = model.RootPathOverride <= 0 ? settings.DefaultRootPath : await RadarrRootPath(model.RootPathOverride, settings); var rootFolderPath = model.RootPathOverride <= 0 ? settings.DefaultRootPath : await RadarrRootPath(model.RootPathOverride, settings);
var result = await RadarrApi.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, settings.MinimumAvailability);
if (!string.IsNullOrEmpty(result.Error?.message)) // Check if the movie already exists? Since it could be unmonitored
var movies = await RadarrApi.GetMovies(settings.ApiKey, settings.FullUri);
var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId);
if (existingMovie == null)
{ {
Log.LogError(LoggingEvents.RadarrCacher,result.Error.message); var result = await RadarrApi.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year,
return new SenderResult { Success = false, Message = result.Error.message, Sent = false }; qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly,
settings.MinimumAvailability);
if (!string.IsNullOrEmpty(result.Error?.message))
{
Log.LogError(LoggingEvents.RadarrCacher, result.Error.message);
return new SenderResult { Success = false, Message = result.Error.message, Sent = false };
}
if (!string.IsNullOrEmpty(result.title))
{
return new SenderResult { Success = true, Sent = false };
}
return new SenderResult { Success = true, Sent = false };
} }
if (!string.IsNullOrEmpty(result.title)) // We have the movie, check if we can request it or change the status
if (!existingMovie.monitored)
{ {
return new SenderResult { Success = true, Sent = false }; // let's set it to monitored and search for it
existingMovie.monitored = true;
await RadarrApi.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri);
// Search for it
await RadarrApi.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri);
return new SenderResult { Success = true, Sent = true };
} }
return new SenderResult { Success = true, Sent = false };
return new SenderResult { Success = false, Sent = false, Message = "Movie is already monitored" };
} }
private async Task<string> RadarrRootPath(int overrideId, RadarrSettings settings) private async Task<string> RadarrRootPath(int overrideId, RadarrSettings settings)

@ -105,9 +105,8 @@ namespace Ombi.Core.Senders
/// </summary> /// </summary>
/// <param name="s"></param> /// <param name="s"></param>
/// <param name="model"></param> /// <param name="model"></param>
/// <param name="qualityId">This is for any qualities overriden from the UI</param>
/// <returns></returns> /// <returns></returns>
public async Task<NewSeries> SendToSonarr(ChildRequests model, string qualityId = null) public async Task<NewSeries> SendToSonarr(ChildRequests model)
{ {
var s = await SonarrSettings.GetSettingsAsync(); var s = await SonarrSettings.GetSettingsAsync();
if (!s.Enabled) if (!s.Enabled)
@ -118,15 +117,12 @@ namespace Ombi.Core.Senders
{ {
return null; return null;
} }
var qualityProfile = 0;
if (!string.IsNullOrEmpty(qualityId)) // try to parse the passed in quality, otherwise use the settings default quality
{
int.TryParse(qualityId, out qualityProfile);
}
if (qualityProfile <= 0) int.TryParse(s.QualityProfile, out var qualityToUse);
if (model.ParentRequest.QualityOverride.HasValue)
{ {
int.TryParse(s.QualityProfile, out qualityProfile); qualityToUse = model.ParentRequest.QualityOverride.Value;
} }
// Get the root path from the rootfolder selected. // Get the root path from the rootfolder selected.
@ -151,7 +147,7 @@ namespace Ombi.Core.Senders
monitored = true, monitored = true,
seasonFolder = s.SeasonFolders, seasonFolder = s.SeasonFolders,
rootFolderPath = rootFolderPath, rootFolderPath = rootFolderPath,
qualityProfileId = qualityProfile, qualityProfileId = qualityToUse,
titleSlug = model.ParentRequest.Title, titleSlug = model.ParentRequest.Title,
addOptions = new AddOptions addOptions = new AddOptions
{ {

@ -81,6 +81,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMovieSender, MovieSender>(); services.AddTransient<IMovieSender, MovieSender>();
services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>(); services.AddTransient<IRecentlyAddedEngine, RecentlyAddedEngine>();
services.AddTransient<ITvSender, TvSender>(); services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
} }
public static void RegisterHttp(this IServiceCollection services) public static void RegisterHttp(this IServiceCollection services)
{ {

@ -9,7 +9,7 @@ namespace Ombi.Helpers
var version = Assembly.GetEntryAssembly() var version = Assembly.GetEntryAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>() .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion; .InformationalVersion;
return version.Equals("1.0.0") ? "3.0.0-DotNetCore" : version; return version.Equals("1.0.0") ? "3.0.0-develop" : version;
} }
} }
} }

@ -42,7 +42,19 @@ namespace Ombi.Mapping.Profiles
.ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.runtime)) .ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.runtime))
.ForMember(dest => dest.Status, opts => opts.MapFrom(src => src.status)) .ForMember(dest => dest.Status, opts => opts.MapFrom(src => src.status))
.ForMember(dest => dest.Tagline, opts => opts.MapFrom(src => src.tagline)) .ForMember(dest => dest.Tagline, opts => opts.MapFrom(src => src.tagline))
.ForMember(dest => dest.VoteCount, opts => opts.MapFrom(src => src.vote_count)); .ForMember(dest => dest.VoteCount, opts => opts.MapFrom(src => src.vote_count))
.ForMember(dest => dest.ReleaseDates, opts => opts.MapFrom(src => src.release_dates));
CreateMap<ReleaseDates, ReleaseDatesDto>()
.ForMember(x => x.Results, o => o.MapFrom(src => src.results));
CreateMap<ReleaseResults, ReleaseResultsDto>()
.ForMember(x => x.ReleaseDate, o => o.MapFrom(s => s.release_dates))
.ForMember(x => x.IsoCode, o => o.MapFrom(s => s.iso_3166_1));
CreateMap<ReleaseDate, ReleaseDateDto>()
.ForMember(x => x.ReleaseDate, o => o.MapFrom(s => s.release_date))
.ForMember(x => x.Type, o => o.MapFrom(s => s.Type));
CreateMap<Genre, GenreDto>(); CreateMap<Genre, GenreDto>();
CreateMap<MovieSearchResult, SearchMovieViewModel>().ReverseMap(); CreateMap<MovieSearchResult, SearchMovieViewModel>().ReverseMap();

@ -81,11 +81,14 @@ namespace Ombi.Notifications
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty; IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty; IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty; NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty;
IssueUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty; RequestedUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
} }
// User Defined // User Defined
public string RequestedUser { get; set; } public string RequestedUser { get; set; }
public string UserName => RequestedUser;
public string IssueUser => RequestedUser;
public string Title { get; set; } public string Title { get; set; }
public string RequestedDate { get; set; } public string RequestedDate { get; set; }
public string Type { get; set; } public string Type { get; set; }
@ -102,7 +105,6 @@ namespace Ombi.Notifications
public string IssueStatus { get; set; } public string IssueStatus { get; set; }
public string IssueSubject { get; set; } public string IssueSubject { get; set; }
public string NewIssueComment { get; set; } public string NewIssueComment { get; set; }
public string IssueUser { get; set; }
// System Defined // System Defined
private string LongDate => DateTime.Now.ToString("D"); private string LongDate => DateTime.Now.ToString("D");
@ -134,6 +136,7 @@ namespace Ombi.Notifications
{nameof(IssueSubject),IssueSubject}, {nameof(IssueSubject),IssueSubject},
{nameof(NewIssueComment),NewIssueComment}, {nameof(NewIssueComment),NewIssueComment},
{nameof(IssueUser),IssueUser}, {nameof(IssueUser),IssueUser},
{nameof(UserName),UserName},
}; };
} }
} }

@ -45,7 +45,7 @@ namespace Ombi.Notifications
private List<INotification> NotificationAgents { get; } private List<INotification> NotificationAgents { get; }
private ILogger<NotificationService> Log { get; } private ILogger<NotificationService> Log { get; }
/// <summary> /// <summary>^
/// Sends a notification to the user. This one is used in normal notification scenarios /// Sends a notification to the user. This one is used in normal notification scenarios
/// </summary> /// </summary>
/// <param name="model">The model.</param> /// <param name="model">The model.</param>

@ -94,6 +94,13 @@ namespace Ombi.Schedule.Jobs.Emby
var existingEmbyUser = allUsers.FirstOrDefault(x => x.ProviderUserId == embyUser.Id); var existingEmbyUser = allUsers.FirstOrDefault(x => x.ProviderUserId == embyUser.Id);
if (existingEmbyUser == null) if (existingEmbyUser == null)
{ {
if (!embyUser.ConnectUserName.HasValue() && !embyUser.Name.HasValue())
{
_log.LogInformation("Could not create Emby user since the have no username, PlexUserId: {0}", embyUser.Id);
continue;
}
// Create this users // Create this users
// We do not store a password against the user since they will authenticate via Plex // We do not store a password against the user since they will authenticate via Plex
var newUser = new OmbiUser var newUser = new OmbiUser

@ -106,14 +106,23 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
// Let's download the correct zip // Let's download the correct zip
var desc = RuntimeInformation.OSDescription; var desc = RuntimeInformation.OSDescription;
var proce = RuntimeInformation.ProcessArchitecture; var process = RuntimeInformation.ProcessArchitecture;
Logger.LogDebug(LoggingEvents.Updater, "OS Information: {0} {1}", desc, proce); Logger.LogDebug(LoggingEvents.Updater, "OS Information: {0} {1}", desc, process);
Downloads download; Downloads download;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
Logger.LogDebug(LoggingEvents.Updater, "We are Windows"); Logger.LogDebug(LoggingEvents.Updater, "We are Windows");
download = updates.Downloads.FirstOrDefault(x => x.Name.Contains("windows.zip", CompareOptions.IgnoreCase)); if (process == Architecture.X64)
{
download = updates.Downloads.FirstOrDefault(x =>
x.Name.Contains("windows.", CompareOptions.IgnoreCase));
}
else
{
download = updates.Downloads.FirstOrDefault(x =>
x.Name.Contains("windows-32bit", CompareOptions.IgnoreCase));
}
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
@ -123,13 +132,16 @@ namespace Ombi.Schedule.Jobs.Ombi
else else
{ {
Logger.LogDebug(LoggingEvents.Updater, "We are linux"); Logger.LogDebug(LoggingEvents.Updater, "We are linux");
if (RuntimeInformation.OSDescription.Contains("arm", CompareOptions.IgnoreCase)) if (process == Architecture.Arm)
{
download = updates.Downloads.FirstOrDefault(x => x.Name.Contains("arm.", CompareOptions.IgnoreCase));
} else if (process == Architecture.Arm64)
{ {
download = updates.Downloads.FirstOrDefault(x => x.Name.Contains("arm", CompareOptions.IgnoreCase)); download = updates.Downloads.FirstOrDefault(x => x.Name.Contains("arm64.", CompareOptions.IgnoreCase));
} }
else else
{ {
download = updates.Downloads.FirstOrDefault(x => x.Name.Contains("linux", CompareOptions.IgnoreCase)); download = updates.Downloads.FirstOrDefault(x => x.Name.Contains("linux.", CompareOptions.IgnoreCase));
} }
} }
if (download == null) if (download == null)
@ -247,24 +259,16 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append($"--windowsServiceName \"{settings.WindowsServiceName}\" "); sb.Append($"--windowsServiceName \"{settings.WindowsServiceName}\" ");
} }
var sb2 = new StringBuilder(); var sb2 = new StringBuilder();
var hasStartupArgs = false;
if (url?.Value.HasValue() ?? false) if (url?.Value.HasValue() ?? false)
{ {
hasStartupArgs = true; sb2.Append($" --host {url.Value}");
sb2.Append(url.Value);
} }
if (storage?.Value.HasValue() ?? false) if (storage?.Value.HasValue() ?? false)
{ {
hasStartupArgs = true; sb2.Append($" --storage {storage.Value}");
sb2.Append(storage.Value);
}
if (hasStartupArgs)
{
sb.Append($"--startupArgs {sb2.ToString()}");
} }
return sb.ToString(); return sb.ToString();
//return string.Join(" ", currentLocation, processName, url?.Value ?? string.Empty, storage?.Value ?? string.Empty);
} }
private void RunScript(UpdateSettings settings, string downloadUrl) private void RunScript(UpdateSettings settings, string downloadUrl)

@ -67,6 +67,16 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
seriesEpisodes = plexEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString()); seriesEpisodes = plexEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString());
} }
if (!seriesEpisodes.Any())
{
// Let's try and match the series by name
seriesEpisodes = plexEpisodes.Where(x =>
x.Series.Title.Equals(child.Title, StringComparison.CurrentCultureIgnoreCase) &&
x.Series.ReleaseYear == child.ParentRequest.ReleaseDate.Year.ToString());
}
foreach (var season in child.SeasonRequests) foreach (var season in child.SeasonRequests)
{ {
foreach (var episode in season.Episodes) foreach (var episode in season.Episodes)

@ -30,6 +30,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Hangfire; using Hangfire;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
@ -81,7 +82,7 @@ namespace Ombi.Schedule.Jobs.Plex
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogWarning(LoggingEvents.Cacher, e, "Exception thrown when attempting to cache the Plex Content"); Logger.LogWarning(LoggingEvents.PlexContentCacher, e, "Exception thrown when attempting to cache the Plex Content");
} }
Logger.LogInformation("Starting EP Cacher"); Logger.LogInformation("Starting EP Cacher");
@ -92,91 +93,145 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
foreach (var servers in plexSettings.Servers ?? new List<PlexServers>()) foreach (var servers in plexSettings.Servers ?? new List<PlexServers>())
{ {
try
{
Logger.LogInformation("Starting to cache the content on server {0}", servers.Name);
await ProcessServer(servers);
}
catch (Exception e)
{
Logger.LogWarning(LoggingEvents.PlexContentCacher, e, "Exception thrown when attempting to cache the Plex Content in server {0}", servers.Name);
}
}
}
Logger.LogInformation("Getting all content from server {0}", servers.Name); private async Task ProcessServer(PlexServers servers)
var allContent = await GetAllContent(servers); {
Logger.LogInformation("We found {0} items", allContent.Count); Logger.LogInformation("Getting all content from server {0}", servers.Name);
var allContent = await GetAllContent(servers);
Logger.LogInformation("We found {0} items", allContent.Count);
// Let's now process this. // Let's now process this.
var contentToAdd = new List<PlexServerContent>(); var contentToAdd = new HashSet<PlexServerContent>();
foreach (var content in allContent) foreach (var content in allContent)
{
if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase))
{ {
if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)) // Process Shows
Logger.LogInformation("Processing TV Shows");
foreach (var show in content.Metadata ?? new Metadata[] { })
{ {
// Process Shows var seasonList = await PlexApi.GetSeasons(servers.PlexAuthToken, servers.FullUri,
Logger.LogInformation("Processing TV Shows"); show.ratingKey);
foreach (var show in content.Metadata ?? new Metadata[] { }) var seasonsContent = new List<PlexSeasonsContent>();
foreach (var season in seasonList.MediaContainer.Metadata)
{ {
var seasonList = await PlexApi.GetSeasons(servers.PlexAuthToken, servers.FullUri, seasonsContent.Add(new PlexSeasonsContent
show.ratingKey);
var seasonsContent = new List<PlexSeasonsContent>();
foreach (var season in seasonList.MediaContainer.Metadata)
{ {
seasonsContent.Add(new PlexSeasonsContent ParentKey = season.parentRatingKey,
{ SeasonKey = season.ratingKey,
ParentKey = season.parentRatingKey, SeasonNumber = season.index,
SeasonKey = season.ratingKey, PlexContentId = show.ratingKey
SeasonNumber = season.index, });
PlexContentId = show.ratingKey }
});
// Do we already have this item?
// Let's try and match
var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title
&& x.ReleaseYear == show.year.ToString()
&& x.Type == PlexMediaTypeEntity.Show);
// Just double check the rating key, since this is our unique constraint
var existingKey = await Repo.GetByKey(show.ratingKey);
if (existingKey != null)
{
// Damn son.
// Let's check if they match up
var doesMatch = show.title.Equals(existingKey.Title,
StringComparison.CurrentCulture);
if (!doesMatch)
{
// Something fucked up on Plex at somepoint... Damn, rebuild of lib maybe?
// Lets delete the matching key
await Repo.Delete(existingKey);
existingKey = null;
} }
}
// Do we already have this item?
// Let's try and match if (existingContent != null)
var existingContent = await Repo.GetFirstContentByCustom(x => x.Title == show.title {
&& x.ReleaseYear == show.year.ToString() // Just check the key
&& x.Type == PlexMediaTypeEntity.Show); if (existingKey != null)
{
if (existingContent == null) // The rating key is all good!
}
else
{ {
// Just check the key // This means the rating key has changed somehow.
var hasSameKey = await Repo.GetByKey(show.ratingKey); // Should probably delete this and get the new one
if (hasSameKey != null) var oldKey = existingContent.Key;
Repo.DeleteWithoutSave(existingContent);
// Because we have changed the rating key, we need to change all children too
var episodeToChange = Repo.GetAllEpisodes().Where(x => x.GrandparentKey == oldKey);
if (episodeToChange.Any())
{ {
existingContent = hasSameKey; foreach (var e in episodeToChange)
{
Repo.DeleteWithoutSave(e);
}
} }
await Repo.SaveChangesAsync();
existingContent = null;
} }
// The ratingKey keeps changing... }
//var existingContent = await Repo.GetByKey(show.ratingKey); // The ratingKey keeps changing...
if (existingContent != null) //var existingContent = await Repo.GetByKey(show.ratingKey);
if (existingContent != null)
{
try
{ {
try Logger.LogInformation("We already have show {0} checking for new seasons",
existingContent.Title);
// Ok so we have it, let's check if there are any new seasons
var itemAdded = false;
foreach (var season in seasonsContent)
{ {
Logger.LogInformation("We already have show {0} checking for new seasons", existingContent.Title); var seasonExists =
// Ok so we have it, let's check if there are any new seasons existingContent.Seasons.FirstOrDefault(x => x.SeasonKey == season.SeasonKey);
var itemAdded = false;
foreach (var season in seasonsContent)
{
var seasonExists = existingContent.Seasons.FirstOrDefault(x => x.SeasonKey == season.SeasonKey);
if (seasonExists != null) if (seasonExists != null)
{ {
// We already have this season // We already have this season
continue; continue;
}
existingContent.Seasons.Add(season);
itemAdded = true;
} }
if (itemAdded) await Repo.Update(existingContent); existingContent.Seasons.Add(season);
} itemAdded = true;
catch (Exception e)
{
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new seasons to title {0}", existingContent.Title);
} }
if (itemAdded) await Repo.Update(existingContent);
} }
else catch (Exception e)
{
Logger.LogError(LoggingEvents.PlexContentCacher, e,
"Exception when adding new seasons to title {0}", existingContent.Title);
}
}
else
{
try
{ {
Logger.LogInformation("New show {0}, so add it", show.title); Logger.LogInformation("New show {0}, so add it", show.title);
// Get the show metadata... This sucks since the `metadata` var contains all information about the show // Get the show metadata... This sucks since the `metadata` var contains all information about the show
// But it does not contain the `guid` property that we need to pull out thetvdb id... // But it does not contain the `guid` property that we need to pull out thetvdb id...
var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri, var showMetadata = await PlexApi.GetMetadata(servers.PlexAuthToken, servers.FullUri,
show.ratingKey); show.ratingKey);
var providerIds = PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault().guid); var providerIds =
PlexHelper.GetProviderIdFromPlexGuid(showMetadata.MediaContainer.Metadata.FirstOrDefault()
.guid);
var item = new PlexServerContent var item = new PlexServerContent
{ {
@ -201,22 +256,60 @@ namespace Ombi.Schedule.Jobs.Plex
item.TvDbId = providerIds.TheTvDb; item.TvDbId = providerIds.TheTvDb;
} }
// Let's just double check to make sure we do not have it now we have some id's
var existingImdb = false;
var existingMovieDbId = false;
var existingTvDbId = false;
if (item.ImdbId.HasValue())
{
existingImdb = await Repo.GetAll().AnyAsync(x =>
x.ImdbId == item.ImdbId && x.Type == PlexMediaTypeEntity.Show);
}
if (item.TheMovieDbId.HasValue())
{
existingMovieDbId = await Repo.GetAll().AnyAsync(x =>
x.TheMovieDbId == item.TheMovieDbId && x.Type == PlexMediaTypeEntity.Show);
}
if (item.TvDbId.HasValue())
{
existingTvDbId = await Repo.GetAll().AnyAsync(x =>
x.TvDbId == item.TvDbId && x.Type == PlexMediaTypeEntity.Show);
}
if (existingImdb || existingTvDbId || existingMovieDbId)
{
// We already have it!
continue;
}
item.Seasons.ToList().AddRange(seasonsContent); item.Seasons.ToList().AddRange(seasonsContent);
contentToAdd.Add(item); contentToAdd.Add(item);
} }
catch (Exception e)
{
Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding tv show {0}",
show.title);
}
}
if (contentToAdd.Count > 500)
{
await Repo.AddRange(contentToAdd);
contentToAdd.Clear();
} }
} }
if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)) }
if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase))
{
Logger.LogInformation("Processing Movies");
foreach (var movie in content?.Metadata ?? new Metadata[] { })
{ {
Logger.LogInformation("Processing Movies"); // Let's check if we have this movie
foreach (var movie in content?.Metadata ?? new Metadata[] { })
{
// Let's check if we have this movie
try
{
var existing = await Repo.GetFirstContentByCustom(x => x.Title == movie.title var existing = await Repo.GetFirstContentByCustom(x => x.Title == movie.title
&& x.ReleaseYear == movie.year.ToString() && x.ReleaseYear == movie.year.ToString()
&& x.Type == PlexMediaTypeEntity.Movie); && x.Type == PlexMediaTypeEntity.Movie);
// The rating key keeps changing // The rating key keeps changing
//var existing = await Repo.GetByKey(movie.ratingKey); //var existing = await Repo.GetByKey(movie.ratingKey);
if (existing != null) if (existing != null)
@ -263,19 +356,29 @@ namespace Ombi.Schedule.Jobs.Plex
} }
contentToAdd.Add(item); contentToAdd.Add(item);
} }
} catch (Exception e)
if (contentToAdd.Count > 500) {
{ Logger.LogError(LoggingEvents.PlexContentCacher, e, "Exception when adding new Movie {0}",
await Repo.AddRange(contentToAdd); movie.title);
contentToAdd = new List<PlexServerContent>(); }
if (contentToAdd.Count > 500)
{
await Repo.AddRange(contentToAdd);
contentToAdd.Clear();
}
} }
} }
if (contentToAdd.Count > 500)
if (contentToAdd.Any())
{ {
await Repo.AddRange(contentToAdd); await Repo.AddRange(contentToAdd);
contentToAdd.Clear();
} }
}
if (contentToAdd.Any())
{
await Repo.AddRange(contentToAdd);
} }
} }

@ -48,8 +48,9 @@ namespace Ombi.Schedule.Jobs.Plex
foreach (var server in s.Servers) foreach (var server in s.Servers)
{ {
await Cache(server); await Cache(server);
BackgroundJob.Enqueue(() => _availabilityChecker.Start());
} }
BackgroundJob.Enqueue(() => _availabilityChecker.Start());
} }
catch (Exception e) catch (Exception e)
{ {
@ -61,7 +62,6 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
if (!Validate(settings)) if (!Validate(settings))
{ {
_log.LogWarning("Validation failed"); _log.LogWarning("Validation failed");
return; return;
} }
@ -100,21 +100,25 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
var currentPosition = 0; var currentPosition = 0;
var resultCount = settings.EpisodeBatchSize == 0 ? 150 : settings.EpisodeBatchSize; var resultCount = settings.EpisodeBatchSize == 0 ? 150 : settings.EpisodeBatchSize;
var currentEpisodes = _repo.GetAllEpisodes();
var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount); var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount);
_log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}"); _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}");
// Delete all the episodes because we cannot uniquly match an episode to series every time, // Delete all the episodes because we cannot uniquly match an episode to series every time,
// see comment below. // see comment below.
await _repo.ExecuteSql("DELETE FROM PlexEpisode");
await ProcessEpsiodes(episodes); // 12.03.2017 - I think we should be able to match them now
//await _repo.ExecuteSql("DELETE FROM PlexEpisode");
await ProcessEpsiodes(episodes, currentEpisodes);
currentPosition += resultCount; currentPosition += resultCount;
while (currentPosition < episodes.MediaContainer.totalSize) while (currentPosition < episodes.MediaContainer.totalSize)
{ {
var ep = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, var ep = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition,
resultCount); resultCount);
await ProcessEpsiodes(ep);
await ProcessEpsiodes(ep, currentEpisodes);
_log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {resultCount} more episodes. Total Remaining {episodes.MediaContainer.totalSize - currentPosition}"); _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {resultCount} more episodes. Total Remaining {episodes.MediaContainer.totalSize - currentPosition}");
currentPosition += resultCount; currentPosition += resultCount;
} }
@ -124,43 +128,69 @@ namespace Ombi.Schedule.Jobs.Plex
await _repo.SaveChangesAsync(); await _repo.SaveChangesAsync();
} }
private async Task ProcessEpsiodes(PlexContainer episodes) private async Task ProcessEpsiodes(PlexContainer episodes, IQueryable<PlexEpisode> currentEpisodes)
{ {
var ep = new HashSet<PlexEpisode>(); var ep = new HashSet<PlexEpisode>();
try
foreach (var episode in episodes?.MediaContainer?.Metadata ?? new Metadata[]{})
{ {
// I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes? foreach (var episode in episodes?.MediaContainer?.Metadata ?? new Metadata[] { })
// We have the parent and grandparent rating keys to link up to the season and series
//var metadata = _api.GetEpisodeMetaData(server.PlexAuthToken, server.FullUri, episode.ratingKey);
// This does seem to work, it looks like we can somehow get different rating, grandparent and parent keys with episodes. Not sure how.
//var epExists = currentEpisodes.Any(x => episode.ratingKey == x.Key &&
// episode.grandparentRatingKey == x.GrandparentKey);
//if (epExists)
//{
// continue;
//}
ep.Add(new PlexEpisode
{ {
EpisodeNumber = episode.index, // I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes?
SeasonNumber = episode.parentIndex, // We have the parent and grandparent rating keys to link up to the season and series
GrandparentKey = episode.grandparentRatingKey, //var metadata = _api.GetEpisodeMetaData(server.PlexAuthToken, server.FullUri, episode.ratingKey);
ParentKey = episode.parentRatingKey,
Key = episode.ratingKey, // This does seem to work, it looks like we can somehow get different rating, grandparent and parent keys with episodes. Not sure how.
Title = episode.title var epExists = currentEpisodes.Any(x => episode.ratingKey == x.Key &&
}); episode.grandparentRatingKey == x.GrandparentKey);
} if (epExists)
{
continue;
}
await _repo.AddRange(ep); // Let's check if we have the parent
var seriesExists = await _repo.GetByKey(episode.grandparentRatingKey);
if (seriesExists == null)
{
// Ok let's try and match it to a title. TODO (This is experimental)
seriesExists = await _repo.GetAll().FirstOrDefaultAsync(x =>
x.Title.Equals(episode.grandparentTitle, StringComparison.CurrentCultureIgnoreCase));
if (seriesExists == null)
{
_log.LogWarning(
"The episode title {0} we cannot find the parent series. The episode grandparentKey = {1}, grandparentTitle = {2}",
episode.title, episode.grandparentRatingKey, episode.grandparentTitle);
continue;
}
// Set the rating key to the correct one
episode.grandparentRatingKey = seriesExists.Key;
}
ep.Add(new PlexEpisode
{
EpisodeNumber = episode.index,
SeasonNumber = episode.parentIndex,
GrandparentKey = episode.grandparentRatingKey,
ParentKey = episode.parentRatingKey,
Key = episode.ratingKey,
Title = episode.title
});
}
await _repo.AddRange(ep);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
} }
private bool Validate(PlexServers settings) private bool Validate(PlexServers settings)
{ {
if (string.IsNullOrEmpty(settings.PlexAuthToken)) if (string.IsNullOrEmpty(settings.PlexAuthToken))
{ {
return false ; return false;
} }
return true; return true;

@ -75,6 +75,12 @@ namespace Ombi.Schedule.Jobs.Plex
var existingPlexUser = allUsers.FirstOrDefault(x => x.ProviderUserId == plexUser.Id); var existingPlexUser = allUsers.FirstOrDefault(x => x.ProviderUserId == plexUser.Id);
if (existingPlexUser == null) if (existingPlexUser == null)
{ {
if (!plexUser.Username.HasValue())
{
_log.LogInformation("Could not create Plex user since the have no username, PlexUserId: {0}", plexUser.Id);
continue;
}
// Create this users // Create this users
// We do not store a password against the user since they will authenticate via Plex // We do not store a password against the user since they will authenticate via Plex
var newUser = new OmbiUser var newUser = new OmbiUser

@ -9,6 +9,7 @@ namespace Ombi.Settings.Settings.Models
{ {
public string ApplicationName { get; set; } public string ApplicationName { get; set; }
public string ApplicationUrl { get; set; } public string ApplicationUrl { get; set; }
public bool Mobile { get; set; }
public string CustomCssLink { get; set; } public string CustomCssLink { get; set; }
public bool EnableCustomDonations { get; set; } public bool EnableCustomDonations { get; set; }
public string CustomDonationUrl { get; set; } public string CustomDonationUrl { get; set; }

@ -7,7 +7,8 @@
public bool Wizard { get; set; } public bool Wizard { get; set; }
public string ApiKey { get; set; } public string ApiKey { get; set; }
public bool IgnoreCertificateErrors { get; set; } public bool IgnoreCertificateErrors { get; set; }
public bool DoNotSendNotificationsForAutoApprove {get;set;} public bool DoNotSendNotificationsForAutoApprove { get; set; }
public bool HideRequestsUsers { get; set; }
} }
} }

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using Ombi.Helpers; using Ombi.Helpers;
namespace Ombi.Store.Entities namespace Ombi.Store.Entities
@ -32,5 +33,29 @@ namespace Ombi.Store.Entities
[NotMapped] [NotMapped]
public string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias; public string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias;
[NotMapped]
public bool EmailLogin { get; set; }
[JsonIgnore]
public override string PasswordHash
{
get => base.PasswordHash;
set => base.PasswordHash = value;
}
[JsonIgnore]
public override string SecurityStamp
{
get => base.SecurityStamp;
set => base.SecurityStamp = value;
}
[JsonIgnore]
public override string ConcurrencyStamp
{
get => base.ConcurrencyStamp;
set => base.ConcurrencyStamp = value;
}
} }
} }

@ -10,11 +10,14 @@ namespace Ombi.Store.Entities.Requests
public string Overview { get; set; } public string Overview { get; set; }
public string PosterPath { get; set; } public string PosterPath { get; set; }
public DateTime ReleaseDate { get; set; } public DateTime ReleaseDate { get; set; }
public DateTime? DigitalReleaseDate { get; set; }
public string Status { get; set; } public string Status { get; set; }
public string Background { get; set; } public string Background { get; set; }
[NotMapped] [NotMapped]
public bool Released => DateTime.UtcNow > ReleaseDate; public bool Released => DateTime.UtcNow > ReleaseDate;
[NotMapped]
public bool DigitalRelease => DigitalReleaseDate.HasValue && DigitalReleaseDate > DateTime.UtcNow;
} }
} }

@ -8,6 +8,7 @@ namespace Ombi.Store.Entities.Requests
{ {
public int TvDbId { get; set; } public int TvDbId { get; set; }
public string ImdbId { get; set; } public string ImdbId { get; set; }
public int? QualityOverride { get; set; }
public int? RootFolder { get; set; } public int? RootFolder { get; set; }
public string Overview { get; set; } public string Overview { get; set; }
public string Title { get; set; } public string Title { get; set; }

@ -0,0 +1 @@
dotnet ef migrations add Inital --context OmbiContext --startup-project ../Ombi/Ombi.csproj

@ -0,0 +1,916 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20180228114507_DigitalRelease")]
partial class DigitalRelease
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Type");
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("ApplicationConfiguration");
});
modelBuilder.Entity("Ombi.Store.Entities.Audit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AuditArea");
b.Property<int>("AuditType");
b.Property<DateTime>("DateTime");
b.Property<string>("Description");
b.Property<string>("User");
b.HasKey("Id");
b.ToTable("Audit");
});
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId")
.IsRequired();
b.Property<string>("ProviderId");
b.Property<string>("Title");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId");
b.Property<int>("EpisodeNumber");
b.Property<string>("ParentId");
b.Property<string>("ProviderId");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Content");
b.Property<string>("SettingsName");
b.HasKey("Id");
b.ToTable("GlobalSettings");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Agent");
b.Property<bool>("Enabled");
b.Property<string>("Message");
b.Property<int>("NotificationType");
b.Property<string>("Subject");
b.HasKey("Id");
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("PlayerId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("NotificationUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("Alias");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<string>("EmbyConnectUserId");
b.Property<int?>("EpisodeRequestLimit");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<int?>("MovieRequestLimit");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("ProviderUserId");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserAccessToken");
b.Property<string>("UserName")
.HasMaxLength(256);
b.Property<int>("UserType");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("GrandparentKey");
b.Property<int>("Key");
b.Property<int>("ParentKey");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ParentKey");
b.Property<int>("PlexContentId");
b.Property<int?>("PlexServerContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("ImdbId");
b.Property<int>("Key");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("TheMovieDbId");
b.Property<string>("Title");
b.Property<string>("TvDbId");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("HasFile");
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<int?>("IssueId");
b.Property<int>("ParentRequestId");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("SeriesType");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentRequestId");
b.HasIndex("RequestedUserId");
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("IssueCategory");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Comment");
b.Property<DateTime>("Date");
b.Property<int?>("IssuesId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("IssuesId");
b.HasIndex("UserId");
b.ToTable("IssueComments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int>("IssueCategoryId");
b.Property<int?>("IssueId");
b.Property<string>("ProviderId");
b.Property<int?>("RequestId");
b.Property<int>("RequestType");
b.Property<DateTime?>("ResovledDate");
b.Property<int>("Status");
b.Property<string>("Subject");
b.Property<string>("Title");
b.Property<string>("UserReportedId");
b.HasKey("Id");
b.HasIndex("IssueCategoryId");
b.HasIndex("IssueId");
b.HasIndex("UserReportedId");
b.ToTable("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<string>("Background");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<DateTime?>("DigitalReleaseDate");
b.Property<string>("ImdbId");
b.Property<int?>("IssueId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeCount");
b.Property<DateTime>("RequestDate");
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<bool>("HasFile");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
.WithMany("ChildRequests")
.HasForeignKey("ParentRequestId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany()
.HasForeignKey("UserReportedId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", 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")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest")
.WithMany("SeasonRequests")
.HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class DigitalRelease : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "DigitalReleaseDate",
table: "MovieRequests",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DigitalReleaseDate",
table: "MovieRequests");
}
}
}

@ -0,0 +1,918 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Ombi.Helpers;
using Ombi.Store.Context;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using System;
namespace Ombi.Store.Migrations
{
[DbContext(typeof(OmbiContext))]
[Migration("20180307131304_SonarrOverrides")]
partial class SonarrOverrides
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Ombi.Store.Entities.ApplicationConfiguration", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Type");
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("ApplicationConfiguration");
});
modelBuilder.Entity("Ombi.Store.Entities.Audit", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AuditArea");
b.Property<int>("AuditType");
b.Property<DateTime>("DateTime");
b.Property<string>("Description");
b.Property<string>("User");
b.HasKey("Id");
b.ToTable("Audit");
});
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId")
.IsRequired();
b.Property<string>("ProviderId");
b.Property<string>("Title");
b.Property<int>("Type");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("EmbyId");
b.Property<int>("EpisodeNumber");
b.Property<string>("ParentId");
b.Property<string>("ProviderId");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.GlobalSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Content");
b.Property<string>("SettingsName");
b.HasKey("Id");
b.ToTable("GlobalSettings");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationTemplates", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("Agent");
b.Property<bool>("Enabled");
b.Property<string>("Message");
b.Property<int>("NotificationType");
b.Property<string>("Subject");
b.HasKey("Id");
b.ToTable("NotificationTemplates");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("PlayerId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("NotificationUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("Alias");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<string>("EmbyConnectUserId");
b.Property<int?>("EpisodeRequestLimit");
b.Property<DateTime?>("LastLoggedIn");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<int?>("MovieRequestLimit");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("ProviderUserId");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserAccessToken");
b.Property<string>("UserName")
.HasMaxLength(256);
b.Property<int>("UserType");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("GrandparentKey");
b.Property<int>("Key");
b.Property<int>("ParentKey");
b.Property<int>("SeasonNumber");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ParentKey");
b.Property<int>("PlexContentId");
b.Property<int?>("PlexServerContentId");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AddedAt");
b.Property<string>("ImdbId");
b.Property<int>("Key");
b.Property<string>("Quality");
b.Property<string>("ReleaseYear");
b.Property<string>("TheMovieDbId");
b.Property<string>("Title");
b.Property<string>("TvDbId");
b.Property<int>("Type");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("HasFile");
b.Property<int>("TheMovieDbId");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<int?>("IssueId");
b.Property<int>("ParentRequestId");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("SeriesType");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("ParentRequestId");
b.HasIndex("RequestedUserId");
b.ToTable("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("IssueCategory");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Comment");
b.Property<DateTime>("Date");
b.Property<int?>("IssuesId");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("IssuesId");
b.HasIndex("UserId");
b.ToTable("IssueComments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Description");
b.Property<int>("IssueCategoryId");
b.Property<int?>("IssueId");
b.Property<string>("ProviderId");
b.Property<int?>("RequestId");
b.Property<int>("RequestType");
b.Property<DateTime?>("ResovledDate");
b.Property<int>("Status");
b.Property<string>("Subject");
b.Property<string>("Title");
b.Property<string>("UserReportedId");
b.HasKey("Id");
b.HasIndex("IssueCategoryId");
b.HasIndex("IssueId");
b.HasIndex("UserReportedId");
b.ToTable("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<string>("Background");
b.Property<bool?>("Denied");
b.Property<string>("DeniedReason");
b.Property<DateTime?>("DigitalReleaseDate");
b.Property<string>("ImdbId");
b.Property<int?>("IssueId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int>("RequestType");
b.Property<DateTime>("RequestedDate");
b.Property<string>("RequestedUserId");
b.Property<int>("RootPathOverride");
b.Property<string>("Status");
b.Property<int>("TheMovieDbId");
b.Property<string>("Title");
b.HasKey("Id");
b.HasIndex("RequestedUserId");
b.ToTable("MovieRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeCount");
b.Property<DateTime>("RequestDate");
b.Property<int>("RequestId");
b.Property<int>("RequestType");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("RequestLog");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ImdbId");
b.Property<string>("Overview");
b.Property<string>("PosterPath");
b.Property<int?>("QualityOverride");
b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder");
b.Property<string>("Status");
b.Property<string>("Title");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("TvRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("EpisodeNumber");
b.Property<bool>("HasFile");
b.Property<int>("SeasonNumber");
b.Property<int>("TvDbId");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Token");
b.Property<string>("UserId");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Tokens");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("AirDate");
b.Property<bool>("Approved");
b.Property<bool>("Available");
b.Property<int>("EpisodeNumber");
b.Property<bool>("Requested");
b.Property<int>("SeasonId");
b.Property<string>("Title");
b.Property<string>("Url");
b.HasKey("Id");
b.HasIndex("SeasonId");
b.ToTable("EpisodeRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("ChildRequestId");
b.Property<int>("SeasonNumber");
b.HasKey("Id");
b.HasIndex("ChildRequestId");
b.ToTable("SeasonRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
});
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.TvRequests", "ParentRequest")
.WithMany("ChildRequests")
.HasForeignKey("ParentRequestId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.Issues", "Issues")
.WithMany("Comments")
.HasForeignKey("IssuesId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.IssueCategory", "IssueCategory")
.WithMany()
.HasForeignKey("IssueCategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.Requests.MovieRequests")
.WithMany("Issues")
.HasForeignKey("IssueId");
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany()
.HasForeignKey("UserReportedId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany()
.HasForeignKey("RequestedUserId");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", 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")
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
{
b.HasOne("Ombi.Store.Repository.Requests.SeasonRequests", "Season")
.WithMany("Episodes")
.HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.HasOne("Ombi.Store.Entities.Requests.ChildRequests", "ChildRequest")
.WithMany("SeasonRequests")
.HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations
{
public partial class SonarrOverrides : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "QualityOverride",
table: "TvRequests",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "QualityOverride",
table: "TvRequests");
}
}
}

@ -553,6 +553,8 @@ namespace Ombi.Store.Migrations
b.Property<string>("DeniedReason"); b.Property<string>("DeniedReason");
b.Property<DateTime?>("DigitalReleaseDate");
b.Property<string>("ImdbId"); b.Property<string>("ImdbId");
b.Property<int?>("IssueId"); b.Property<int?>("IssueId");
@ -619,6 +621,8 @@ namespace Ombi.Store.Migrations
b.Property<string>("PosterPath"); b.Property<string>("PosterPath");
b.Property<int?>("QualityOverride");
b.Property<DateTime>("ReleaseDate"); b.Property<DateTime>("ReleaseDate");
b.Property<int?>("RootFolder"); b.Property<int?>("RootFolder");

@ -19,5 +19,8 @@ namespace Ombi.Store.Repository
Task AddRange(IEnumerable<PlexEpisode> content); Task AddRange(IEnumerable<PlexEpisode> content);
IEnumerable<PlexServerContent> GetWhereContentByCustom(Expression<Func<PlexServerContent, bool>> predicate); IEnumerable<PlexServerContent> GetWhereContentByCustom(Expression<Func<PlexServerContent, bool>> predicate);
Task<PlexServerContent> GetFirstContentByCustom(Expression<Func<PlexServerContent, bool>> predicate); Task<PlexServerContent> GetFirstContentByCustom(Expression<Func<PlexServerContent, bool>> predicate);
Task DeleteEpisode(PlexEpisode content);
void DeleteWithoutSave(PlexServerContent content);
void DeleteWithoutSave(PlexEpisode content);
} }
} }

@ -103,12 +103,29 @@ namespace Ombi.Store.Repository
return Db.PlexEpisode.Include(x => x.Series).AsQueryable(); return Db.PlexEpisode.Include(x => x.Series).AsQueryable();
} }
public void DeleteWithoutSave(PlexServerContent content)
{
Db.PlexServerContent.Remove(content);
}
public void DeleteWithoutSave(PlexEpisode content)
{
Db.PlexEpisode.Remove(content);
}
public async Task<PlexEpisode> Add(PlexEpisode content) public async Task<PlexEpisode> Add(PlexEpisode content)
{ {
await Db.PlexEpisode.AddAsync(content); await Db.PlexEpisode.AddAsync(content);
await Db.SaveChangesAsync(); await Db.SaveChangesAsync();
return content; return content;
} }
public async Task DeleteEpisode(PlexEpisode content)
{
Db.PlexEpisode.Remove(content);
await Db.SaveChangesAsync();
}
public async Task<PlexEpisode> GetEpisodeByKey(int key) public async Task<PlexEpisode> GetEpisodeByKey(int key)
{ {
return await Db.PlexEpisode.FirstOrDefaultAsync(x => x.Key == key); return await Db.PlexEpisode.FirstOrDefaultAsync(x => x.Key == key);

@ -11,5 +11,7 @@ namespace Ombi.Store.Repository.Requests
Task Update(MovieRequests request); Task Update(MovieRequests request);
Task Save(); Task Save();
IQueryable<MovieRequests> GetWithUser(); IQueryable<MovieRequests> GetWithUser();
IQueryable<MovieRequests> GetWithUser(string userId);
IQueryable<MovieRequests> GetAll(string userId);
} }
} }

@ -14,11 +14,13 @@ namespace Ombi.Store.Repository.Requests
Task Delete(TvRequests request); Task Delete(TvRequests request);
Task DeleteChild(ChildRequests request); Task DeleteChild(ChildRequests request);
IQueryable<TvRequests> Get(); IQueryable<TvRequests> Get();
IQueryable<TvRequests> Get(string userId);
Task<TvRequests> GetRequestAsync(int tvDbId); Task<TvRequests> GetRequestAsync(int tvDbId);
TvRequests GetRequest(int tvDbId); TvRequests GetRequest(int tvDbId);
Task Update(TvRequests request); Task Update(TvRequests request);
Task UpdateChild(ChildRequests request); Task UpdateChild(ChildRequests request);
IQueryable<ChildRequests> GetChild(); IQueryable<ChildRequests> GetChild();
IQueryable<ChildRequests> GetChild(string userId);
Task Save(); Task Save();
Task DeleteChildRange(IEnumerable<ChildRequests> request); Task DeleteChildRange(IEnumerable<ChildRequests> request);
} }

@ -33,6 +33,11 @@ namespace Ombi.Store.Repository.Requests
} }
public IQueryable<MovieRequests> GetAll(string userId)
{
return GetWithUser().Where(x => x.RequestedUserId == userId);
}
public MovieRequests GetRequest(int theMovieDbId) public MovieRequests GetRequest(int theMovieDbId)
{ {
return Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId) return Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId)
@ -48,6 +53,16 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .AsQueryable();
} }
public IQueryable<MovieRequests> GetWithUser(string userId)
{
return Db.MovieRequests
.Where(x => x.RequestedUserId == userId)
.Include(x => x.RequestedUser)
.ThenInclude(x => x.NotificationUserIds)
.AsQueryable();
}
public async Task Update(MovieRequests request) public async Task Update(MovieRequests request)
{ {
if (Db.Entry(request).State == EntityState.Detached) if (Db.Entry(request).State == EntityState.Detached)

@ -48,6 +48,18 @@ namespace Ombi.Store.Repository.Requests
.ThenInclude(x => x.Episodes) .ThenInclude(x => x.Episodes)
.AsQueryable(); .AsQueryable();
} }
public IQueryable<TvRequests> Get(string userId)
{
return Db.TvRequests
.Include(x => x.ChildRequests)
.ThenInclude(x => x.RequestedUser)
.Include(x => x.ChildRequests)
.ThenInclude(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.Where(x => x.ChildRequests.Any(a => a.RequestedUserId == userId))
.AsQueryable();
}
public IQueryable<ChildRequests> GetChild() public IQueryable<ChildRequests> GetChild()
{ {
return Db.ChildRequests return Db.ChildRequests
@ -58,6 +70,17 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .AsQueryable();
} }
public IQueryable<ChildRequests> GetChild(string userId)
{
return Db.ChildRequests
.Where(x => x.RequestedUserId == userId)
.Include(x => x.RequestedUser)
.Include(x => x.ParentRequest)
.Include(x => x.SeasonRequests)
.ThenInclude(x => x.Episodes)
.AsQueryable();
}
public async Task Save() public async Task Save()
{ {
await Db.SaveChangesAsync(); await Db.SaveChangesAsync();

@ -8,11 +8,12 @@ namespace Ombi.Api.TheMovieDb
public interface IMovieDbApi public interface IMovieDbApi
{ {
Task<MovieResponseDto> GetMovieInformation(int movieId); Task<MovieResponseDto> GetMovieInformation(int movieId);
Task<MovieResponseDto> GetMovieInformationWithVideo(int movieId); Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId);
Task<List<MovieSearchResult>> NowPlaying(); Task<List<MovieSearchResult>> NowPlaying();
Task<List<MovieSearchResult>> PopularMovies(); Task<List<MovieSearchResult>> PopularMovies();
Task<List<MovieSearchResult>> SearchMovie(string searchTerm); Task<List<MovieSearchResult>> SearchMovie(string searchTerm);
Task<List<MovieSearchResult>> TopRated(); Task<List<MovieSearchResult>> TopRated();
Task<List<MovieSearchResult>> Upcoming(); Task<List<MovieSearchResult>> Upcoming();
Task<List<MovieSearchResult>> SimilarMovies(int movieId);
} }
} }

@ -24,6 +24,10 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using System.Collections.Generic;
namespace Ombi.TheMovieDbApi.Models namespace Ombi.TheMovieDbApi.Models
{ {
@ -54,5 +58,26 @@ namespace Ombi.TheMovieDbApi.Models
public bool video { get; set; } public bool video { get; set; }
public float vote_average { get; set; } public float vote_average { get; set; }
public int vote_count { get; set; } public int vote_count { get; set; }
public ReleaseDates release_dates { get; set; }
}
public class ReleaseDates
{
public List<ReleaseResults> results { get; set; }
}
public class ReleaseResults
{
public string iso_3166_1 { get; set; }
public List<ReleaseDate> release_dates { get; set; }
}
public class ReleaseDate
{
public string Certification { get; set; }
public string iso_639_1 { get; set; }
public string note { get; set; }
public DateTime release_date { get; set; }
public int Type { get; set; }
} }
} }

@ -1,4 +1,7 @@
namespace Ombi.Api.TheMovieDb.Models using System;
using System.Collections.Generic;
namespace Ombi.Api.TheMovieDb.Models
{ {
public class MovieResponseDto public class MovieResponseDto
{ {
@ -23,5 +26,33 @@
public bool Video { get; set; } public bool Video { get; set; }
public float VoteAverage { get; set; } public float VoteAverage { get; set; }
public int VoteCount { get; set; } public int VoteCount { get; set; }
public ReleaseDatesDto ReleaseDates { get; set; }
}
public class ReleaseDatesDto
{
public List<ReleaseResultsDto> Results { get; set; }
}
public class ReleaseResultsDto
{
public string IsoCode { get; set; }
public List<ReleaseDateDto> ReleaseDate { get; set; }
}
public class ReleaseDateDto
{
public DateTime ReleaseDate { get; set; }
public ReleaseDateType Type { get; set; }
}
public enum ReleaseDateType
{
Premiere = 1,
TheatricalLimited = 2,
Theatrical = 3,
Digital = 4,
Physical = 5,
Tv = 6
} }
} }

@ -30,11 +30,20 @@ namespace Ombi.Api.TheMovieDb
return Mapper.Map<MovieResponseDto>(result); return Mapper.Map<MovieResponseDto>(result);
} }
public async Task<MovieResponseDto> GetMovieInformationWithVideo(int movieId) public async Task<List<MovieSearchResult>> SimilarMovies(int movieId)
{
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results);
}
public async Task<MovieResponseDto> GetMovieInformationWithExtraInfo(int movieId)
{ {
var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}", BaseUri, HttpMethod.Get);
request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken);
request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos"); request.FullUri = request.FullUri.AddQueryParameter("append_to_response", "videos,release_dates");
var result = await Api.Request<MovieResponse>(request); var result = await Api.Request<MovieResponse>(request);
return Mapper.Map<MovieResponseDto>(result); return Mapper.Map<MovieResponseDto>(result);
} }

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading; using System.Threading;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -33,27 +34,27 @@ namespace Ombi.Updater
} }
// Make sure the process has been killed // Make sure the process has been killed
while (p.FindProcessByName(opt.ProcessName).Any()) while (p.FindProcessByName(opt.ProcessName).Any())
{
Thread.Sleep(500);
_log.LogDebug("Found another process called {0}, KILLING!", opt.ProcessName);
var proc = p.FindProcessByName(opt.ProcessName).FirstOrDefault();
if (proc != null)
{ {
Thread.Sleep(500); _log.LogDebug($"[{proc.Id}] - {proc.Name} - Path: {proc.StartPath}");
_log.LogDebug("Found another process called {0}, KILLING!", opt.ProcessName); opt.OmbiProcessId = proc.Id;
var proc = p.FindProcessByName(opt.ProcessName).FirstOrDefault(); p.Kill(opt);
if (proc != null)
{
_log.LogDebug($"[{proc.Id}] - {proc.Name} - Path: {proc.StartPath}");
opt.OmbiProcessId = proc.Id;
p.Kill(opt);
}
} }
_log.LogDebug("Starting to move the files");
MoveFiles(opt);
_log.LogDebug("Files replaced");
// Start Ombi
StartOmbi(opt);
} }
private void StartOmbi(StartupOptions options) _log.LogDebug("Starting to move the files");
MoveFiles(opt);
_log.LogDebug("Files replaced");
// Start Ombi
StartOmbi(opt);
}
private void StartOmbi(StartupOptions options)
{ {
_log.LogDebug("Starting ombi"); _log.LogDebug("Starting ombi");
var fileName = "Ombi.exe"; var fileName = "Ombi.exe";
@ -71,19 +72,29 @@ namespace Ombi.Updater
Arguments = $"/C net start \"{options.WindowsServiceName}\"" Arguments = $"/C net start \"{options.WindowsServiceName}\""
}; };
using (var process = new Process{StartInfo = startInfo}) using (var process = new Process { StartInfo = startInfo })
{ {
process.Start(); process.Start();
} }
} }
else else
{ {
var startupArgsBuilder = new StringBuilder();
if (!string.IsNullOrEmpty(options.Host))
{
startupArgsBuilder.Append($"--host {options.Host} ");
}
if (!string.IsNullOrEmpty(options.Storage))
{
startupArgsBuilder.Append($"--storage {options.Storage}");
}
var start = new ProcessStartInfo var start = new ProcessStartInfo
{ {
UseShellExecute = false, UseShellExecute = false,
FileName = Path.Combine(options.ApplicationPath, fileName), FileName = Path.Combine(options.ApplicationPath, fileName),
WorkingDirectory = options.ApplicationPath, WorkingDirectory = options.ApplicationPath,
Arguments = options.StartupArgs Arguments = startupArgsBuilder.ToString()
}; };
using (var proc = new Process { StartInfo = start }) using (var proc = new Process { StartInfo = start })
{ {

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RuntimeIdentifiers>win10-x64;win10-x32;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;</RuntimeIdentifiers> <RuntimeIdentifiers>win10-x64;win10-x86;osx-x64;ubuntu-x64;debian.8-x64;centos.7-x64;linux-x64;linux-arm;linux-arm64;</RuntimeIdentifiers>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyVersion>3.0.0.0</AssemblyVersion> <AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion> <FileVersion>3.0.0.0</FileVersion>

@ -1,14 +1,10 @@
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using CommandLine; using CommandLine;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog; using Serilog;
using Serilog.Events;
using ILogger = Serilog.ILogger;
namespace Ombi.Updater namespace Ombi.Updater
{ {
@ -78,8 +74,10 @@ namespace Ombi.Updater
public string ApplicationPath { get; set; } public string ApplicationPath { get; set; }
[Option("processId", Required = false)] [Option("processId", Required = false)]
public int OmbiProcessId { get; set; } public int OmbiProcessId { get; set; }
[Option("startupArgs", Required = false)] [Option("host", Required = false)]
public string StartupArgs { get; set; } public string Host { get; set; }
[Option("storage", Required = false)]
public string Storage { get; set; }
[Option("windowsServiceName", Required = false)] [Option("windowsServiceName", Required = false)]
public string WindowsServiceName { get; set; } public string WindowsServiceName { get; set; }

@ -92,7 +92,7 @@
<a [routerLink]="['/usermanagement/updatedetails']"> <a [routerLink]="['/usermanagement/updatedetails']">
<i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a> <i class="fa fa-key"></i>{{ 'NavigationBar.UpdateDetails' | translate }}</a>
</li> </li>
<li *ngIf="showMobileLink" [routerLinkActive]="['active']"> <li *ngIf="customizationSettings.mobile" [routerLinkActive]="['active']">
<a href="#" (click)="openMobileApp($event)"> <a href="#" (click)="openMobileApp($event)">
<i class="fa fa-mobile"></i>{{ 'NavigationBar.OpenMobileApp' | translate }}</a> <i class="fa fa-mobile"></i>{{ 'NavigationBar.OpenMobileApp' | translate }}</a>
</li> </li>

@ -1,4 +1,5 @@
import { Component, OnInit } from "@angular/core"; import { PlatformLocation } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { NavigationStart, Router } from "@angular/router"; import { NavigationStart, Router } from "@angular/router";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { AuthService } from "./auth/auth.service"; import { AuthService } from "./auth/auth.service";
@ -22,7 +23,6 @@ export class AppComponent implements OnInit {
public updateAvailable: boolean; public updateAvailable: boolean;
public currentUrl: string; public currentUrl: string;
public userAccessToken: string; public userAccessToken: string;
public showMobileLink = false;
private checkedForUpdate: boolean; private checkedForUpdate: boolean;
@ -32,7 +32,14 @@ export class AppComponent implements OnInit {
private readonly settingsService: SettingsService, private readonly settingsService: SettingsService,
private readonly jobService: JobService, private readonly jobService: JobService,
public readonly translate: TranslateService, public readonly translate: TranslateService,
private readonly identityService: IdentityService) { private readonly identityService: IdentityService,
private readonly platformLocation: PlatformLocation) {
const base = this.platformLocation.getBaseHrefFromDOM();
if (base.length > 1) {
__webpack_public_path__ = base + "/dist/";
}
this.translate.addLangs(["en", "de", "fr","da","es","it","nl","sv","no"]); this.translate.addLangs(["en", "de", "fr","da","es","it","nl","sv","no"]);
// this language will be used as a fallback when a translation isn't found in the current language // this language will be used as a fallback when a translation isn't found in the current language
this.translate.setDefaultLang("en"); this.translate.setDefaultLang("en");

@ -1,28 +1,5 @@
import { IUser } from "./IUser"; import { IUser } from "./IUser";
export interface IMediaBase {
imdbId: string;
id: number;
providerId: number;
title: string;
overview: string;
posterPath: string;
releaseDate: Date;
status: string;
requestedDate: Date;
approved: boolean;
type: RequestType;
requested: boolean;
available: boolean;
otherMessage: string;
adminNote: string;
requestedUser: string;
issueId: number;
denied: boolean;
deniedReason: string;
released: boolean;
}
export enum RequestType { export enum RequestType {
movie = 1, movie = 1,
tvShow = 2, tvShow = 2,
@ -34,7 +11,9 @@ export interface IMovieRequests extends IFullBaseRequest {
theMovieDbId: number; theMovieDbId: number;
rootPathOverride: number; rootPathOverride: number;
qualityOverride: number; qualityOverride: number;
digitalReleaseDate: Date;
// For the UI
rootPathOverrideTitle: string; rootPathOverrideTitle: string;
qualityOverrideTitle: string; qualityOverrideTitle: string;
} }
@ -84,6 +63,11 @@ export interface ITvRequests {
releaseDate: Date; releaseDate: Date;
status: string; status: string;
childRequests: IChildRequests[]; childRequests: IChildRequests[];
qualityOverride: number;
// For UI display
qualityOverrideTitle: string;
rootPathOverrideTitle: string;
} }
export interface IChildRequests extends IBaseRequest { export interface IChildRequests extends IBaseRequest {

@ -22,6 +22,7 @@
available: boolean; available: boolean;
plexUrl: string; plexUrl: string;
quality: string; quality: string;
digitalReleaseDate: Date;
// for the UI // for the UI
requestProcessing: boolean; requestProcessing: boolean;

@ -14,6 +14,7 @@ export interface IOmbiSettings extends ISettings {
apiKey: string; apiKey: string;
ignoreCertificateErrors: boolean; ignoreCertificateErrors: boolean;
doNotSendNotificationsForAutoApprove: boolean; doNotSendNotificationsForAutoApprove: boolean;
hideRequestsUsers: boolean;
} }
export interface IUpdateSettings extends ISettings { export interface IUpdateSettings extends ISettings {
@ -96,6 +97,7 @@ export interface ICustomizationSettings extends ISettings {
applicationName: string; applicationName: string;
applicationUrl: string; applicationUrl: string;
logo: string; logo: string;
mobile: boolean;
customCssLink: string; customCssLink: string;
enableCustomDonations: boolean; enableCustomDonations: boolean;
customDonationUrl: string; customDonationUrl: string;

@ -49,3 +49,14 @@ export interface IMobileUsersViewModel {
username: string; username: string;
devices: number; devices: number;
} }
export interface IMassEmailUserModel {
user: IUser;
selected: boolean;
}
export interface IMassEmailModel {
subject: string;
body: string;
users: IUser[];
}

@ -13,15 +13,15 @@
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('issue.status')"> <th (click)="setOrder('status')">
<a [translate]="'Issues.Status'"></a> <a [translate]="'Issues.Status'"></a>
<span *ngIf="order === 'issue.status'"> <span *ngIf="order === 'status'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('issue.reportedUser')"> <th (click)="setOrder('reportedUser')">
<a [translate]="'Issues.ReportedBy'"></a> <a [translate]="'Issues.ReportedBy'"></a>
<span *ngIf="order === 'issue.reportedUser'"> <span *ngIf="order === 'reportedUser'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>

@ -1,4 +1,13 @@
div.centered { @media only screen and (max-width: 992px) {
div.centered {
max-height: 100%;
overflow-y: auto;
width: 100%;
padding: 50% 12.5%;
}
}
div.centered {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;

@ -1,11 +1,11 @@
import { Component, Input } from "@angular/core"; // import { Component, Input } from "@angular/core";
import { IMediaBase } from "../interfaces"; // import { IMediaBase } from "../interfaces";
@Component({ // @Component({
selector: "request-card", // selector: "request-card",
templateUrl: "./request-card.component.html", // templateUrl: "./request-card.component.html",
}) // })
export class RequestCardComponent { // export class RequestCardComponent {
@Input() public request: IMediaBase; // @Input() public request: IMediaBase;
} // }

@ -3,8 +3,50 @@
<div class="input-group"> <div class="input-group">
<input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)"> <input type="text" id="search" class="form-control form-control-custom searchwidth" placeholder="Search" (keyup)="search($event)">
<span class="input-group-btn"> <span class="input-group-btn">
<button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = true" > <button id="filterBtn" class="btn btn-sm btn-info-outline" (click)="filterDisplay = !filterDisplay" >
<i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}</button> <i class="fa fa-filter"></i> {{ 'Requests.Filter' | translate }}
</button>
<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-sort"></i> {{ 'Requests.Sort' | translate }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a (click)="setOrder('requestedDate')">{{ 'Requests.SortRequestDate' | translate }}
<span *ngIf="order === 'requestedDate'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('title')">{{ 'Requests.SortTitle' | translate}}
<span *ngIf="order === 'title'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('releaseDate')">{{ 'Requests.TheatricalReleaseSort' | translate }}
<span *ngIf="order === 'releaseDate'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('requestedUser.userAlias')">{{ 'Requests.SortRequestedBy' | translate }}
<span *ngIf="order === 'requestedUser.userAlias'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
<a (click)="setOrder('status')">{{ 'Requests.SortStatus' | translate }}
<span *ngIf="order === 'status'">
<span [hidden]="reverse"><small><i class="fa fa-arrow-down" aria-hidden="true"></i></small></span>
<span [hidden]="!reverse"><small><i class="fa fa-arrow-up" aria-hidden="true"></i></small></span>
</span>
</a>
</li>
</ul>
</span> </span>
</div> </div>
@ -17,7 +59,7 @@
<div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="100" (scrolled)="loadMore()"> <div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="100" (scrolled)="loadMore()">
<div *ngFor="let request of movieRequests"> <div *ngFor="let request of movieRequests | orderBy: order : reverse : 'case-insensitive'">
<div class="row"> <div class="row">
@ -66,7 +108,8 @@
</div> </div>
<div id="releaseDate">{{ 'Requests.ReleaseDate' | translate }} {{request.releaseDate | date}}</div> <div id="releaseDate">{{ 'Requests.TheatricalRelease' | translate: {date: request.releaseDate | date: 'mediumDate'} }}</div>
<div *ngIf="request.digitalReleaseDate" id="digitalReleaseDate">{{ 'Requests.DigitalRelease' | translate: {date: request.digitalReleaseDate | date: 'mediumDate'} }}</div>
<div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | date}}</div> <div id="requestedDate">{{ 'Requests.RequestDate' | translate }} {{request.requestedDate | date}}</div>
<br /> <br />
<div *ngIf="isAdmin"> <div *ngIf="isAdmin">
@ -168,7 +211,7 @@
<p-sidebar [(visible)]="filterDisplay" styleClass="ui-sidebar-md side-back side-small"> <p-sidebar [(visible)]="filterDisplay" styleClass="ui-sidebar-md side-back side-small">
<h3>{{ 'Filter.Filter' | translate }}</h3> <h3>{{ 'Requests.Filter' | translate }}</h3>
<hr> <hr>
<h4>{{ 'Filter.FilterHeaderAvailability' | translate }}</h4> <h4>{{ 'Filter.FilterHeaderAvailability' | translate }}</h4>
@ -198,7 +241,14 @@
<label for="Processing">{{ 'Common.ProcessingRequest' | translate }}</label> <label for="Processing">{{ 'Common.ProcessingRequest' | translate }}</label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="radio">
<input type="radio" id="pendingApproval" name="Status" (click)="filterStatus(filterType.PendingApproval, $event)">
<label for="pendingApproval">{{ 'Filter.PendingApproval' | translate }}</label>
</div>
</div>
<button class="btn btn-sm btn-primary-outline" (click)="clearFilter()"> <button class="btn btn-sm btn-primary-outline" (click)="clearFilter()">
<i class="fa fa-filter"></i> {{ 'Requests.ClearFilter' | translate }}</button> <i class="fa fa-filter"></i> {{ 'Filter.ClearFilter' | translate }}</button>
</p-sidebar> </p-sidebar>

@ -36,6 +36,9 @@ export class MovieRequestsComponent implements OnInit {
public filter: IFilter; public filter: IFilter;
public filterType = FilterType; public filterType = FilterType;
public order: string = "requestedDate";
public reverse = false;
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
@ -172,6 +175,14 @@ export class MovieRequestsComponent implements OnInit {
}); });
} }
public setOrder(value: string) {
if (this.order === value) {
this.reverse = !this.reverse;
}
this.order = value;
}
private loadRequests(amountToLoad: number, currentlyLoaded: number) { private loadRequests(amountToLoad: number, currentlyLoaded: number) {
this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1) this.requestService.getMovieRequests(amountToLoad, currentlyLoaded + 1)
.subscribe(x => { .subscribe(x => {

@ -2,6 +2,7 @@
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { OrderModule } from "ngx-order-pipe";
import { InfiniteScrollModule } from "ngx-infinite-scroll"; import { InfiniteScrollModule } from "ngx-infinite-scroll";
@ -14,7 +15,7 @@ import { TvRequestsComponent } from "./tvrequests.component";
import { SidebarModule, TreeTableModule } from "primeng/primeng"; import { SidebarModule, TreeTableModule } from "primeng/primeng";
import { IdentityService, RadarrService, RequestService } from "../services"; import { IdentityService, RadarrService, RequestService, SonarrService } from "../services";
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";
@ -34,6 +35,7 @@ const routes: Routes = [
TreeTableModule, TreeTableModule,
SharedModule, SharedModule,
SidebarModule, SidebarModule,
OrderModule,
], ],
declarations: [ declarations: [
RequestComponent, RequestComponent,
@ -48,7 +50,8 @@ const routes: Routes = [
IdentityService, IdentityService,
RequestService, RequestService,
RadarrService, RadarrService,
], SonarrService,
],
}) })
export class RequestsModule { } export class RequestsModule { }

@ -51,17 +51,61 @@
<div>Release Date: {{node.data.releaseDate | date}}</div> <div>Release Date: {{node.data.releaseDate | date}}</div>
<div *ngIf="isAdmin">
<div *ngIf="node.data.qualityOverrideTitle">{{ 'Requests.QualityOverride' | translate }}
<span>{{node.data.qualityOverrideTitle}} </span>
</div>
<div *ngIf="node.data.rootPathOverrideTitle">{{ 'Requests.RootFolderOverride' | translate }}
<span>{{node.data.rootPathOverrideTitle}} </span>
</div>
</div>
<br /> <br />
</div> </div>
<div class="col-sm-3 col-sm-push-3 small-padding"> <div class="col-sm-3 col-sm-push-3 small-padding">
<button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab($event)"><i class="fa fa-plus"></i> View</button> <button style="text-align: right" class="btn btn-sm btn-success-outline" (click)="openClosestTab($event)"><i class="fa fa-plus"></i> View</button>
<div *ngIf="isAdmin">
<!--Sonarr Root Folder-->
<div *ngIf="sonarrRootFolders" class="btn-group btn-split" id="rootFolderBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeRootFolder' | translate }}</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let folder of sonarrRootFolders">
<a href="#" (click)="selectRootFolder(node.data, folder, $event)">{{folder.path}}</a>
</li>
</ul>
</div>
<!--Sonarr Quality Profiles -->
<div *ngIf="sonarrProfiles" class="btn-group btn-split" id="changeQualityBtn">
<button type="button" class="btn btn-sm btn-warning-outline">
<i class="fa fa-plus"></i> {{ 'Requests.ChangeQualityProfile' | translate }}</button>
<button type="button" class="btn btn-warning-outline dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li *ngFor="let profile of sonarrProfiles">
<a href="#" (click)="selectQualityProfile(node.data, profile, $event)">{{profile.name}}</a>
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!--This is the section that holds the child seasons if they want to specify specific episodes--> <!--This is the section that holds the child seasons if they want to specify specific episodes-->
<div *ngIf="node.leaf"> <div *ngIf="node.leaf">
<tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin" (requestDeleted)="childRequestDeleted($event)" [issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled" [issueProviderId]="node.data.tvDbId" ></tvrequests-children> <tvrequests-children [childRequests]="node.data" [isAdmin] ="isAdmin"
(requestDeleted)="childRequestDeleted($event)"
[issueCategories]="issueCategories" [issuesEnabled]="issuesEnabled"
[issueProviderId]="node.data.tvDbId"></tvrequests-children>
</div> </div>
</ng-template> </ng-template>
</p-column> </p-column>

@ -11,10 +11,10 @@ import "rxjs/add/operator/distinctUntilChanged";
import "rxjs/add/operator/map"; import "rxjs/add/operator/map";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { RequestService } from "../services"; import { NotificationService, RequestService, SonarrService } from "../services";
import { TreeNode } from "primeng/primeng"; import { TreeNode } from "primeng/primeng";
import { IIssueCategory, ITvRequests } from "../interfaces"; import { IIssueCategory, ISonarrProfile, ISonarrRootFolder, ITvRequests } from "../interfaces";
@Component({ @Component({
selector: "tv-requests", selector: "tv-requests",
@ -34,13 +34,18 @@ export class TvRequestsComponent implements OnInit {
@Input() public issuesEnabled: boolean; @Input() public issuesEnabled: boolean;
public issueProviderId: string; public issueProviderId: string;
public sonarrProfiles: ISonarrProfile[] = [];
public sonarrRootFolders: ISonarrRootFolder[] = [];
private currentlyLoaded: number; private currentlyLoaded: number;
private amountToLoad: number; private amountToLoad: number;
constructor(private requestService: RequestService, constructor(private requestService: RequestService,
private auth: AuthService, private auth: AuthService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private imageService: ImageService) { private imageService: ImageService,
private sonarrService: SonarrService,
private notificationService: NotificationService) {
this.searchChanged this.searchChanged
.debounceTime(600) // Wait Xms after the last event before emitting last event .debounceTime(600) // Wait Xms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value .distinctUntilChanged() // only emit if value is different from previous value
@ -54,9 +59,11 @@ export class TvRequestsComponent implements OnInit {
.subscribe(m => { .subscribe(m => {
this.tvRequests = m; this.tvRequests = m;
this.tvRequests.forEach((val) => this.loadBackdrop(val)); this.tvRequests.forEach((val) => this.loadBackdrop(val));
this.tvRequests.forEach((val) => this.setOverride(val.data));
}); });
}); });
} }
public openClosestTab(el: any) { public openClosestTab(el: any) {
const rowclass = "undefined ng-star-inserted"; const rowclass = "undefined ng-star-inserted";
el = el.toElement || el.relatedTarget || el.target; el = el.toElement || el.relatedTarget || el.target;
@ -83,11 +90,18 @@ export class TvRequestsComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
const profile = <ISonarrProfile>{name:"test",id:1 };
const folder = <ISonarrRootFolder>{path:"testpath", id:1};
this.sonarrProfiles.push(profile);
this.sonarrRootFolders.push(folder);
this.amountToLoad = 1000; this.amountToLoad = 1000;
this.currentlyLoaded = 1000; this.currentlyLoaded = 1000;
this.tvRequests = []; this.tvRequests = [];
this.loadInit();
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.loadInit();
} }
public loadMore() { public loadMore() {
@ -117,14 +131,72 @@ export class TvRequestsComponent implements OnInit {
this.ngOnInit(); this.ngOnInit();
} }
public selectRootFolder(searchResult: ITvRequests, rootFolderSelected: ISonarrRootFolder, event: any) {
event.preventDefault();
searchResult.rootFolder = rootFolderSelected.id;
this.setOverride(searchResult);
this.updateRequest(searchResult);
}
public selectQualityProfile(searchResult: ITvRequests, profileSelected: ISonarrProfile, event: any) {
event.preventDefault();
searchResult.qualityOverride = profileSelected.id;
this.setOverride(searchResult);
this.updateRequest(searchResult);
}
private setOverride(req: ITvRequests): void {
this.setQualityOverrides(req);
this.setRootFolderOverrides(req);
}
private updateRequest(request: ITvRequests) {
this.requestService.updateTvRequest(request)
.subscribe(x => {
this.notificationService.success("Request Updated");
this.setOverride(x);
request = x;
});
}
private setQualityOverrides(req: ITvRequests): void {
if (this.sonarrProfiles) {
const profile = this.sonarrProfiles.filter((p) => {
return p.id === req.qualityOverride;
});
if (profile.length > 0) {
req.qualityOverrideTitle = profile[0].name;
}
}
}
private setRootFolderOverrides(req: ITvRequests): void {
if (this.sonarrRootFolders) {
const path = this.sonarrRootFolders.filter((folder) => {
return folder.id === req.rootFolder;
});
if (path.length > 0) {
req.rootPathOverrideTitle = path[0].path;
}
}
}
private loadInit() { private loadInit() {
this.requestService.getTvRequestsTree(this.amountToLoad, 0) this.requestService.getTvRequestsTree(this.amountToLoad, 0)
.subscribe(x => { .subscribe(x => {
this.tvRequests = x; this.tvRequests = x;
this.tvRequests.forEach((val, index) => { this.tvRequests.forEach((val, index) => {
this.loadBackdrop(val); this.loadBackdrop(val);
this.setOverride(val.data);
}); });
}); });
if(this.isAdmin) {
this.sonarrService.getQualityProfilesWithoutSettings()
.subscribe(x => this.sonarrProfiles = x);
this.sonarrService.getRootFoldersWithoutSettings()
.subscribe(x => this.sonarrRootFolders = x);
}
} }
private resetSearch() { private resetSearch() {

@ -41,8 +41,9 @@
<a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank"> <a href="https://www.themoviedb.org/movie/{{result.id}}/" target="_blank">
<h4>{{result.title}} ({{result.releaseDate | date: 'yyyy'}})</h4> <h4>{{result.title}} ({{result.releaseDate | date: 'yyyy'}})</h4>
</a> </a>
<span class="tags"> <span class="tags">
<span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.ReleaseDate' | translate }} {{result.releaseDate | date: 'dd/MM/yyyy'}}</span> <span *ngIf="result.releaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.TheatricalRelease' | translate: {date: result.releaseDate | date: 'mediumDate'} }}</span>
<span *ngIf="result.digitalReleaseDate" class="label label-info" id="releaseDateLabel" target="_blank">{{ 'Search.DigitalDate' | translate: {date: result.digitalReleaseDate | date: 'mediumDate'} }}</span>
<a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a> <a *ngIf="result.homepage" href="{{result.homepage}}" id="homePageLabel" target="_blank"><span class="label label-info" [translate]="'Search.Movies.HomePage'"></span></a>
@ -79,7 +80,8 @@
<i *ngIf="result.processed && !result.requestProcessing" class="fa fa-check"></i>{{ 'Common.Request' | translate }}</button> <i *ngIf="result.processed && !result.requestProcessing" class="fa fa-check"></i>{{ 'Common.Request' | translate }}</button>
</ng-template> </ng-template>
</div> </div>
<button style="text-align: right" class="btn btn-sm btn-info-outline" (click)="similarMovies(result.id)"> <i class="fa fa-eye"></i> {{ 'Search.Similar' | translate }}</button>
<br/> <br/>
<div *ngIf="result.available"> <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> View On Plex</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> View On Plex</a>

@ -146,6 +146,15 @@ export class MovieSearchComponent implements OnInit {
this.issueProviderId = req.id.toString(); this.issueProviderId = req.id.toString();
} }
public similarMovies(theMovieDbId: number) {
this.clearResults();
this.searchService.similarMovies(theMovieDbId)
.subscribe(x => {
this.movieResults = x;
this.getExtraInfo();
});
}
private getExtraInfo() { private getExtraInfo() {
this.movieResults.forEach((val, index) => { this.movieResults.forEach((val, index) => {

@ -62,7 +62,7 @@
<div class="col-sm-8 small-padding"> <div class="col-sm-8 small-padding">
<div> <div>
<a href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank"> <a *ngIf="node.data.imdbId" href="http://www.imdb.com/title/{{node.data.imdbId}}/" target="_blank">
<h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4> <h4>{{node.data.title}} ({{node.data.firstAired | date: 'yyyy'}})</h4>
</a> </a>

@ -20,4 +20,11 @@ export class SonarrService extends ServiceHelpers {
public getQualityProfiles(settings: ISonarrSettings): Observable<ISonarrProfile[]> { public getQualityProfiles(settings: ISonarrSettings): Observable<ISonarrProfile[]> {
return this.http.post<ISonarrProfile[]>(`${this.url}/Profiles/`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<ISonarrProfile[]>(`${this.url}/Profiles/`, JSON.stringify(settings), {headers: this.headers});
} }
public getRootFoldersWithoutSettings(): Observable<ISonarrRootFolder[]> {
return this.http.get<ISonarrRootFolder[]>(`${this.url}/RootFolders/`, {headers: this.headers});
}
public getQualityProfilesWithoutSettings(): Observable<ISonarrProfile[]> {
return this.http.get<ISonarrProfile[]>(`${this.url}/Profiles/`, {headers: this.headers});
}
} }

@ -1,15 +0,0 @@
export * from "./applications";
export * from "./helpers";
export * from "./identity.service";
export * from "./image.service";
export * from "./landingpage.service";
export * from "./notification.service";
export * from "./request.service";
export * from "./search.service";
export * from "./service.helpers";
export * from "./settings.service";
export * from "./status.service";
export * from "./job.service";
export * from "./issues.service";
export * from "./mobile.service";
export * from "./recentlyAdded.service";

@ -0,0 +1,19 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx";
import { IMassEmailModel } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class NotificationMessageService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/notifications/", platformLocation);
}
public sendMassEmail(model: IMassEmailModel): Observable<boolean> {
return this.http.post<boolean>(`${this.url}massemail/`, JSON.stringify(model) ,{headers: this.headers});
}
}

@ -19,6 +19,9 @@ export class SearchService extends ServiceHelpers {
public searchMovie(searchTerm: string): Observable<ISearchMovieResult[]> { 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 popularMovies(): Observable<ISearchMovieResult[]> { public popularMovies(): Observable<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/Popular`); return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/Popular`);
@ -54,15 +57,15 @@ export class SearchService extends ServiceHelpers {
} }
public popularTv(): Observable<TreeNode[]> { public popularTv(): Observable<TreeNode[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/popular`, {headers: this.headers}); return this.http.get<TreeNode[]>(`${this.url}/Tv/popular/tree`, {headers: this.headers});
} }
public mostWatchedTv(): Observable<TreeNode[]> { public mostWatchedTv(): Observable<TreeNode[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/mostwatched`, {headers: this.headers}); return this.http.get<TreeNode[]>(`${this.url}/Tv/mostwatched/tree`, {headers: this.headers});
} }
public anticipatedTv(): Observable<TreeNode[]> { public anticipatedTv(): Observable<TreeNode[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/anticipated`, {headers: this.headers}); return this.http.get<TreeNode[]>(`${this.url}/Tv/anticipated/tree`, {headers: this.headers});
} }
public trendingTv(): Observable<TreeNode[]> { public trendingTv(): Observable<TreeNode[]> {
return this.http.get<TreeNode[]>(`${this.url}/Tv/trending`, {headers: this.headers}); return this.http.get<TreeNode[]>(`${this.url}/Tv/trending/tree`, {headers: this.headers});
} }
} }

@ -3,6 +3,12 @@
<fieldset *ngIf="settings"> <fieldset *ngIf="settings">
<legend>Customization</legend> <legend>Customization</legend>
<div class="row">
<div class="col-md-2 col-md-push-10">
<span style="vertical-align: top;">Advanced</span>
<p-inputSwitch id="customInputSwitch" [(ngModel)]="advanced"></p-inputSwitch>
</div>
</div>
<div class="col-md-5"> <div class="col-md-5">
<div class="form-group"> <div class="form-group">
<label for="applicationName" class="control-label">Application Name</label> <label for="applicationName" class="control-label">Application Name</label>
@ -20,6 +26,13 @@
</div> </div>
</div> </div>
<div class="form-group" *ngIf="advanced">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.mobile" [checked]="settings.mobile">
<label for="enable">Enable Mobile</label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="logo" class="control-label">Custom Logo</label> <label for="logo" class="control-label">Custom Logo</label>
<div> <div>

@ -11,6 +11,7 @@ export class CustomizationComponent implements OnInit {
public settings: ICustomizationSettings; public settings: ICustomizationSettings;
public themes: IThemes[]; public themes: IThemes[];
public advanced: boolean;
constructor(private settingsService: SettingsService, private notificationService: NotificationService) { } constructor(private settingsService: SettingsService, private notificationService: NotificationService) { }

@ -0,0 +1,50 @@
<settings-menu></settings-menu>
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Mass-Email'"></wiki>
<fieldset>
<legend>Mass Email</legend>
<div class="col-md-6">
<div class="form-group">
<input type="text" class="form-control form-control-custom " id="subject" name="subject" placeholder="Subject" [(ngModel)]="subject" [ngClass]="{'form-error': missingSubject}">
<small *ngIf="missingSubject" class="error-text">Hey! We need a subject!</small>
</div>
<div class="form-group" >
<textarea rows="10" type="text" class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="message"></textarea>
</div>
<div class="form-group">
<label for="logo" class="control-label">Message Preview</label>
<br/>
<small>May appear differently on email clients</small>
<hr/>
<div [innerHTML]="message"></div>
</div>
<div class="form-group">
<div>
<button type="submit" id="save" (click)="send()" class="btn btn-primary-outline">Send</button>
</div>
</div>
</div>
<div class="col-md-6">
<!--Users Section-->
<label class="control-label">Recipients</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="all" (click)="selectAllUsers()">
<label for="all">Select All</label>
</div>
</div>
<div class="form-group" *ngFor="let u of users">
<div class="checkbox">
<input type="checkbox" id="{{u.user.id}}" [(ngModel)]="u.selected" (click)="selectSingleUser(u)">
<label for="{{u.user.id}}">{{u.user.userName}}</label>
</div>
</div>
</div>
</fieldset>

@ -0,0 +1,75 @@
import { Component, OnInit } from "@angular/core";
import { IMassEmailModel, IMassEmailUserModel } from "../../interfaces";
import { IdentityService, NotificationMessageService, NotificationService, SettingsService } from "../../services";
@Component({
templateUrl: "./massemail.component.html",
})
export class MassEmailComponent implements OnInit {
public users: IMassEmailUserModel[] = [];
public message: string;
public subject: string;
public missingSubject = false;
public emailEnabled: boolean;
constructor(private readonly notification: NotificationService,
private readonly identityService: IdentityService,
private readonly notificationMessageService: NotificationMessageService,
private readonly settingsService: SettingsService) {
}
public ngOnInit(): void {
this.identityService.getUsers().subscribe(x => {
x.forEach(u => {
this.users.push({
user: u,
selected: false,
});
});
});
this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailEnabled = x);
}
public selectAllUsers() {
this.users.forEach(u => u.selected = !u.selected);
}
public selectSingleUser(user: IMassEmailUserModel) {
user.selected = !user.selected;
}
public send() {
if(!this.subject) {
this.missingSubject = true;
return;
}
if(!this.emailEnabled) {
this.notification.error("You have not yet setup your email notifications, do that first!");
return;
}
this.missingSubject = false;
// Where(x => x.selected).Select(x => x.user)
const selectedUsers = this.users.filter(u => {
return u.selected;
}).map(u => u.user);
if(selectedUsers.length <=0) {
this.notification.error("You need to select at least one user to send the email");
return;
}
const model = <IMassEmailModel>{
users: selectedUsers,
subject: this.subject,
body: this.message,
};
this.notification.info("Sending","Sending mass email... Please wait");
this.notificationMessageService.sendMassEmail(model).subscribe(x => {
this.notification.success("We have sent the mass email to the users selected!");
});
}
}

@ -53,6 +53,14 @@
</div> </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>
</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<input type="checkbox" id="ignoreCertificateErrors" name="ignoreCertificateErrors" formControlName="ignoreCertificateErrors"> <input type="checkbox" id="ignoreCertificateErrors" name="ignoreCertificateErrors" formControlName="ignoreCertificateErrors">

@ -24,6 +24,7 @@ export class OmbiComponent implements OnInit {
ignoreCertificateErrors: [x.ignoreCertificateErrors], ignoreCertificateErrors: [x.ignoreCertificateErrors],
baseUrl: [x.baseUrl], baseUrl: [x.baseUrl],
doNotSendNotificationsForAutoApprove: [x.doNotSendNotificationsForAutoApprove], doNotSendNotificationsForAutoApprove: [x.doNotSendNotificationsForAutoApprove],
hideRequestsUsers: [x.hideRequestsUsers],
}); });
}); });
} }
@ -41,7 +42,7 @@ export class OmbiComponent implements OnInit {
} }
const result = <IOmbiSettings>form.value; const result = <IOmbiSettings>form.value;
if(result.baseUrl.length > 0) { if(result.baseUrl && result.baseUrl.length > 0) {
if(!result.baseUrl.startsWith("/")) { if(!result.baseUrl.startsWith("/")) {
this.notificationService.error("Please ensure your base url starts with a '/'"); this.notificationService.error("Please ensure your base url starts with a '/'");
return; return;

@ -7,7 +7,8 @@ import { ClipboardModule } from "ngx-clipboard/dist";
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services"; import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, NotificationMessageService, PlexService, RadarrService,
SonarrService, TesterService, ValidationService } from "../services";
import { PipeModule } from "../pipes/pipe.module"; import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component"; import { AboutComponent } from "./about/about.component";
@ -19,6 +20,7 @@ import { EmbyComponent } from "./emby/emby.component";
import { IssuesComponent } from "./issues/issues.component"; import { IssuesComponent } from "./issues/issues.component";
import { JobsComponent } from "./jobs/jobs.component"; import { JobsComponent } from "./jobs/jobs.component";
import { LandingPageComponent } from "./landingpage/landingpage.component"; import { LandingPageComponent } from "./landingpage/landingpage.component";
import { MassEmailComponent } from "./massemail/massemail.component";
import { DiscordComponent } from "./notifications/discord.component"; import { DiscordComponent } from "./notifications/discord.component";
import { EmailNotificationComponent } from "./notifications/emailnotification.component"; import { EmailNotificationComponent } from "./notifications/emailnotification.component";
import { MattermostComponent } from "./notifications/mattermost.component"; import { MattermostComponent } from "./notifications/mattermost.component";
@ -66,6 +68,7 @@ const routes: Routes = [
{ path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] }, { path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] },
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] }, { path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] }, { path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] },
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
]; ];
@NgModule({ @NgModule({
@ -114,6 +117,7 @@ const routes: Routes = [
IssuesComponent, IssuesComponent,
AuthenticationComponent, AuthenticationComponent,
MobileComponent, MobileComponent,
MassEmailComponent,
], ],
exports: [ exports: [
RouterModule, RouterModule,
@ -131,6 +135,7 @@ const routes: Routes = [
PlexService, PlexService,
EmbyService, EmbyService,
MobileService, MobileService,
NotificationMessageService,
], ],
}) })

@ -55,6 +55,7 @@
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/MassEmail']">Mass Email</a></li>
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>--> <!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>-->
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Discord']">Discord</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Discord']">Discord</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Slack']">Slack</a></li> <li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Slack']">Slack</a></li>

@ -16,44 +16,45 @@
</td> </td>
</a> </a>
</th> </th>
<th (click)="setOrder('u.userName')"> <th (click)="setOrder('userName')">
<a> <a>
Username Username
</a> </a>
<span *ngIf="order === 'u.userName'"> <span *ngIf="order === 'userName'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('u.alias')"> <th (click)="setOrder('alias')">
<a> <a>
Alias Alias
</a> </a>
<span *ngIf="order === 'u.alias'"> <span *ngIf="order === 'alias'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('u.emailAddress')"> <th (click)="setOrder('emailAddress')">
<a> <a>
Email Email
</a> </a>
<span *ngIf="order === 'u.emailAddress'"> <span *ngIf="order === 'emailAddress'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span>
<span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th> <th>
Roles Roles
</th> </th>
<th (click)="setOrder('u.lastLoggedIn')"> <th (click)="setOrder('lastLoggedIn')">
<a> Last Logged In</a> <a> Last Logged In</a>
<span *ngIf="order === 'u.lastLoggedIn'"> <span *ngIf="order === 'lastLoggedIn'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>
<th (click)="setOrder('u.userType')"> <th (click)="setOrder('userType')">
<a> <a>
User Type User Type
</a> </a>
<span *ngIf="order === 'u.userType'"> <span *ngIf="order === 'userType'">
<span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span> <span [hidden]="reverse"><i class="fa fa-arrow-down" aria-hidden="true"></i></span><span [hidden]="!reverse"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</span> </span>
</th> </th>

@ -13,7 +13,7 @@ export class UserManagementComponent implements OnInit {
public emailSettings: IEmailNotificationSettings; public emailSettings: IEmailNotificationSettings;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public order: string = "u.userName"; public order: string = "userName";
public reverse = false; public reverse = false;
public showBulkEdit = false; public showBulkEdit = false;

@ -1,5 +1,4 @@
@import '../base.scss'; $primary-colour: #df691a;
$primary-colour: #df691a;
$primary-colour-outline: #ff761b; $primary-colour-outline: #ff761b;
$bg-colour: #333333; $bg-colour: #333333;
$bg-colour-disabled: #252424; $bg-colour-disabled: #252424;

@ -22,6 +22,49 @@ $i: !important;
} }
} }
@media only screen and (max-width: 768px) {
.table-usermanagement {
/* Force table to not be like tables anymore */
display: block;
thead, tbody, th, td, tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50% $i;
min-height: 25px;
}
td:before {
/* Now like a table header */
position: absolute;
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
}
/* Label the data */
.td-labelled:before {
content: attr(data-label)
}
}
}
@media (max-width: 48em) { @media (max-width: 48em) {
.home { .home {
padding-top: 1rem; padding-top: 1rem;
@ -841,8 +884,13 @@ textarea {
border: 1px solid $form-color-lighter; border: 1px solid $form-color-lighter;
} }
.ui-treetable tfoot td, .ui-treetable th {
text-align: left;
}
.ui-treetable tbody td { .ui-treetable tbody td {
white-space: inherit; white-space: inherit;
overflow: visible;
} }
table a:not(.btn) { table a:not(.btn) {
@ -897,49 +945,6 @@ a > h4:hover {
padding-top:15px; padding-top:15px;
} }
@media only screen and (max-width: 768px) {
.table-usermanagement {
/* Force table to not be like tables anymore */
display: block;
thead, tbody, th, td, tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50% $i;
min-height: 25px;
}
td:before {
/* Now like a table header */
position: absolute;
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
}
/* Label the data */
.td-labelled:before {
content: attr(data-label)
}
}
}
.searchWidth { .searchWidth {
width: 94%; width: 94%;
} }

@ -46,5 +46,27 @@ namespace Ombi.Controllers.External
{ {
return await SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); return await SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
} }
/// <summary>
/// Gets the Sonarr profiles.
/// </summary>
/// <returns></returns>
[HttpGet("Profiles")]
public async Task<IEnumerable<SonarrProfile>> GetProfiles()
{
var settings = await SonarrSettings.GetSettingsAsync();
return await SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Sonarr root folders.
/// </summary>
/// <returns></returns>
[HttpGet("RootFolders")]
public async Task<IEnumerable<SonarrRootFolder>> GetRootFolders()
{
var settings = await SonarrSettings.GetSettingsAsync();
return await SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
}
} }
} }

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using AutoMapper; using AutoMapper;
using Hangfire; using Hangfire;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -472,8 +472,18 @@ namespace Ombi.Controllers
// Get the roles // Get the roles
var userRoles = await UserManager.GetRolesAsync(user); var userRoles = await UserManager.GetRolesAsync(user);
// Am I modifying myself?
var modifyingSelf = user.UserName.Equals(User.Identity.Name, StringComparison.CurrentCultureIgnoreCase);
foreach (var role in userRoles) foreach (var role in userRoles)
{ {
if (modifyingSelf && role.Equals(OmbiRoles.Admin))
{
// We do not want to remove the admin role from yourself, this must be an accident
var claim = ui.Claims.FirstOrDefault(x => x.Value == OmbiRoles.Admin && x.Enabled);
ui.Claims.Remove(claim);
continue;
}
await UserManager.RemoveFromRoleAsync(user, role); await UserManager.RemoveFromRoleAsync(user, role);
} }
@ -613,25 +623,54 @@ namespace Ombi.Controllers
return defaultMessage; return defaultMessage;
} }
// We have the user
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
// We now need to email the user with this token
var emailSettings = await EmailSettings.GetSettingsAsync();
var customizationSettings = await CustomizationSettings.GetSettingsAsync(); var customizationSettings = await CustomizationSettings.GetSettingsAsync();
var appName = (string.IsNullOrEmpty(customizationSettings.ApplicationName) var appName = (string.IsNullOrEmpty(customizationSettings.ApplicationName)
? "Ombi" ? "Ombi"
: customizationSettings.ApplicationName); : customizationSettings.ApplicationName);
var emailSettings = await EmailSettings.GetSettingsAsync();
customizationSettings.AddToUrl("/token?token="); customizationSettings.AddToUrl("/token?token=");
var url = customizationSettings.ApplicationUrl; var url = customizationSettings.ApplicationUrl;
await EmailProvider.SendAdHoc(new NotificationMessage if (user.UserType == UserType.PlexUser)
{
await EmailProvider.SendAdHoc(new NotificationMessage
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message =
$"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
$"<a href=\"https://www.plex.tv/sign-in/password-reset/\"> Reset </a>"
}, emailSettings);
}
else if (user.UserType == UserType.EmbyUser && user.IsEmbyConnect)
{ {
To = user.Email, await EmailProvider.SendAdHoc(new NotificationMessage
Subject = $"{appName} Password Reset", {
Message = $"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" + To = user.Email,
$"<a href=\"{url}{token}\"> Reset </a>" Subject = $"{appName} Password Reset",
}, emailSettings); Message =
$"You recently made a request to reset your {appName} account.<br/><br/>" +
$"To reset your password you need to go to <a href=\"https://emby.media/community/index.php\">Emby.Media</a> and then click on your Username > Edit Profile > Email and Password"
}, emailSettings);
}
else
{
// We have the user
var token = await UserManager.GeneratePasswordResetTokenAsync(user);
var encodedToken = WebUtility.UrlEncode(token);
await EmailProvider.SendAdHoc(new NotificationMessage
{
To = user.Email,
Subject = $"{appName} Password Reset",
Message =
$"You recently made a request to reset your {appName} account. Please click the link below to complete the process.<br/><br/>" +
$"<a href=\"{url}{encodedToken}\"> Reset </a>"
}, emailSettings);
}
return defaultMessage; return defaultMessage;
} }

@ -17,7 +17,7 @@ namespace Ombi.Controllers
} }
private ILogger Logger { get; } private ILogger Logger { get; }
private const string Message = "Exception: {0} at {1}. Stacktrade {2}"; private const string Message = "Exception: {0} at {1}. Stacktrace {2}";
[HttpPost] [HttpPost]
public IActionResult Log([FromBody]UiLoggingModel l) public IActionResult Log([FromBody]UiLoggingModel l)

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

Loading…
Cancel
Save