pull/130/head^2
Shannon Barrett 9 years ago
commit 208c45340b

@ -0,0 +1,22 @@
If this is a bug report please make sure you have filled the following in:
(If it's not a bug and a feature request then just remove the below)
#### Plex Requests.Net Version:
#### Operating System:
#### Mono Version:
#### Applicable Logs (from `/logs/` directory or the Admin page):
```
Logs go here (Please make sure you remove any personal information from the logs)
```
#### Reproduction Steps:
Please include any steps to reproduce the issue, this the request that is causing the problem etc.

@ -0,0 +1,35 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IHeadphonesApi.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;
namespace PlexRequests.Api.Interfaces
{
public interface IHeadphonesApi
{
bool AddAlbum(string apiKey, Uri baseUrl, string albumId);
}
}

@ -0,0 +1,37 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: IMusicBrainzApi.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 PlexRequests.Api.Models.Music;
namespace PlexRequests.Api.Interfaces
{
public interface IMusicBrainzApi
{
MusicBrainzSearchResults SearchAlbum(string searchTerm);
MusicBrainzCoverArt GetCoverArt(string releaseId);
MusicBrainzReleaseInfo GetAlbum(string releaseId);
}
}

@ -47,6 +47,8 @@
<ItemGroup> <ItemGroup>
<Compile Include="IApiRequest.cs" /> <Compile Include="IApiRequest.cs" />
<Compile Include="ICouchPotatoApi.cs" /> <Compile Include="ICouchPotatoApi.cs" />
<Compile Include="IHeadphonesApi.cs" />
<Compile Include="IMusicBrainzApi.cs" />
<Compile Include="IPlexApi.cs" /> <Compile Include="IPlexApi.cs" />
<Compile Include="IPushbulletApi.cs" /> <Compile Include="IPushbulletApi.cs" />
<Compile Include="IPushoverApi.cs" /> <Compile Include="IPushoverApi.cs" />

@ -0,0 +1,45 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HeadphonesAlbumSearchResult.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
namespace PlexRequests.Api.Models.Music
{
public class HeadphonesAlbumSearchResult
{
public string rgid { get; set; }
public string albumurl { get; set; }
public string tracks { get; set; }
public string date { get; set; }
public string id { get; set; } // Artist ID
public string rgtype { get; set; }
public string title { get; set; }
public string url { get; set; }
public string country { get; set; }
public string albumid { get; set; } // AlbumId
public int score { get; set; }
public string uniquename { get; set; }
public string formats { get; set; }
}
}

@ -0,0 +1,37 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HeadphonesSearchResult.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
namespace PlexRequests.Api.Models.Music
{
public class HeadphonesArtistSearchResult
{
public string url { get; set; } // MusicBrainz url
public int score { get; set; } // Search Match score?
public string name { get; set; } // Artist Name
public string uniquename { get; set; } // Artist Unique Name
public string id { get; set; } // Artist Unique ID for MusicBrainz
}
}

@ -0,0 +1,55 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: MusicBrainzCoverArt.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;
namespace PlexRequests.Api.Models.Music
{
public class Thumbnails
{
public string large { get; set; }
public string small { get; set; }
}
public class Image
{
public List<string> types { get; set; }
public bool front { get; set; }
public bool back { get; set; }
public int edit { get; set; }
public string image { get; set; }
public string comment { get; set; }
public bool approved { get; set; }
public string id { get; set; }
public Thumbnails thumbnails { get; set; }
}
public class MusicBrainzCoverArt
{
public List<Image> images { get; set; }
public string release { get; set; }
}
}

@ -0,0 +1,66 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: MusicBrainzReleaseInfo.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 Newtonsoft.Json;
namespace PlexRequests.Api.Models.Music
{
public class CoverArtArchive
{
public int count { get; set; }
public bool back { get; set; }
public bool artwork { get; set; }
public bool front { get; set; }
public bool darkened { get; set; }
}
public class MusicBrainzReleaseInfo
{
public string date { get; set; }
public string status { get; set; }
public string asin { get; set; }
public string title { get; set; }
public string quality { get; set; }
public string country { get; set; }
public string packaging { get; set; }
[JsonProperty(PropertyName = "text-representation")]
public TextRepresentation TextRepresentation { get; set; }
[JsonProperty(PropertyName = "cover-art-archive")]
public CoverArtArchive CoverArtArchive { get; set; }
public string barcode { get; set; }
public string disambiguation { get; set; }
[JsonProperty(PropertyName = "release-events")]
public List<ReleaseEvent> ReleaseRvents { get; set; }
public string id { get; set; }
}
}

@ -0,0 +1,152 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: MusicBrainzSearchResults.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 Newtonsoft.Json;
namespace PlexRequests.Api.Models.Music
{
public class TextRepresentation
{
public string language { get; set; }
public string script { get; set; }
}
public class Alias
{
[JsonProperty(PropertyName = "sort-name")]
public string SortName { get; set; }
public string name { get; set; }
public object locale { get; set; }
public string type { get; set; }
public object primary { get; set; }
[JsonProperty(PropertyName = "begin-date")]
public object BeginDate { get; set; }
[JsonProperty(PropertyName = "end-date")]
public object EndDate { get; set; }
}
public class Artist
{
public string id { get; set; }
public string name { get; set; }
[JsonProperty(PropertyName = "sort-date")]
public string SortName { get; set; }
public string disambiguation { get; set; }
public List<Alias> aliases { get; set; }
}
public class ArtistCredit
{
public Artist artist { get; set; }
}
public class ReleaseGroup
{
public string id { get; set; }
[JsonProperty(PropertyName = "primary-type")]
public string PrimaryType { get; set; }
[JsonProperty(PropertyName = "secondary-types")]
public List<string> SecondaryTypes { get; set; }
}
public class Area
{
public string id { get; set; }
public string name { get; set; }
[JsonProperty(PropertyName = "sort-name")]
public string SortName { get; set; }
[JsonProperty(PropertyName = "iso-3166-1-codes")]
public List<string> ISO31661Codes { get; set; }
}
public class ReleaseEvent
{
public string date { get; set; }
public Area area { get; set; }
}
public class Label
{
public string id { get; set; }
public string name { get; set; }
}
public class LabelInfo
{
[JsonProperty(PropertyName = "catalog-number")]
public string CatalogNumber { get; set; }
public Label label { get; set; }
}
public class Medium
{
public string format { get; set; }
[JsonProperty(PropertyName = "disc-count")]
public int DiscCount { get; set; }
[JsonProperty(PropertyName = "catalog-number")]
public int CatalogNumber { get; set; }
}
public class Release
{
public string id { get; set; }
public string score { get; set; }
public int count { get; set; }
public string title { get; set; }
public string status { get; set; }
public string disambiguation { get; set; }
public string packaging { get; set; }
[JsonProperty(PropertyName = "text-representation")]
public TextRepresentation TextRepresentation { get; set; }
[JsonProperty(PropertyName = "artist-credit")]
public List<ArtistCredit> ArtistCredit { get; set; }
[JsonProperty(PropertyName = "release-group")]
public ReleaseGroup ReleaseGroup { get; set; }
public string date { get; set; }
public string country { get; set; }
[JsonProperty(PropertyName = "release-events")]
public List<ReleaseEvent> ReleaseEvents { get; set; }
public string barcode { get; set; }
public string asin { get; set; }
[JsonProperty(PropertyName = "label-info")]
public List<LabelInfo> LabelInfo { get; set; }
[JsonProperty(PropertyName = "track-count")]
public int TrackCount { get; set; }
public List<Medium> media { get; set; }
}
public class MusicBrainzSearchResults
{
public string created { get; set; }
public int count { get; set; }
public int offset { get; set; }
public List<Release> releases { get; set; }
}
}

@ -48,6 +48,11 @@
<Compile Include="Movie\CouchPotatoAdd.cs" /> <Compile Include="Movie\CouchPotatoAdd.cs" />
<Compile Include="Movie\CouchPotatoProfiles.cs" /> <Compile Include="Movie\CouchPotatoProfiles.cs" />
<Compile Include="Movie\CouchPotatoStatus.cs" /> <Compile Include="Movie\CouchPotatoStatus.cs" />
<Compile Include="Music\HeadphonesAlbumSearchResult.cs" />
<Compile Include="Music\HeadphonesArtistSearchResult.cs" />
<Compile Include="Music\MusicBrainzCoverArt.cs" />
<Compile Include="Music\MusicBrainzReleaseInfo.cs" />
<Compile Include="Music\MusicBrainzSearchResults.cs" />
<Compile Include="Notifications\PushbulletPush.cs" /> <Compile Include="Notifications\PushbulletPush.cs" />
<Compile Include="Notifications\PushbulletResponse.cs" /> <Compile Include="Notifications\PushbulletResponse.cs" />
<Compile Include="Notifications\PushoverResponse.cs" /> <Compile Include="Notifications\PushoverResponse.cs" />
@ -66,6 +71,7 @@
<Compile Include="SickRage\SickRageStatus.cs" /> <Compile Include="SickRage\SickRageStatus.cs" />
<Compile Include="SickRage\SickRageTvAdd.cs" /> <Compile Include="SickRage\SickRageTvAdd.cs" />
<Compile Include="Sonarr\SonarrAddSeries.cs" /> <Compile Include="Sonarr\SonarrAddSeries.cs" />
<Compile Include="Sonarr\SonarrError.cs" />
<Compile Include="Sonarr\SonarrProfile.cs" /> <Compile Include="Sonarr\SonarrProfile.cs" />
<Compile Include="Sonarr\SystemStatus.cs" /> <Compile Include="Sonarr\SystemStatus.cs" />
<Compile Include="Tv\Authentication.cs" /> <Compile Include="Tv\Authentication.cs" />

@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json;
namespace PlexRequests.Api.Models.Sonarr namespace PlexRequests.Api.Models.Sonarr
{ {
public class Season public class Season
@ -23,6 +25,8 @@ namespace PlexRequests.Api.Models.Sonarr
public string imdbId { get; set; } public string imdbId { get; set; }
public string titleSlug { get; set; } public string titleSlug { get; set; }
public int id { get; set; } public int id { get; set; }
[JsonIgnore]
public string ErrorMessage { get; set; }
} }
public class AddOptions public class AddOptions

@ -0,0 +1,36 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SonarrError.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
namespace PlexRequests.Api.Models.Sonarr
{
public class SonarrError
{
public string propertyName { get; set; }
public string errorMessage { get; set; }
public string attemptedValue { get; set; }
public string[] formattedMessageArguments { get; set; }
}
}

@ -26,13 +26,9 @@
#endregion #endregion
using System; using System;
using System.IO; using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog; using NLog;
@ -96,20 +92,13 @@ namespace PlexRequests.Api
throw new ApplicationException(message, response.ErrorException); throw new ApplicationException(message, response.ErrorException);
} }
try
{ var json = JsonConvert.DeserializeObject<T>(response.Content);
var json = JsonConvert.DeserializeObject<T>(response.Content);
return json; return json;
}
catch (Exception e)
{
Log.Fatal(e);
Log.Info(response.Content);
throw;
}
} }
public T DeserializeXml<T>(string input) private T DeserializeXml<T>(string input)
where T : class where T : class
{ {
var ser = new XmlSerializer(typeof(T)); var ser = new XmlSerializer(typeof(T));

@ -0,0 +1,74 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HeadphonesApi.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;
using System.Collections.Generic;
using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Music;
using RestSharp;
namespace PlexRequests.Api
{
public class HeadphonesApi : IHeadphonesApi
{
public HeadphonesApi()
{
Api = new ApiRequest();
}
private ApiRequest Api { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public bool AddAlbum(string apiKey, Uri baseUrl, string albumId)
{
Log.Trace("Adding album: {0}", albumId);
var request = new RestRequest
{
Resource = "/api?cmd=addAlbum&id={albumId}",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddUrlSegment("albumId", albumId);
try
{
//var result = Api.Execute<string>(request, baseUrl);
return false;
}
catch (JsonSerializationException jse)
{
Log.Warn(jse);
return false; // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
}
}

@ -0,0 +1,114 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: MusicBrainzApi.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;
using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Music;
using RestSharp;
namespace PlexRequests.Api
{
public class MusicBrainzApi : IMusicBrainzApi
{
public MusicBrainzApi()
{
Api = new ApiRequest();
}
private ApiRequest Api { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private readonly Uri BaseUri = new Uri("http://musicbrainz.org/ws/2/");
public MusicBrainzSearchResults SearchAlbum(string searchTerm)
{
Log.Trace("Searching for album: {0}", searchTerm);
var request = new RestRequest
{
Resource = "release/?query={searchTerm}&fmt=json",
Method = Method.GET
};
request.AddUrlSegment("searchTerm", searchTerm);
try
{
return Api.ExecuteJson<MusicBrainzSearchResults>(request, BaseUri);
}
catch (JsonSerializationException jse)
{
Log.Warn(jse);
return new MusicBrainzSearchResults(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public MusicBrainzReleaseInfo GetAlbum(string releaseId)
{
Log.Trace("Getting album: {0}", releaseId);
var request = new RestRequest
{
Resource = "release/{albumId}?fmt=json",
Method = Method.GET
};
request.AddUrlSegment("albumId", releaseId);
try
{
return Api.Execute<MusicBrainzReleaseInfo>(request, BaseUri);
}
catch (JsonSerializationException jse)
{
Log.Warn(jse);
return new MusicBrainzReleaseInfo(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public MusicBrainzCoverArt GetCoverArt(string releaseId)
{
Log.Trace("Getting cover art for release: {0}", releaseId);
var request = new RestRequest
{
Resource = "release/{releaseId}",
Method = Method.GET
};
request.AddUrlSegment("releaseId", releaseId);
try
{
return Api.Execute<MusicBrainzCoverArt>(request, new Uri("http://coverartarchive.org/"));
}
catch (Exception e)
{
Log.Warn(e);
return new MusicBrainzCoverArt(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
}
}

@ -66,6 +66,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ApiRequest.cs" /> <Compile Include="ApiRequest.cs" />
<Compile Include="MusicBrainzApi.cs" />
<Compile Include="MockApiData.Designer.cs"> <Compile Include="MockApiData.Designer.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
@ -75,6 +76,7 @@
<Compile Include="PushoverApi.cs" /> <Compile Include="PushoverApi.cs" />
<Compile Include="PushbulletApi.cs" /> <Compile Include="PushbulletApi.cs" />
<Compile Include="SickrageApi.cs" /> <Compile Include="SickrageApi.cs" />
<Compile Include="HeadphonesApi.cs" />
<Compile Include="SonarrApi.cs" /> <Compile Include="SonarrApi.cs" />
<Compile Include="CouchPotatoApi.cs" /> <Compile Include="CouchPotatoApi.cs" />
<Compile Include="MovieBase.cs" /> <Compile Include="MovieBase.cs" />

@ -27,9 +27,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using NLog; using NLog;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Sonarr; using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Helpers;
using RestSharp; using RestSharp;
namespace PlexRequests.Api namespace PlexRequests.Api
@ -56,7 +61,8 @@ namespace PlexRequests.Api
public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl) public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl)
{ {
Log.Debug("Adding series {0}", title);
Log.Debug("Seasons = {0}, out of {1} seasons", seasons.DumpJson(), seasonCount);
var request = new RestRequest var request = new RestRequest
{ {
Resource = "/api/Series?", Resource = "/api/Series?",
@ -74,7 +80,6 @@ namespace PlexRequests.Api
rootFolderPath = rootPath rootFolderPath = rootPath
}; };
for (var i = 1; i <= seasonCount; i++) for (var i = 1; i <= seasonCount; i++)
{ {
var season = new Season var season = new Season
@ -85,12 +90,25 @@ namespace PlexRequests.Api
options.seasons.Add(season); options.seasons.Add(season);
} }
Log.Debug("Sonarr API Options:");
Log.Debug(options.DumpJson());
request.AddHeader("X-Api-Key", apiKey); request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options); request.AddJsonBody(options);
var obj = Api.ExecuteJson<SonarrAddSeries>(request, baseUrl); SonarrAddSeries result;
try
{
result = Api.ExecuteJson<SonarrAddSeries>(request, baseUrl);
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
var error = Api.ExecuteJson<SonarrError>(request, baseUrl);
result = new SonarrAddSeries { ErrorMessage = error.errorMessage };
}
return obj; return result;
} }
public SystemStatus SystemStatus(string apiKey, Uri baseUrl) public SystemStatus SystemStatus(string apiKey, Uri baseUrl)

@ -33,7 +33,9 @@ namespace PlexRequests.Core
public interface IRequestService public interface IRequestService
{ {
long AddRequest(RequestedModel model); long AddRequest(RequestedModel model);
bool CheckRequest(int providerId); RequestedModel CheckRequest(int providerId);
RequestedModel CheckRequest(string musicId);
void DeleteRequest(RequestedModel request); void DeleteRequest(RequestedModel request);
bool UpdateRequest(RequestedModel model); bool UpdateRequest(RequestedModel model);
RequestedModel Get(int id); RequestedModel Get(int id);

@ -52,16 +52,24 @@ namespace PlexRequests.Core
// TODO Keep an eye on this, since we are now doing 2 DB update for 1 single request, inserting and then updating // TODO Keep an eye on this, since we are now doing 2 DB update for 1 single request, inserting and then updating
model.Id = (int)id; model.Id = (int)id;
entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id }; entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id, MusicId = model.MusicBrainzId};
var result = Repo.Update(entity); var result = Repo.Update(entity);
return result ? id : -1; return result ? id : -1;
} }
public bool CheckRequest(int providerId) public RequestedModel CheckRequest(int providerId)
{ {
var blobs = Repo.GetAll(); var blobs = Repo.GetAll();
return blobs.Any(x => x.ProviderId == providerId); var blob = blobs.FirstOrDefault(x => x.ProviderId == providerId);
return blob != null ? ByteConverterHelper.ReturnObject<RequestedModel>(blob.Content) : null;
}
public RequestedModel CheckRequest(string musicId)
{
var blobs = Repo.GetAll();
var blob = blobs.FirstOrDefault(x => x.MusicId == musicId);
return blob != null ? ByteConverterHelper.ReturnObject<RequestedModel>(blob.Content) : null;
} }
public void DeleteRequest(RequestedModel request) public void DeleteRequest(RequestedModel request)
@ -79,6 +87,10 @@ namespace PlexRequests.Core
public RequestedModel Get(int id) public RequestedModel Get(int id)
{ {
var blob = Repo.Get(id); var blob = Repo.Get(id);
if (blob == null)
{
return new RequestedModel();
}
var model = ByteConverterHelper.ReturnObject<RequestedModel>(blob.Content); var model = ByteConverterHelper.ReturnObject<RequestedModel>(blob.Content);
return model; return model;
} }

@ -46,6 +46,10 @@
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> <HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit, Version=0.19.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Octokit, Version=0.19.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath> <HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath>
<Private>True</Private> <Private>True</Private>
@ -74,6 +78,7 @@
<Compile Include="Models\StatusModel.cs" /> <Compile Include="Models\StatusModel.cs" />
<Compile Include="Models\UserProperties.cs" /> <Compile Include="Models\UserProperties.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" /> <Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />
<Compile Include="SettingModels\PushoverNotificationSettings.cs" /> <Compile Include="SettingModels\PushoverNotificationSettings.cs" />
<Compile Include="SettingModels\PushBulletNotificationSettings.cs" /> <Compile Include="SettingModels\PushBulletNotificationSettings.cs" />
<Compile Include="SettingModels\EmailNotificationSettings.cs" /> <Compile Include="SettingModels\EmailNotificationSettings.cs" />

@ -0,0 +1,58 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: CouchPotatoSettings.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;
using Newtonsoft.Json;
using PlexRequests.Helpers;
namespace PlexRequests.Core.SettingModels
{
public class HeadphonesSettings : Settings
{
public bool Enabled { get; set; }
public string Ip { get; set; }
public int Port { get; set; }
public string ApiKey { get; set; }
public bool Ssl { get; set; }
public string SubDir { get; set; }
[JsonIgnore]
public Uri FullUri
{
get
{
if (!string.IsNullOrEmpty(SubDir))
{
var formattedSubDir = Ip.ReturnUriWithSubDir(Port, Ssl, SubDir);
return formattedSubDir;
}
var formatted = Ip.ReturnUri(Port, Ssl);
return formatted;
}
}
}
}

@ -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 Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace PlexRequests.Core.SettingModels namespace PlexRequests.Core.SettingModels
{ {
public class PlexRequestSettings : Settings public class PlexRequestSettings : Settings
@ -32,8 +36,33 @@ namespace PlexRequests.Core.SettingModels
public bool SearchForMovies { get; set; } public bool SearchForMovies { get; set; }
public bool SearchForTvShows { get; set; } public bool SearchForTvShows { get; set; }
public bool SearchForMusic { get; set; }
public bool RequireMovieApproval { get; set; } public bool RequireMovieApproval { get; set; }
public bool RequireTvShowApproval { get; set; } public bool RequireTvShowApproval { get; set; }
public bool RequireMusicApproval { get; set; }
public bool UsersCanViewOnlyOwnRequests { get; set; }
public int WeeklyRequestLimit { get; set; } public int WeeklyRequestLimit { get; set; }
public string NoApprovalUsers { get; set; }
[JsonIgnore]
public List<string> ApprovalWhiteList
{
get
{
var users = new List<string>();
if (string.IsNullOrEmpty(NoApprovalUsers))
{
return users;
}
var splitUsers = NoApprovalUsers.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var user in splitUsers)
{
if (!string.IsNullOrWhiteSpace(user))
users.Add(user.Trim());
}
return users;
}
}
} }
} }

@ -30,6 +30,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Mono.Data.Sqlite; using Mono.Data.Sqlite;
using NLog;
using PlexRequests.Api; using PlexRequests.Api;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
@ -40,6 +41,9 @@ namespace PlexRequests.Core
{ {
public class Setup public class Setup
{ {
public const int SchemaVersion = 1;
private static Logger Log = LogManager.GetCurrentClassLogger();
private static DbConfiguration Db { get; set; } private static DbConfiguration Db { get; set; }
public string SetupDb() public string SetupDb()
{ {
@ -53,11 +57,40 @@ namespace PlexRequests.Core
} }
MigrateDb(); MigrateDb();
CheckSchema();
return Db.DbConnection().ConnectionString; return Db.DbConnection().ConnectionString;
} }
public static string ConnectionString => Db.DbConnection().ConnectionString; public static string ConnectionString => Db.DbConnection().ConnectionString;
private void CheckSchema()
{
var connection = Db.DbConnection();
var schema = connection.GetSchemaVersion();
if (schema == null)
{
connection.CreateSchema(); // Set the default.
schema = connection.GetSchemaVersion();
}
var version = schema.SchemaVersion;
if (version == 0)
{
connection.UpdateSchemaVersion(SchemaVersion);
try
{
TableCreation.AlterTable(Db.DbConnection(), "RequestBlobs", "ADD COLUMN", "MusicId", false, "TEXT");
}
catch (Exception e)
{
Log.Error("Tried updating the schema to version 1");
Log.Error(e);
}
return;
}
}
private void CreateDefaultSettingsPage() private void CreateDefaultSettingsPage()
{ {
var defaultSettings = new PlexRequestSettings var defaultSettings = new PlexRequestSettings
@ -72,8 +105,9 @@ namespace PlexRequests.Core
s.SaveSettings(defaultSettings); s.SaveSettings(defaultSettings);
} }
private void MigrateDb() // TODO: Remove when no longer needed private void MigrateDb() // TODO: Remove in v1.7
{ {
var result = new List<long>(); var result = new List<long>();
RequestedModel[] requestedModels; RequestedModel[] requestedModels;
var repo = new GenericRepository<RequestedModel>(Db, new MemoryCacheProvider()); var repo = new GenericRepository<RequestedModel>(Db, new MemoryCacheProvider());
@ -121,7 +155,7 @@ namespace PlexRequests.Core
result.Add(id); result.Add(id);
} }
foreach (var source in requestedModels.Where(x => x.Type== RequestType.Movie)) foreach (var source in requestedModels.Where(x => x.Type == RequestType.Movie))
{ {
var id = jsonRepo.AddRequest(source); var id = jsonRepo.AddRequest(source);
result.Add(id); result.Add(id);

@ -3,6 +3,7 @@
<package id="Nancy" version="1.4.3" targetFramework="net452" /> <package id="Nancy" version="1.4.3" targetFramework="net452" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net452" /> <package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net452" />
<package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" /> <package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" />
<package id="NLog" version="4.2.3" targetFramework="net46" />
<package id="Octokit" version="0.19.0" targetFramework="net46" /> <package id="Octokit" version="0.19.0" targetFramework="net46" />
<package id="valueinjecter" version="3.1.1.2" targetFramework="net452" /> <package id="valueinjecter" version="3.1.1.2" targetFramework="net452" />
</packages> </packages>

@ -0,0 +1,26 @@
using System;
using System.Linq;
namespace PlexRequests.Helpers
{
public static class DateTimeHelper
{
public static DateTimeOffset OffsetUTCDateTime(DateTime utcDateTime, int minuteOffset)
{
//TimeSpan ts = TimeSpan.FromMinutes(-minuteOffset);
//return new DateTimeOffset(utcDateTime).ToOffset(ts);
// this is a workaround below to work with MONO
var tzi = FindTimeZoneFromOffset(minuteOffset);
var utcOffset = tzi.GetUtcOffset(utcDateTime);
var newDate = utcDateTime + utcOffset;
return new DateTimeOffset(newDate.Ticks, utcOffset);
}
private static TimeZoneInfo FindTimeZoneFromOffset(int minuteOffset)
{
var tzc = TimeZoneInfo.GetSystemTimeZones();
return tzc.FirstOrDefault(x => x.BaseUtcOffset.TotalMinutes == -minuteOffset);
}
}
}

@ -52,6 +52,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="AssemblyHelper.cs" /> <Compile Include="AssemblyHelper.cs" />
<Compile Include="ByteConverterHelper.cs" /> <Compile Include="ByteConverterHelper.cs" />
<Compile Include="DateTimeHelper.cs" />
<Compile Include="Exceptions\ApplicationSettingsException.cs" /> <Compile Include="Exceptions\ApplicationSettingsException.cs" />
<Compile Include="HtmlRemover.cs" /> <Compile Include="HtmlRemover.cs" />
<Compile Include="ICacheProvider.cs" /> <Compile Include="ICacheProvider.cs" />

@ -32,12 +32,12 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models;
using PlexRequests.Api.Models.Plex; using PlexRequests.Api.Models.Plex;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Exceptions; using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
namespace PlexRequests.Services.Tests namespace PlexRequests.Services.Tests
{ {
@ -66,7 +66,7 @@ namespace PlexRequests.Services.Tests
var requestMock = new Mock<IRequestService>(); var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>(); var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch {Video = new List<Video> {new Video {Title = "title", Year = "2011"} } }; var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "title", Year = "2011" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" }); settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" }); authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
@ -87,7 +87,7 @@ namespace PlexRequests.Services.Tests
var requestMock = new Mock<IRequestService>(); var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>(); var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Directory = new Directory1 {Title = "title", Year = "2013"} }; var searchResult = new PlexSearch { Directory = new Directory1 { Title = "title", Year = "2013" } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" }); settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" }); authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
@ -100,6 +100,27 @@ namespace PlexRequests.Services.Tests
Assert.That(result, Is.True); Assert.That(result, Is.True);
} }
[Test]
public void IsAvailableDirectoryTitleWithoutYearTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Directory = new Directory1 { Title = "title", } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", null);
Assert.That(result, Is.True);
}
[Test] [Test]
public void IsNotAvailableTest() public void IsNotAvailableTest()
{ {
@ -108,7 +129,7 @@ namespace PlexRequests.Services.Tests
var requestMock = new Mock<IRequestService>(); var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>(); var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "wrong tistle", Year = "2011"} } }; var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "wrong title", Year = "2011" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" }); settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" }); authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
@ -121,6 +142,27 @@ namespace PlexRequests.Services.Tests
Assert.That(result, Is.False); Assert.That(result, Is.False);
} }
[Test]
public void IsNotAvailableTestWihtoutYear()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "wrong title" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", null);
Assert.That(result, Is.False);
}
[Test] [Test]
public void IsYearDoesNotMatchTest() public void IsYearDoesNotMatchTest()
{ {
@ -141,5 +183,247 @@ namespace PlexRequests.Services.Tests
Assert.That(result, Is.False); Assert.That(result, Is.False);
} }
[Test]
public void TitleDoesNotMatchTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "title23", Year = "2019" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2019");
Assert.That(result, Is.False);
}
[Test]
public void TitleDoesNotMatchWithoutYearTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "title23" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", null);
Assert.That(result, Is.False);
}
[Test]
public void CheckAndUpdateNoPlexSettingsTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
}
[Test]
public void CheckAndUpdateNoAuthSettingsTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "123" });
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
}
[Test]
public void CheckAndUpdateNoRequestsTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(new List<RequestedModel>());
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
}
[Test]
public void CheckAndUpdateRequestsThatDoNotExistInPlexTest()
{
var requests = new List<RequestedModel> {
new RequestedModel
{
Id = 123,
Title = "title1",
Available = false,
},
new RequestedModel
{
Id=222,
Title = "title3",
Available = false
},
new RequestedModel
{
Id = 333,
Title= "missingTitle",
Available = false
},
new RequestedModel
{
Id= 444,
Title = "already found",
Available = true
}
};
var search = new PlexSearch
{
Video = new List<Video>
{
new Video
{
Title = "Title4",
Year = "2012"
},
new Video
{
Title = "Title2",
}
},
Directory = new Directory1
{
Title = "Title9",
Year = "1978"
}
};
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(requests);
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(search);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Exactly(3));
}
[Test]
public void CheckAndUpdateRequestsAllRequestsTest()
{
var requests = new List<RequestedModel> {
new RequestedModel
{
Id = 123,
Title = "title1",
Available = false,
},
new RequestedModel
{
Id=222,
Title = "title3",
Available = false
},
new RequestedModel
{
Id = 333,
Title= "missingTitle",
Available = false
},
new RequestedModel
{
Id= 444,
Title = "Hi",
Available = false
}
};
var search = new PlexSearch
{
Video = new List<Video>
{
new Video
{
Title = "title1",
Year = "2012"
},
new Video
{
Title = "Title3",
}
,
new Video
{
Title = "Hi",
}
},
Directory = new Directory1
{
Title = "missingTitle",
Year = "1978"
}
};
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(requests);
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(search);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Once);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Exactly(4));
}
} }
} }

@ -84,6 +84,10 @@
<Project>{566EFA49-68F8-4716-9693-A6B3F2624DEA}</Project> <Project>{566EFA49-68F8-4716-9693-A6B3F2624DEA}</Project>
<Name>PlexRequests.Services</Name> <Name>PlexRequests.Services</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\PlexRequests.Store\PlexRequests.Store.csproj">
<Project>{92433867-2B7B-477B-A566-96C382427525}</Project>
<Name>PlexRequests.Store</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Choose> <Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">

@ -48,9 +48,12 @@ namespace PlexRequests.Services
{ {
public AvailabilityUpdateService() public AvailabilityUpdateService()
{ {
var memCache = new MemoryCacheProvider();
var dbConfig = new DbConfiguration(new SqliteFactory());
var repo = new SettingsJsonRepository(dbConfig, memCache);
ConfigurationReader = new ConfigurationReader(); ConfigurationReader = new ConfigurationReader();
var repo = new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider()); Checker = new PlexAvailabilityChecker(new SettingsServiceV2<PlexSettings>(repo), new SettingsServiceV2<AuthenticationSettings>(repo), new JsonRequestService(new RequestJsonRepository(dbConfig, memCache)), new PlexApi());
Checker = new PlexAvailabilityChecker(new SettingsServiceV2<PlexSettings>(repo), new SettingsServiceV2<AuthenticationSettings>(repo), new JsonRequestService(new RequestJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())), new PlexApi());
HostingEnvironment.RegisterObject(this); HostingEnvironment.RegisterObject(this);
} }

@ -27,6 +27,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Core.SettingModels;
namespace PlexRequests.Services.Interfaces namespace PlexRequests.Services.Interfaces
{ {
@ -35,5 +36,7 @@ namespace PlexRequests.Services.Interfaces
string NotificationName { get; } string NotificationName { get; }
Task NotifyAsync(NotificationModel model); Task NotifyAsync(NotificationModel model);
Task NotifyAsync(NotificationModel model, Settings settings);
} }
} }

@ -27,12 +27,14 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Core.SettingModels;
namespace PlexRequests.Services.Interfaces namespace PlexRequests.Services.Interfaces
{ {
public interface INotificationService public interface INotificationService
{ {
Task Publish(NotificationModel model); Task Publish(NotificationModel model);
Task Publish(NotificationModel model, Settings settings);
void Subscribe(INotification notification); void Subscribe(INotification notification);
void UnSubscribe(INotification notification); void UnSubscribe(INotification notification);

@ -46,24 +46,29 @@ namespace PlexRequests.Services.Notification
private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private ISettingsService<EmailNotificationSettings> EmailNotificationSettings { get; } private ISettingsService<EmailNotificationSettings> EmailNotificationSettings { get; }
private EmailNotificationSettings Settings => GetConfiguration();
public string NotificationName => "EmailMessageNotification"; public string NotificationName => "EmailMessageNotification";
public async Task NotifyAsync(NotificationModel model) public async Task NotifyAsync(NotificationModel model)
{ {
var configuration = GetConfiguration(); var configuration = GetConfiguration();
if (!ValidateConfiguration(configuration)) await NotifyAsync(model, configuration);
{ }
return;
} public async Task NotifyAsync(NotificationModel model, Settings settings)
{
if (settings == null) await NotifyAsync(model);
var emailSettings = (EmailNotificationSettings)settings;
if (!ValidateConfiguration(emailSettings)) return;
switch (model.NotificationType) switch (model.NotificationType)
{ {
case NotificationType.NewRequest: case NotificationType.NewRequest:
await EmailNewRequest(model); await EmailNewRequest(model, emailSettings);
break; break;
case NotificationType.Issue: case NotificationType.Issue:
await EmailIssue(model); await EmailIssue(model, emailSettings);
break; break;
case NotificationType.RequestAvailable: case NotificationType.RequestAvailable:
throw new NotImplementedException(); throw new NotImplementedException();
@ -74,6 +79,10 @@ namespace PlexRequests.Services.Notification
case NotificationType.AdminNote: case NotificationType.AdminNote:
throw new NotImplementedException(); throw new NotImplementedException();
case NotificationType.Test:
await EmailTest(model, emailSettings);
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }
@ -100,23 +109,23 @@ namespace PlexRequests.Services.Notification
return true; return true;
} }
private async Task EmailNewRequest(NotificationModel model) private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
{ {
var message = new MailMessage var message = new MailMessage
{ {
IsBodyHtml = true, IsBodyHtml = true,
To = { new MailAddress(Settings.RecipientEmail) }, To = { new MailAddress(settings.RecipientEmail) },
Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}", Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}",
From = new MailAddress(Settings.EmailSender), From = new MailAddress(settings.EmailSender),
Subject = $"Plex Requests: New request for {model.Title}!" Subject = $"Plex Requests: New request for {model.Title}!"
}; };
try try
{ {
using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort)) using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
{ {
smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
smtp.EnableSsl = Settings.Ssl; smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false); await smtp.SendMailAsync(message).ConfigureAwait(false);
} }
} }
@ -130,23 +139,53 @@ namespace PlexRequests.Services.Notification
} }
} }
private async Task EmailIssue(NotificationModel model) private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings)
{ {
var message = new MailMessage var message = new MailMessage
{ {
IsBodyHtml = true, IsBodyHtml = true,
To = { new MailAddress(Settings.RecipientEmail) }, To = { new MailAddress(settings.RecipientEmail) },
Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
From = new MailAddress(Settings.RecipientEmail), From = new MailAddress(settings.RecipientEmail),
Subject = $"Plex Requests: New issue for {model.Title}!" Subject = $"Plex Requests: New issue for {model.Title}!"
}; };
try try
{ {
using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort)) using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
{
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false);
}
}
catch (SmtpException smtp)
{
Log.Error(smtp);
}
catch (Exception e)
{
Log.Error(e);
}
}
private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings)
{
var message = new MailMessage
{
IsBodyHtml = true,
To = { new MailAddress(settings.RecipientEmail) },
Body = "This is just a test! Success!",
From = new MailAddress(settings.RecipientEmail),
Subject = "Plex Requests: Test Message!"
};
try
{
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
{ {
smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
smtp.EnableSsl = Settings.Ssl; smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false); await smtp.SendMailAsync(message).ConfigureAwait(false);
} }
} }

@ -32,6 +32,7 @@ using System.Threading.Tasks;
using NLog; using NLog;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Core.SettingModels;
namespace PlexRequests.Services.Notification namespace PlexRequests.Services.Notification
{ {
@ -47,6 +48,13 @@ namespace PlexRequests.Services.Notification
await Task.WhenAll(notificationTasks).ConfigureAwait(false); await Task.WhenAll(notificationTasks).ConfigureAwait(false);
} }
public async Task Publish(NotificationModel model, Settings settings)
{
var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model, settings));
await Task.WhenAll(notificationTasks).ConfigureAwait(false);
}
public void Subscribe(INotification notification) public void Subscribe(INotification notification)
{ {
Observers.TryAdd(notification.NotificationName, notification); Observers.TryAdd(notification.NotificationName, notification);
@ -67,6 +75,19 @@ namespace PlexRequests.Services.Notification
{ {
Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception"); Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception");
} }
}
private static async Task NotifyAsync(INotification notification, NotificationModel model, Settings settings)
{
try
{
await notification.NotifyAsync(model, settings).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception");
}
} }
} }
} }

@ -33,5 +33,6 @@ namespace PlexRequests.Services.Notification
RequestAvailable, RequestAvailable,
RequestApproved, RequestApproved,
AdminNote, AdminNote,
Test
} }
} }

@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification
public string NotificationName => "PushbulletNotification"; public string NotificationName => "PushbulletNotification";
public async Task NotifyAsync(NotificationModel model) public async Task NotifyAsync(NotificationModel model)
{ {
if (!ValidateConfiguration()) var configuration = GetSettings();
{ await NotifyAsync(model, configuration);
return; }
}
public async Task NotifyAsync(NotificationModel model, Settings settings)
{
if (settings == null) await NotifyAsync(model);
var pushSettings = (PushbulletNotificationSettings)settings;
if (!ValidateConfiguration(pushSettings)) return;
switch (model.NotificationType) switch (model.NotificationType)
{ {
case NotificationType.NewRequest: case NotificationType.NewRequest:
await PushNewRequestAsync(model); await PushNewRequestAsync(model, pushSettings);
break; break;
case NotificationType.Issue: case NotificationType.Issue:
await PushIssueAsync(model); await PushIssueAsync(model, pushSettings);
break; break;
case NotificationType.RequestAvailable: case NotificationType.RequestAvailable:
break; break;
@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification
break; break;
case NotificationType.AdminNote: case NotificationType.AdminNote:
break; break;
case NotificationType.Test:
await PushTestAsync(model, pushSettings);
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }
} }
private bool ValidateConfiguration() private bool ValidateConfiguration(PushbulletNotificationSettings settings)
{ {
if (!Settings.Enabled) if (!settings.Enabled)
{ {
return false; return false;
} }
if (string.IsNullOrEmpty(Settings.AccessToken)) if (string.IsNullOrEmpty(settings.AccessToken))
{ {
return false; return false;
} }
@ -93,13 +103,13 @@ namespace PlexRequests.Services.Notification
return SettingsService.GetSettings(); return SettingsService.GetSettings();
} }
private async Task PushNewRequestAsync(NotificationModel model) private async Task PushNewRequestAsync(NotificationModel model, PushbulletNotificationSettings settings)
{ {
var message = $"{model.Title} has been requested by user: {model.User}"; var message = $"{model.Title} has been requested by user: {model.User}";
var pushTitle = $"Plex Requests: {model.Title} has been requested!"; var pushTitle = $"Plex Requests: {model.Title} has been requested!";
try try
{ {
var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
if (result == null) if (result == null)
{ {
Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
@ -111,13 +121,31 @@ namespace PlexRequests.Services.Notification
} }
} }
private async Task PushIssueAsync(NotificationModel model) private async Task PushIssueAsync(NotificationModel model, PushbulletNotificationSettings settings)
{ {
var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}"; var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}";
try try
{ {
var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
if (result != null)
{
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
}
}
catch (Exception e)
{
Log.Error(e);
}
}
private async Task PushTestAsync(NotificationModel model, PushbulletNotificationSettings settings)
{
var message = "This is just a test! Success!";
var pushTitle = "Plex Requests: Test Message!";
try
{
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
if (result != null) if (result != null)
{ {
Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); Log.Error("Pushbullet api returned a null value, the notification did not get pushed");

@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification
public string NotificationName => "PushoverNotification"; public string NotificationName => "PushoverNotification";
public async Task NotifyAsync(NotificationModel model) public async Task NotifyAsync(NotificationModel model)
{ {
if (!ValidateConfiguration()) var configuration = GetSettings();
{ await NotifyAsync(model, configuration);
return; }
}
public async Task NotifyAsync(NotificationModel model, Settings settings)
{
if (settings == null) await NotifyAsync(model);
var pushSettings = (PushoverNotificationSettings)settings;
if (!ValidateConfiguration(pushSettings)) return;
switch (model.NotificationType) switch (model.NotificationType)
{ {
case NotificationType.NewRequest: case NotificationType.NewRequest:
await PushNewRequestAsync(model); await PushNewRequestAsync(model, pushSettings);
break; break;
case NotificationType.Issue: case NotificationType.Issue:
await PushIssueAsync(model); await PushIssueAsync(model, pushSettings);
break; break;
case NotificationType.RequestAvailable: case NotificationType.RequestAvailable:
break; break;
@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification
break; break;
case NotificationType.AdminNote: case NotificationType.AdminNote:
break; break;
case NotificationType.Test:
await PushTestAsync(model, pushSettings);
break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }
} }
private bool ValidateConfiguration() private bool ValidateConfiguration(PushoverNotificationSettings settings)
{ {
if (!Settings.Enabled) if (!settings.Enabled)
{ {
return false; return false;
} }
if (string.IsNullOrEmpty(Settings.AccessToken) || string.IsNullOrEmpty(Settings.UserToken)) if (string.IsNullOrEmpty(settings.AccessToken) || string.IsNullOrEmpty(settings.UserToken))
{ {
return false; return false;
} }
@ -93,12 +103,12 @@ namespace PlexRequests.Services.Notification
return SettingsService.GetSettings(); return SettingsService.GetSettings();
} }
private async Task PushNewRequestAsync(NotificationModel model) private async Task PushNewRequestAsync(NotificationModel model, PushoverNotificationSettings settings)
{ {
var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}"; var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}";
try try
{ {
var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken); var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
if (result?.status != 1) if (result?.status != 1)
{ {
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
@ -110,12 +120,29 @@ namespace PlexRequests.Services.Notification
} }
} }
private async Task PushIssueAsync(NotificationModel model) private async Task PushIssueAsync(NotificationModel model, PushoverNotificationSettings settings)
{ {
var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
try try
{ {
var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken); var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
if (result?.status != 1)
{
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
}
}
catch (Exception e)
{
Log.Error(e);
}
}
private async Task PushTestAsync(NotificationModel model, PushoverNotificationSettings settings)
{
var message = $"Plex Requests: Test Message!";
try
{
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
if (result?.status != 1) if (result?.status != 1)
{ {
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");

@ -24,14 +24,17 @@
// 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; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions; using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Store; using PlexRequests.Store;
@ -52,33 +55,77 @@ namespace PlexRequests.Services
private ISettingsService<AuthenticationSettings> Auth { get; } private ISettingsService<AuthenticationSettings> Auth { get; }
private IRequestService RequestService { get; } private IRequestService RequestService { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
private IPlexApi PlexApi { get; set; } private IPlexApi PlexApi { get; }
public void CheckAndUpdateAll(long check) public void CheckAndUpdateAll(long check)
{ {
Log.Trace("This is check no. {0}", check);
Log.Trace("Getting the settings");
var plexSettings = Plex.GetSettings(); var plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings(); var authSettings = Auth.GetSettings();
Log.Trace("Getting all the requests");
var requests = RequestService.GetAll(); var requests = RequestService.GetAll();
var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray();
if (!ValidateSettings(plexSettings, authSettings, requestedModels)) Log.Trace("Requests Count {0}", requestedModels.Length);
if (!ValidateSettings(plexSettings, authSettings) || !requestedModels.Any())
{ {
Log.Info("Validation of the settings failed or there is no requests.");
return; return;
} }
var modifiedModel = new List<RequestedModel>(); var modifiedModel = new List<RequestedModel>();
foreach (var r in requestedModels) foreach (var r in requestedModels)
{ {
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri); Log.Trace("We are going to see if Plex has the following title: {0}", r.Title);
var result = results.Video.FirstOrDefault(x => x.Title == r.Title); PlexSearch results;
var originalRequest = RequestService.Get(r.Id); try
{
results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri);
}
catch (Exception e)
{
Log.Error("We failed to search Plex for the following request:");
Log.Error(r.DumpJson());
Log.Error(e);
break; // Let's finish processing and not crash the process, there is a reason why we cannot connect.
}
if (results == null)
{
Log.Trace("Could not find any matching result for this title.");
continue;
}
Log.Trace("Search results from Plex for the following request: {0}", r.Title);
Log.Trace(results.DumpJson());
var videoResult = results.Video.FirstOrDefault(x => x.Title == r.Title);
var directoryResult = results.Directory?.Title.Equals(r.Title, StringComparison.CurrentCultureIgnoreCase);
Log.Trace("The result from Plex where the title matches for the video : {0}", videoResult != null);
Log.Trace("The result from Plex where the title matches for the directory : {0}", directoryResult != null);
originalRequest.Available = result != null; var directoryResultVal = directoryResult ?? false;
modifiedModel.Add(originalRequest);
if (videoResult != null || directoryResultVal)
{
r.Available = true;
modifiedModel.Add(r);
continue;
}
Log.Trace("The result from Plex where the title's match was null, so that means the content is not yet in Plex.");
} }
RequestService.BatchUpdate(modifiedModel); Log.Trace("Updating the requests now");
Log.Trace("Requests that will be updates:");
Log.Trace(modifiedModel.SelectMany(x => x.Title).DumpJson());
if(modifiedModel.Any())
{ RequestService.BatchUpdate(modifiedModel);}
} }
/// <summary> /// <summary>
@ -90,45 +137,31 @@ namespace PlexRequests.Services
/// <exception cref="ApplicationSettingsException">The settings are not configured for Plex or Authentication</exception> /// <exception cref="ApplicationSettingsException">The settings are not configured for Plex or Authentication</exception>
public bool IsAvailable(string title, string year) public bool IsAvailable(string title, string year)
{ {
Log.Trace("Checking if the following {0} {1} is available in Plex", title, year);
var plexSettings = Plex.GetSettings(); var plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings(); var authSettings = Auth.GetSettings();
if (!ValidateSettings(plexSettings, authSettings)) if (!ValidateSettings(plexSettings, authSettings))
{ {
Log.Warn("The settings are not configured");
throw new ApplicationSettingsException("The settings are not configured for Plex or Authentication"); throw new ApplicationSettingsException("The settings are not configured for Plex or Authentication");
} }
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
if (!string.IsNullOrEmpty(year)) if (!string.IsNullOrEmpty(year))
{ {
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri); var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase) && x.Year == year);
var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title) && x.Year == year); var directoryTitle = string.Equals(results.Directory?.Title, title, StringComparison.CurrentCultureIgnoreCase) && results.Directory?.Year == year;
var directoryTitle = results.Directory?.Title == title && results.Directory?.Year == year;
return result?.Title != null || directoryTitle; return result?.Title != null || directoryTitle;
} }
else else
{ {
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri); var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase));
var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title)); var directoryTitle = string.Equals(results.Directory?.Title, title, StringComparison.CurrentCultureIgnoreCase);
var directoryTitle = results.Directory?.Title == title;
return result?.Title != null || directoryTitle; return result?.Title != null || directoryTitle;
} }
} }
private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth, IEnumerable<RequestedModel> requests)
{
if (plex.Ip == null || auth.PlexAuthToken == null || requests == null)
{
Log.Warn("A setting is null, Ensure Plex is configured correctly, and we have a Plex Auth token.");
return false;
}
if (!requests.Any())
{
Log.Info("We have no requests to check if they are available on Plex.");
return false;
}
return true;
}
private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth) private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth)
{ {
if (plex?.Ip == null || auth?.PlexAuthToken == null) if (plex?.Ip == null || auth?.PlexAuthToken == null)

@ -32,7 +32,7 @@ namespace PlexRequests.Services
{ {
public class UpdateInterval : IIntervals public class UpdateInterval : IIntervals
{ {
public TimeSpan Notification => TimeSpan.FromMinutes(5); public TimeSpan Notification => TimeSpan.FromMinutes(10);
} }
} }

@ -27,12 +27,11 @@
using System; using System;
using System.Data; using System.Data;
using System.IO; using System.IO;
using System.Windows.Forms;
using Mono.Data.Sqlite; using Mono.Data.Sqlite;
using NLog; using NLog;
using PlexRequests.Helpers;
using PlexRequests.Store.Repository;
namespace PlexRequests.Store namespace PlexRequests.Store
{ {
@ -44,12 +43,14 @@ namespace PlexRequests.Store
Factory = provider; Factory = provider;
} }
private SqliteFactory Factory { get; set; } private SqliteFactory Factory { get; }
private string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile);
public virtual bool CheckDb() public virtual bool CheckDb()
{ {
Log.Trace("Checking DB"); Log.Trace("Checking DB");
if (!File.Exists(DbFile)) Console.WriteLine("Location of the database: {0}",CurrentPath);
if (!File.Exists(CurrentPath))
{ {
Log.Trace("DB doesn't exist, creating a new one"); Log.Trace("DB doesn't exist, creating a new one");
CreateDatabase(); CreateDatabase();
@ -72,7 +73,7 @@ namespace PlexRequests.Store
{ {
throw new SqliteException("Factory returned null"); throw new SqliteException("Factory returned null");
} }
fact.ConnectionString = "Data Source=" + DbFile; fact.ConnectionString = "Data Source=" + CurrentPath;
return fact; return fact;
} }
@ -83,14 +84,16 @@ namespace PlexRequests.Store
{ {
try try
{ {
using (File.Create(DbFile)) using (File.Create(CurrentPath))
{ {
} }
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e.Message); Log.Error(e);
} }
} }
} }
} }

@ -34,5 +34,6 @@ namespace PlexRequests.Store.Models
public int ProviderId { get; set; } public int ProviderId { get; set; }
public byte[] Content { get; set; } public byte[] Content { get; set; }
public RequestType Type { get; set; } public RequestType Type { get; set; }
public string MusicId { get; set; }
} }
} }

@ -52,6 +52,7 @@
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />

@ -38,12 +38,10 @@ namespace PlexRequests.Store.Repository
{ {
private ICacheProvider Cache { get; } private ICacheProvider Cache { get; }
private string TypeName { get; }
public RequestJsonRepository(ISqliteConfiguration config, ICacheProvider cacheProvider) public RequestJsonRepository(ISqliteConfiguration config, ICacheProvider cacheProvider)
{ {
Db = config; Db = config;
Cache = cacheProvider; Cache = cacheProvider;
TypeName = typeof(RequestJsonRepository).Name;
} }
private ISqliteConfiguration Db { get; } private ISqliteConfiguration Db { get; }
@ -60,7 +58,7 @@ namespace PlexRequests.Store.Repository
public IEnumerable<RequestBlobs> GetAll() public IEnumerable<RequestBlobs> GetAll()
{ {
var key = TypeName + "GetAll"; var key = "GetAll";
var item = Cache.GetOrSet(key, () => var item = Cache.GetOrSet(key, () =>
{ {
using (var con = Db.DbConnection()) using (var con = Db.DbConnection())
@ -74,7 +72,7 @@ namespace PlexRequests.Store.Repository
public RequestBlobs Get(int id) public RequestBlobs Get(int id)
{ {
var key = TypeName + "Get" + id; var key = "Get" + id;
var item = Cache.GetOrSet(key, () => var item = Cache.GetOrSet(key, () =>
{ {
using (var con = Db.DbConnection()) using (var con = Db.DbConnection())
@ -107,7 +105,7 @@ namespace PlexRequests.Store.Repository
private void ResetCache() private void ResetCache()
{ {
Cache.Remove("Get"); Cache.Remove("Get");
Cache.Remove(TypeName + "GetAll"); Cache.Remove("GetAll");
} }
public bool UpdateAll(IEnumerable<RequestBlobs> entity) public bool UpdateAll(IEnumerable<RequestBlobs> entity)

@ -1,13 +1,19 @@
using System; using System;
using System.Security.Cryptography;
using Dapper.Contrib.Extensions; using Dapper.Contrib.Extensions;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace PlexRequests.Store namespace PlexRequests.Store
{ {
[Table("Requested")] [Table("Requested")]
public class RequestedModel : Entity public class RequestedModel : Entity
{ {
public RequestedModel()
{
RequestedUsers = new List<string>();
}
// ReSharper disable once IdentifierTypo // ReSharper disable once IdentifierTypo
public int ProviderId { get; set; } public int ProviderId { get; set; }
public string ImdbId { get; set; } public string ImdbId { get; set; }
@ -18,7 +24,10 @@ namespace PlexRequests.Store
public RequestType Type { get; set; } public RequestType Type { get; set; }
public string Status { get; set; } public string Status { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
[Obsolete("Use RequestedUsers")]
public string RequestedBy { get; set; } public string RequestedBy { get; set; }
public DateTime RequestedDate { get; set; } public DateTime RequestedDate { get; set; }
public bool Available { get; set; } public bool Available { get; set; }
public IssueState Issues { get; set; } public IssueState Issues { get; set; }
@ -27,12 +36,48 @@ namespace PlexRequests.Store
public int[] SeasonList { get; set; } public int[] SeasonList { get; set; }
public int SeasonCount { get; set; } public int SeasonCount { get; set; }
public string SeasonsRequested { get; set; } public string SeasonsRequested { get; set; }
public string MusicBrainzId { get; set; }
public List<string> RequestedUsers { get; set; }
[JsonIgnore]
public List<string> AllUsers
{
get
{
var u = new List<string>();
if (!string.IsNullOrEmpty(RequestedBy))
{
u.Add(RequestedBy);
}
if (RequestedUsers.Any())
{
u.AddRange(RequestedUsers.Where(requestedUser => requestedUser != RequestedBy));
}
return u;
}
}
[JsonIgnore]
public bool CanApprove
{
get
{
return !Approved && !Available;
}
}
public bool UserHasRequested(string username)
{
return AllUsers.Any(x => x.Equals(username, StringComparison.OrdinalIgnoreCase));
}
} }
public enum RequestType public enum RequestType
{ {
Movie, Movie,
TvShow TvShow,
Album
} }
public enum IssueState public enum IssueState

@ -25,7 +25,8 @@ CREATE TABLE IF NOT EXISTS RequestBlobs
Id INTEGER PRIMARY KEY AUTOINCREMENT, Id INTEGER PRIMARY KEY AUTOINCREMENT,
ProviderId INTEGER NOT NULL, ProviderId INTEGER NOT NULL,
Type INTEGER NOT NULL, Type INTEGER NOT NULL,
Content BLOB NOT NULL Content BLOB NOT NULL,
MusicId TEXT
); );
CREATE UNIQUE INDEX IF NOT EXISTS RequestBlobs_Id ON RequestBlobs (Id); CREATE UNIQUE INDEX IF NOT EXISTS RequestBlobs_Id ON RequestBlobs (Id);
@ -40,3 +41,9 @@ CREATE TABLE IF NOT EXISTS Logs
Exception varchar(100) NOT NULL Exception varchar(100) NOT NULL
); );
CREATE UNIQUE INDEX IF NOT EXISTS Logs_Id ON Logs (Id); CREATE UNIQUE INDEX IF NOT EXISTS Logs_Id ON Logs (Id);
CREATE TABLE IF NOT EXISTS DBInfo
(
SchemaVersion INTEGER
);

@ -25,7 +25,7 @@
// *********************************************************************** // ***********************************************************************
#endregion #endregion
using System.Data; using System.Data;
using System.Linq;
using Dapper; using Dapper;
using Dapper.Contrib.Extensions; using Dapper.Contrib.Extensions;
@ -44,6 +44,57 @@ namespace PlexRequests.Store
connection.Close(); connection.Close();
} }
public static void AlterTable(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
{
connection.Open();
var result = connection.Query<TableInfo>($"PRAGMA table_info({tableName});");
if (result.Any(x => x.name == newColumn))
{
return;
}
var query = $"ALTER TABLE {tableName} {alterType} {newColumn} {dataType}";
if (isNullable)
{
query = query + " NOT NULL";
}
connection.Execute(query);
connection.Close();
}
public static DbInfo GetSchemaVersion(this IDbConnection con)
{
con.Open();
var result = con.Query<DbInfo>("SELECT * FROM DBInfo");
con.Close();
return result.FirstOrDefault();
}
public static void UpdateSchemaVersion(this IDbConnection con, int version)
{
con.Open();
con.Query($"UPDATE DBInfo SET SchemaVersion = {version}");
con.Close();
}
public static void CreateSchema(this IDbConnection con)
{
con.Open();
con.Query("INSERT INTO DBInfo (SchemaVersion) values (0)");
con.Close();
}
[Table("DBInfo")]
public class DbInfo
{
public int SchemaVersion { get; set; }
}
[Table("sqlite_master")] [Table("sqlite_master")]
public class SqliteMasterTable public class SqliteMasterTable
{ {
@ -54,5 +105,17 @@ namespace PlexRequests.Store
public long rootpage { get; set; } public long rootpage { get; set; }
public string sql { get; set; } public string sql { get; set; }
} }
[Table("table_info")]
public class TableInfo
{
public int cid { get; set; }
public string name { get; set; }
public int notnull { get; set; }
public string dflt_value { get; set; }
public int pk { get; set; }
}
} }
} }

@ -59,6 +59,7 @@ namespace PlexRequests.UI.Tests
private Mock<ISettingsService<EmailNotificationSettings>> EmailMock { get; set; } private Mock<ISettingsService<EmailNotificationSettings>> EmailMock { get; set; }
private Mock<ISettingsService<PushbulletNotificationSettings>> PushbulletSettings { get; set; } private Mock<ISettingsService<PushbulletNotificationSettings>> PushbulletSettings { get; set; }
private Mock<ISettingsService<PushoverNotificationSettings>> PushoverSettings { get; set; } private Mock<ISettingsService<PushoverNotificationSettings>> PushoverSettings { get; set; }
private Mock<ISettingsService<HeadphonesSettings>> HeadphonesSettings { get; set; }
private Mock<IPlexApi> PlexMock { get; set; } private Mock<IPlexApi> PlexMock { get; set; }
private Mock<ISonarrApi> SonarrApiMock { get; set; } private Mock<ISonarrApi> SonarrApiMock { get; set; }
private Mock<IPushbulletApi> PushbulletApi { get; set; } private Mock<IPushbulletApi> PushbulletApi { get; set; }
@ -94,6 +95,7 @@ namespace PlexRequests.UI.Tests
PushoverSettings = new Mock<ISettingsService<PushoverNotificationSettings>>(); PushoverSettings = new Mock<ISettingsService<PushoverNotificationSettings>>();
PushoverApi = new Mock<IPushoverApi>(); PushoverApi = new Mock<IPushoverApi>();
NotificationService = new Mock<INotificationService>(); NotificationService = new Mock<INotificationService>();
HeadphonesSettings = new Mock<ISettingsService<HeadphonesSettings>>();
Bootstrapper = new ConfigurableBootstrapper(with => Bootstrapper = new ConfigurableBootstrapper(with =>
{ {
@ -114,6 +116,7 @@ namespace PlexRequests.UI.Tests
with.Dependency(PushoverSettings.Object); with.Dependency(PushoverSettings.Object);
with.Dependency(PushoverApi.Object); with.Dependency(PushoverApi.Object);
with.Dependency(NotificationService.Object); with.Dependency(NotificationService.Object);
with.Dependency(HeadphonesSettings.Object);
with.RootPathProvider<TestRootPathProvider>(); with.RootPathProvider<TestRootPathProvider>();
with.RequestStartup((container, pipelines, context) => with.RequestStartup((container, pipelines, context) =>
{ {

@ -76,9 +76,9 @@ namespace PlexRequests.UI
container.Register<ISettingsService<EmailNotificationSettings>, SettingsServiceV2<EmailNotificationSettings>>(); container.Register<ISettingsService<EmailNotificationSettings>, SettingsServiceV2<EmailNotificationSettings>>();
container.Register<ISettingsService<PushbulletNotificationSettings>, SettingsServiceV2<PushbulletNotificationSettings>>(); container.Register<ISettingsService<PushbulletNotificationSettings>, SettingsServiceV2<PushbulletNotificationSettings>>();
container.Register<ISettingsService<PushoverNotificationSettings>, SettingsServiceV2<PushoverNotificationSettings>>(); container.Register<ISettingsService<PushoverNotificationSettings>, SettingsServiceV2<PushoverNotificationSettings>>();
container.Register<ISettingsService<HeadphonesSettings>, SettingsServiceV2<HeadphonesSettings>>();
// Repo's // Repo's
container.Register<IRepository<RequestedModel>, GenericRepository<RequestedModel>>();
container.Register<IRepository<LogEntity>, GenericRepository<LogEntity>>(); container.Register<IRepository<LogEntity>, GenericRepository<LogEntity>>();
container.Register<IRequestService, JsonRequestService>(); container.Register<IRequestService, JsonRequestService>();
container.Register<ISettingsRepository, SettingsJsonRepository>(); container.Register<ISettingsRepository, SettingsJsonRepository>();
@ -95,19 +95,21 @@ namespace PlexRequests.UI
container.Register<ISickRageApi, SickrageApi>(); container.Register<ISickRageApi, SickrageApi>();
container.Register<ISonarrApi, SonarrApi>(); container.Register<ISonarrApi, SonarrApi>();
container.Register<IPlexApi, PlexApi>(); container.Register<IPlexApi, PlexApi>();
container.Register<IMusicBrainzApi, MusicBrainzApi>();
container.Register<IHeadphonesApi, HeadphonesApi>();
// NotificationService // NotificationService
container.Register<INotificationService, NotificationService>().AsSingleton(); container.Register<INotificationService, NotificationService>().AsSingleton();
SubscribeAllObservers(container); SubscribeAllObservers(container);
base.ConfigureRequestContainer(container, context); base.ConfigureRequestContainer(container, context);
}
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
TaskManager.TaskFactory = new PlexTaskFactory(); TaskManager.TaskFactory = new PlexTaskFactory();
TaskManager.Initialize(new PlexRegistry()); TaskManager.Initialize(new PlexRegistry());
}
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default); CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default);
StaticConfiguration.DisableErrorTraces = false; StaticConfiguration.DisableErrorTraces = false;
@ -123,6 +125,7 @@ namespace PlexRequests.UI
FormsAuthentication.Enable(pipelines, formsAuthConfiguration); FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback += ServicePointManager.ServerCertificateValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true; (sender, certificate, chain, sslPolicyErrors) => true;

@ -22,7 +22,9 @@
.form-control-custom { .form-control-custom {
background-color: #4e5d6c !important; background-color: #4e5d6c !important;
color: white !important; } color: white !important;
border-radius: 0;
box-shadow: 0 0 0 !important; }
h1 { h1 {
font-size: 3.5rem !important; font-size: 3.5rem !important;
@ -40,6 +42,22 @@ label {
margin-bottom: 0.5rem !important; margin-bottom: 0.5rem !important;
font-size: 16px !important; } font-size: 16px !important; }
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
background: #4e5d6c; }
.navbar .nav a .fa,
.dropdown-menu a .fa {
font-size: 130%;
top: 1px;
position: relative;
display: inline-block;
margin-right: 5px; }
.dropdown-menu a .fa {
top: 2px; }
.btn-danger-outline { .btn-danger-outline {
color: #d9534f !important; color: #d9534f !important;
background-color: transparent; background-color: transparent;
@ -126,3 +144,68 @@ label {
#tvList .mix { #tvList .mix {
display: none; } display: none; }
.scroll-top-wrapper {
position: fixed;
opacity: 0;
visibility: hidden;
overflow: hidden;
text-align: center;
z-index: 99999999;
background-color: #4e5d6c;
color: #eeeeee;
width: 50px;
height: 48px;
line-height: 48px;
right: 30px;
bottom: 30px;
padding-top: 2px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
-webkit-transition: all 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out; }
.scroll-top-wrapper:hover {
background-color: #637689; }
.scroll-top-wrapper.show {
visibility: visible;
cursor: pointer;
opacity: 1.0; }
.scroll-top-wrapper i.fa {
line-height: inherit; }
.no-search-results {
text-align: center; }
.no-search-results .no-search-results-icon {
font-size: 10em;
color: #4e5d6c; }
.no-search-results .no-search-results-text {
margin: 20px 0;
color: #ccc; }
.form-control-search {
padding: 25px 105px 25px 16px; }
.form-control-withbuttons {
padding-right: 105px; }
.input-group-addon .btn-group {
position: absolute;
right: 45px;
z-index: 3;
top: 13px;
box-shadow: 0 0 0; }
.input-group-addon .btn-group .btn {
border: 1px solid rgba(255, 255, 255, 0.7) !important;
padding: 3px 12px;
color: rgba(255, 255, 255, 0.7) !important; }

@ -1 +1 @@
@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;} @media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:25px 105px 25px 16px;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:13px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}

@ -1,11 +1,14 @@
$form-color: #4e5d6c; $form-color: #4e5d6c;
$form-color-lighter: #637689;
$primary-colour: #df691a; $primary-colour: #df691a;
$primary-colour-outline: #ff761b; $primary-colour-outline: #ff761b;
$info-colour: #5bc0de; $info-colour: #5bc0de;
$warning-colour: #f0ad4e; $warning-colour: #f0ad4e;
$danger-colour: #d9534f; $danger-colour: #d9534f;
$success-colour: #5cb85c; $success-colour: #5cb85c;
$i:!important; $i:
!important
;
@media (min-width: 768px ) { @media (min-width: 768px ) {
.row { .row {
@ -42,6 +45,8 @@ $i:!important;
.form-control-custom { .form-control-custom {
background-color: $form-color $i; background-color: $form-color $i;
color: white $i; color: white $i;
border-radius: 0;
box-shadow: 0 0 0 !important;
} }
@ -65,6 +70,25 @@ label {
font-size: 16px $i; font-size: 16px $i;
} }
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
background: #4e5d6c;
}
.navbar .nav a .fa,
.dropdown-menu a .fa {
font-size: 130%;
top: 1px;
position: relative;
display: inline-block;
margin-right: 5px;
}
.dropdown-menu a .fa {
top: 2px;
}
.btn-danger-outline { .btn-danger-outline {
color: $danger-colour $i; color: $danger-colour $i;
background-color: transparent; background-color: transparent;
@ -156,9 +180,89 @@ label {
border-color: $success-colour $i; border-color: $success-colour $i;
} }
#movieList .mix{ #movieList .mix {
display: none; display: none;
}
#tvList .mix {
display: none;
}
$border-radius: 10px;
.scroll-top-wrapper {
position: fixed;
opacity: 0;
visibility: hidden;
overflow: hidden;
text-align: center;
z-index: 99999999;
background-color: $form-color;
color: #eeeeee;
width: 50px;
height: 48px;
line-height: 48px;
right: 30px;
bottom: 30px;
padding-top: 2px;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
border-bottom-left-radius: $border-radius;
-webkit-transition: all 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out;
}
.scroll-top-wrapper:hover {
background-color: $form-color-lighter;
}
.scroll-top-wrapper.show {
visibility: visible;
cursor: pointer;
opacity: 1.0;
} }
#tvList .mix{
display: none; .scroll-top-wrapper i.fa {
line-height: inherit;
}
.no-search-results {
text-align: center;
}
.no-search-results .no-search-results-icon {
font-size: 10em;
color: $form-color;
}
.no-search-results .no-search-results-text {
margin: 20px 0;
color: #ccc;
}
.form-control-search {
padding: 25px 105px 25px 16px;
}
.form-control-withbuttons {
padding-right: 105px;
}
.input-group-addon .btn-group {
position: absolute;
right: 45px;
z-index: 3;
top: 13px;
box-shadow: 0 0 0;
}
.input-group-addon .btn-group .btn {
border: 1px solid rgba(255,255,255,.7) !important;
padding: 3px 12px;
color: rgba(255,255,255,.7) !important;
} }

@ -6,71 +6,128 @@
}); });
var searchSource = $("#search-template").html(); var searchSource = $("#search-template").html();
var albumSource = $("#album-template").html();
var searchTemplate = Handlebars.compile(searchSource); var searchTemplate = Handlebars.compile(searchSource);
var albumTemplate = Handlebars.compile(albumSource);
var movieTimer = 0; var movieTimer = 0;
var tvimer = 0; var tvimer = 0;
movieLoad(); var mixItUpDefault = {
tvLoad(); animation: { enable: true },
load: {
filter: 'all',
sort: 'requestorder:desc'
},
layout: {
display: 'block'
},
callbacks: {
onMixStart: function (state, futureState) {
$('.mix', this).removeAttr('data-bound').removeData('bound'); // fix for animation issues in other tabs
}
}
};
initLoad();
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr('href'); var target = $(e.target).attr('href');
var activeState = ""; var activeState = "";
if (target === "#TvShowTab") {
if ($('#movieList').mixItUp('isLoaded')) {
activeState = $('#movieList').mixItUp('getState');
$('#movieList').mixItUp('destroy');
}
if (!$('#tvList').mixItUp('isLoaded')) {
$('#tvList').mixItUp({
load: {
filter: activeState.activeFilter || 'all',
sort: activeState.activeSort || 'default:asc'
},
layout: {
display: 'block'
}
}); var $ml = $('#movieList');
var $tvl = $('#tvList');
$('.approve-category').hide();
if (target === "#TvShowTab") {
$('#approveTVShows').show();
if ($ml.mixItUp('isLoaded')) {
activeState = $ml.mixItUp('getState');
$ml.mixItUp('destroy');
} }
if ($tvl.mixItUp('isLoaded')) $tvl.mixItUp('destroy');
$tvl.mixItUp(mixItUpConfig(activeState)); // init or reinit
} }
if (target === "#MoviesTab") { if (target === "#MoviesTab") {
if ($('#tvList').mixItUp('isLoaded')) { $('#approveMovies').show();
activeState = $('#tvList').mixItUp('getState'); if ($tvl.mixItUp('isLoaded')) {
$('#tvList').mixItUp('destroy'); activeState = $tvl.mixItUp('getState');
} $tvl.mixItUp('destroy');
if (!$('#movieList').mixItUp('isLoaded')) {
$('#movieList').mixItUp({
load: {
filter: activeState.activeFilter || 'all',
sort: activeState.activeSort || 'default:asc'
},
layout: {
display: 'block'
}
});
} }
if ($ml.mixItUp('isLoaded')) $ml.mixItUp('destroy');
$ml.mixItUp(mixItUpConfig(activeState)); // init or reinit
} }
//$('.mix[data-bound]').removeAttr('data-bound');
}); });
// Approve all // Approve all
$('#approveAll').click(function () { $('#approveMovies').click(function (e) {
e.preventDefault();
var buttonId = e.target.id;
var origHtml = $(this).html();
if ($('#' + buttonId).text() === " Loading...") {
return;
}
loadingButton(buttonId, "success");
$.ajax({ $.ajax({
type: 'post', type: 'post',
url: '/approval/approveall', url: '/approval/approveallmovies',
dataType: "json", dataType: "json",
success: function (response) { success: function (response) {
if (checkJsonResponse(response)) { if (checkJsonResponse(response)) {
generateNotify("Success! All requests approved!", "success"); generateNotify("Success! All Movie requests approved!", "success");
movieLoad();
} }
}, },
error: function (e) { error: function (e) {
console.log(e); console.log(e);
generateNotify("Something went wrong!", "danger"); generateNotify("Something went wrong!", "danger");
},
complete: function (e) {
finishLoading(buttonId, "success", origHtml);
} }
}); });
}); });
$('#approveTVShows').click(function (e) {
e.preventDefault();
var buttonId = e.target.id;
var origHtml = $(this).html();
if ($('#' + buttonId).text() === " Loading...") {
return;
}
loadingButton(buttonId, "success");
$.ajax({
type: 'post',
url: '/approval/approvealltvshows',
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! All TV Show requests approved!", "success");
tvLoad();
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
},
complete: function (e) {
finishLoading(buttonId, "success", origHtml);
}
});
});
// filtering/sorting
$('.filter,.sort', '.dropdown-menu').click(function (e) {
var $this = $(this);
$('.fa-check-square', $this.parents('.dropdown-menu:first')).removeClass('fa-check-square').addClass('fa-square-o');
$this.children('.fa').first().removeClass('fa-square-o').addClass('fa-check-square');
});
// Report Issue // Report Issue
@ -315,36 +372,89 @@ $(document).on("click", ".change", function (e) {
}); });
function mixItUpConfig(activeState) {
var conf = mixItUpDefault;
if (activeState) {
if (activeState.activeFilter) conf['load']['filter'] = activeState.activeFilter;
if (activeState.activeSort) conf['load']['sort'] = activeState.activeSort;
}
return conf;
};
function initLoad() {
movieLoad();
tvLoad();
albumLoad();
//noResultsMusic
}
function movieLoad() { function movieLoad() {
$("#movieList").html(""); var $ml = $('#movieList');
if ($ml.mixItUp('isLoaded')) {
activeState = $ml.mixItUp('getState');
$ml.mixItUp('destroy');
}
$ml.html("");
$.ajax("/requests/movies/").success(function (results) { $.ajax("/requests/movies/").success(function (results) {
results.forEach(function (result) { if (results.length > 0) {
var context = buildRequestContext(result, "movie"); results.forEach(function (result) {
var context = buildRequestContext(result, "movie");
var html = searchTemplate(context); var html = searchTemplate(context);
$("#movieList").append(html); $ml.append(html);
}); });
$('#movieList').mixItUp({ }
layout: { else {
display: 'block' $ml.html(noResultsHtml.format("movie"));
}, }
load: { $ml.mixItUp(mixItUpConfig());
filter: 'all'
}
});
}); });
}; };
function tvLoad() { function tvLoad() {
$("#tvList").html(""); var $tvl = $('#tvList');
if ($tvl.mixItUp('isLoaded')) {
activeState = $tvl.mixItUp('getState');
$tvl.mixItUp('destroy');
}
$tvl.html("");
$.ajax("/requests/tvshows/").success(function (results) { $.ajax("/requests/tvshows/").success(function (results) {
results.forEach(function (result) { if (results.length > 0) {
var context = buildRequestContext(result, "tv"); results.forEach(function (result) {
var html = searchTemplate(context); var context = buildRequestContext(result, "tv");
$("#tvList").append(html); var html = searchTemplate(context);
}); $tvl.append(html);
});
}
else {
$tvl.html(noResultsHtml.format("tv show"));
}
$tvl.mixItUp(mixItUpConfig());
});
};
function albumLoad() {
var $albumL = $('#MusicList');
if ($albumL.mixItUp('isLoaded')) {
activeState = $albumL.mixItUp('getState');
$albumL.mixItUp('destroy');
}
$albumL.html("");
$.ajax("/requests/albums/").success(function (results) {
if (results.length > 0) {
results.forEach(function (result) {
var context = buildRequestContext(result, "album");
var html = searchTemplate(context);
$albumL.append(html);
});
}
else {
$albumL.html(noResultsMusic.format("albums"));
}
$albumL.mixItUp(mixItUpConfig());
}); });
}; };
@ -359,9 +469,11 @@ function buildRequestContext(result, type) {
type: type, type: type,
status: result.status, status: result.status,
releaseDate: result.releaseDate, releaseDate: result.releaseDate,
releaseDateTicks: result.releaseDateTicks,
approved: result.approved, approved: result.approved,
requestedBy: result.requestedBy, requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '',
requestedDate: result.requestedDate, requestedDate: result.requestedDate,
requestedDateTicks: result.requestedDateTicks,
available: result.available, available: result.available,
admin: result.admin, admin: result.admin,
issues: result.issues, issues: result.issues,
@ -369,20 +481,11 @@ function buildRequestContext(result, type) {
requestId: result.id, requestId: result.id,
adminNote: result.adminNotes, adminNote: result.adminNotes,
imdb: result.imdbId, imdb: result.imdbId,
seriesRequested: result.tvSeriesRequestType seriesRequested: result.tvSeriesRequestType,
coverArtUrl: result.coverArtUrl,
}; };
return context; return context;
} }
function startFilter(elementId) {
$('#'+element).mixItUp({
load: {
filter: activeState.activeFilter || 'all',
sort: activeState.activeSort || 'default:asc'
},
layout: {
display: 'block'
}
});
}

@ -6,27 +6,39 @@
}); });
var searchSource = $("#search-template").html(); var searchSource = $("#search-template").html();
var musicSource = $("#music-template").html();
var searchTemplate = Handlebars.compile(searchSource); var searchTemplate = Handlebars.compile(searchSource);
var movieTimer = 0; var musicTemplate = Handlebars.compile(musicSource);
var tvimer = 0;
var searchTimer = 0;
// Type in movie search // Type in movie search
$("#movieSearchContent").on("input", function () { $("#movieSearchContent").on("input", function () {
if (movieTimer) { if (searchTimer) {
clearTimeout(movieTimer); clearTimeout(searchTimer);
} }
$('#movieSearchButton').attr("class","fa fa-spinner fa-spin"); $('#movieSearchButton').attr("class","fa fa-spinner fa-spin");
movieTimer = setTimeout(movieSearch, 400); searchTimer = setTimeout(movieSearch, 400);
}); });
$('#moviesComingSoon').on('click', function (e) {
e.preventDefault();
moviesComingSoon();
});
$('#moviesInTheaters').on('click', function (e) {
e.preventDefault();
moviesInTheaters();
});
// Type in TV search // Type in TV search
$("#tvSearchContent").on("input", function () { $("#tvSearchContent").on("input", function () {
if (tvimer) { if (searchTimer) {
clearTimeout(tvimer); clearTimeout(searchTimer);
} }
$('#tvSearchButton').attr("class", "fa fa-spinner fa-spin"); $('#tvSearchButton').attr("class", "fa fa-spinner fa-spin");
tvimer = setTimeout(tvSearch, 400); searchTimer = setTimeout(tvSearch, 400);
}); });
// Click TV dropdown option // Click TV dropdown option
@ -60,6 +72,16 @@ $(document).on("click", ".dropdownTv", function (e) {
sendRequestAjax(data, type, url, buttonId); sendRequestAjax(data, type, url, buttonId);
}); });
// Search Music
$("#musicSearchContent").on("input", function () {
if (searchTimer) {
clearTimeout(searchTimer);
}
$('#musicSearchButton').attr("class", "fa fa-spinner fa-spin");
searchTimer = setTimeout(musicSearch, 400);
});
// Click Request for movie // Click Request for movie
$(document).on("click", ".requestMovie", function (e) { $(document).on("click", ".requestMovie", function (e) {
e.preventDefault(); e.preventDefault();
@ -82,6 +104,28 @@ $(document).on("click", ".requestMovie", function (e) {
}); });
// Click Request for album
$(document).on("click", ".requestAlbum", function (e) {
e.preventDefault();
var buttonId = e.target.id;
if ($("#" + buttonId).attr('disabled')) {
return;
}
$("#" + buttonId).prop("disabled", true);
loadingButton(buttonId, "primary");
var $form = $('#form' + buttonId);
var type = $form.prop('method');
var url = $form.prop('action');
var data = $form.serialize();
sendRequestAjax(data, type, url, buttonId);
});
function sendRequestAjax(data, type, url, buttonId) { function sendRequestAjax(data, type, url, buttonId) {
$.ajax({ $.ajax({
type: type, type: type,
@ -112,10 +156,23 @@ function sendRequestAjax(data, type, url, buttonId) {
} }
function movieSearch() { function movieSearch() {
$("#movieList").html("");
var query = $("#movieSearchContent").val(); var query = $("#movieSearchContent").val();
getMovies("/search/movie/" + query);
}
$.ajax("/search/movie/" + query).success(function (results) { function moviesComingSoon() {
getMovies("/search/movie/upcoming");
}
function moviesInTheaters() {
getMovies("/search/movie/playing");
}
function getMovies(url) {
$("#movieList").html("");
$.ajax(url).success(function (results) {
if (results.length > 0) { if (results.length > 0) {
results.forEach(function(result) { results.forEach(function(result) {
var context = buildMovieContext(result); var context = buildMovieContext(result);
@ -124,15 +181,22 @@ function movieSearch() {
$("#movieList").append(html); $("#movieList").append(html);
}); });
} }
else {
$("#movieList").html(noResultsHtml);
}
$('#movieSearchButton').attr("class","fa fa-search"); $('#movieSearchButton').attr("class","fa fa-search");
}); });
}; };
function tvSearch() { function tvSearch() {
$("#tvList").html("");
var query = $("#tvSearchContent").val(); var query = $("#tvSearchContent").val();
getTvShows("/search/tv/" + query);
}
function getTvShows(url) {
$("#tvList").html("");
$.ajax("/search/tv/" + query).success(function (results) { $.ajax(url).success(function (results) {
if (results.length > 0) { if (results.length > 0) {
results.forEach(function(result) { results.forEach(function(result) {
var context = buildTvShowContext(result); var context = buildTvShowContext(result);
@ -140,10 +204,36 @@ function tvSearch() {
$("#tvList").append(html); $("#tvList").append(html);
}); });
} }
else {
$("#tvList").html(noResultsHtml);
}
$('#tvSearchButton').attr("class", "fa fa-search"); $('#tvSearchButton').attr("class", "fa fa-search");
}); });
}; };
function musicSearch() {
var query = $("#musicSearchContent").val();
getMusic("/search/music/" + query);
}
function getMusic(url) {
$("#musicList").html("");
$.ajax(url).success(function (results) {
if (results.length > 0) {
results.forEach(function (result) {
var context = buildMusicContext(result);
var html = musicTemplate(context);
$("#musicList").append(html);
});
}
else {
$("#musicList").html(noResultsMusic);
}
$('#musicSearchButton').attr("class", "fa fa-search");
});
};
function buildMovieContext(result) { function buildMovieContext(result) {
var date = new Date(result.releaseDate); var date = new Date(result.releaseDate);
@ -177,3 +267,21 @@ function buildTvShowContext(result) {
}; };
return context; return context;
} }
function buildMusicContext(result) {
var context = {
id: result.id,
title: result.title,
overview: result.overview,
year: result.releaseDate,
type: "album",
trackCount: result.trackCount,
coverArtUrl: result.coverArtUrl,
artist: result.artist,
releaseType: result.releaseType,
country: result.country
};
return context;
}

@ -1,4 +1,14 @@
function generateNotify(message, type) { String.prototype.format = String.prototype.f = function () {
var s = this,
i = arguments.length;
while (i--) {
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
}
return s;
}
function generateNotify(message, type) {
// type = danger, warning, info, successs // type = danger, warning, info, successs
$.notify({ $.notify({
// options // options
@ -35,3 +45,8 @@ function finishLoading(elementId, originalCss, html) {
$('#' + elementId).addClass("btn-" + originalCss + "-outline"); $('#' + elementId).addClass("btn-" + originalCss + "-outline");
$('#' + elementId).html(html); $('#' + elementId).html(html);
} }
var noResultsHtml = "<div class='no-search-results'>" +
"<i class='fa fa-film no-search-results-icon'></i><div class='no-search-results-text'>Sorry, we didn't find any results!</div></div>";
var noResultsMusic = "<div class='no-search-results'>" +
"<i class='fa fa-headphones no-search-results-icon'></i><div class='no-search-results-text'>Sorry, we didn't find any results!</div></div>";

@ -24,17 +24,13 @@
// 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 Nancy;
using NLog; using NLog;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage; using PlexRequests.Api.Models.SickRage;
using PlexRequests.Api.Models.Sonarr; using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Helpers namespace PlexRequests.UI.Helpers
{ {

@ -12,12 +12,12 @@ namespace PlexRequests.UI.Jobs
//typeof(AvailabilityUpdateService); //typeof(AvailabilityUpdateService);
var container = TinyIoCContainer.Current; var container = TinyIoCContainer.Current;
var a= container.ResolveAll(typeof(T)); var a= container.Resolve(typeof(T));
object outT; object outT;
container.TryResolve(typeof(T), out outT); container.TryResolve(typeof(T), out outT);
return (T)outT; return (T)a;
} }
} }
} }

@ -37,11 +37,13 @@ namespace PlexRequests.UI.Models
public string Title { get; set; } public string Title { get; set; }
public string PosterPath { get; set; } public string PosterPath { get; set; }
public string ReleaseDate { get; set; } public string ReleaseDate { get; set; }
public long ReleaseDateTicks { get; set; }
public RequestType Type { get; set; } public RequestType Type { get; set; }
public string Status { get; set; } public string Status { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
public string RequestedBy { get; set; } public string[] RequestedUsers { get; set; }
public string RequestedDate { get; set; } public string RequestedDate { get; set; }
public long RequestedDateTicks { get; set; }
public string ReleaseYear { get; set; } public string ReleaseYear { get; set; }
public bool Available { get; set; } public bool Available { get; set; }
public bool Admin { get; set; } public bool Admin { get; set; }
@ -49,5 +51,6 @@ namespace PlexRequests.UI.Models
public string OtherMessage { get; set; } public string OtherMessage { get; set; }
public string AdminNotes { get; set; } public string AdminNotes { get; set; }
public string TvSeriesRequestType { get; set; } public string TvSeriesRequestType { get; set; }
public string MusicBrainzId { get; set; }
} }
} }

@ -0,0 +1,41 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: SearchMusicViewModel.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
namespace PlexRequests.UI.Models
{
public class SearchMusicViewModel
{
public string Id { get; set; }
public string Overview { get; set; }
public string CoverArtUrl { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public string ReleaseDate { get; set; }
public int TrackCount { get; set; }
public string ReleaseType { get; set; }
public string Country { get; set; }
}
}

@ -29,5 +29,6 @@ namespace PlexRequests.UI.Models
public class SessionKeys public class SessionKeys
{ {
public const string UsernameKey = "Username"; public const string UsernameKey = "Username";
public const string ClientDateTimeOffsetKey = "ClientDateTimeOffset";
} }
} }

@ -28,6 +28,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.Linq; using System.Linq;
using System.Web.UI.HtmlControls;
using Humanizer; using Humanizer;
using MarkdownSharp; using MarkdownSharp;
@ -51,12 +53,13 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using System;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class AdminModule : NancyModule public class AdminModule : NancyModule
{ {
private ISettingsService<PlexRequestSettings> RpService { get; } private ISettingsService<PlexRequestSettings> PrService { get; }
private ISettingsService<CouchPotatoSettings> CpService { get; } private ISettingsService<CouchPotatoSettings> CpService { get; }
private ISettingsService<AuthenticationSettings> AuthService { get; } private ISettingsService<AuthenticationSettings> AuthService { get; }
private ISettingsService<PlexSettings> PlexService { get; } private ISettingsService<PlexSettings> PlexService { get; }
@ -65,6 +68,7 @@ namespace PlexRequests.UI.Modules
private ISettingsService<EmailNotificationSettings> EmailService { get; } private ISettingsService<EmailNotificationSettings> EmailService { get; }
private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; } private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; }
private ISettingsService<PushoverNotificationSettings> PushoverService { get; } private ISettingsService<PushoverNotificationSettings> PushoverService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISonarrApi SonarrApi { get; } private ISonarrApi SonarrApi { get; }
private IPushbulletApi PushbulletApi { get; } private IPushbulletApi PushbulletApi { get; }
@ -74,7 +78,7 @@ namespace PlexRequests.UI.Modules
private INotificationService NotificationService { get; } private INotificationService NotificationService { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> rpService, public AdminModule(ISettingsService<PlexRequestSettings> prService,
ISettingsService<CouchPotatoSettings> cpService, ISettingsService<CouchPotatoSettings> cpService,
ISettingsService<AuthenticationSettings> auth, ISettingsService<AuthenticationSettings> auth,
ISettingsService<PlexSettings> plex, ISettingsService<PlexSettings> plex,
@ -89,9 +93,10 @@ namespace PlexRequests.UI.Modules
ISettingsService<PushoverNotificationSettings> pushoverSettings, ISettingsService<PushoverNotificationSettings> pushoverSettings,
IPushoverApi pushoverApi, IPushoverApi pushoverApi,
IRepository<LogEntity> logsRepo, IRepository<LogEntity> logsRepo,
INotificationService notify) : base("admin") INotificationService notify,
ISettingsService<HeadphonesSettings> headphones) : base("admin")
{ {
RpService = rpService; PrService = prService;
CpService = cpService; CpService = cpService;
AuthService = auth; AuthService = auth;
PlexService = plex; PlexService = plex;
@ -107,6 +112,7 @@ namespace PlexRequests.UI.Modules
PushoverService = pushoverSettings; PushoverService = pushoverSettings;
PushoverApi = pushoverApi; PushoverApi = pushoverApi;
NotificationService = notify; NotificationService = notify;
HeadphonesService = headphones;
#if !DEBUG #if !DEBUG
this.RequiresAuthentication(); this.RequiresAuthentication();
@ -139,18 +145,24 @@ namespace PlexRequests.UI.Modules
Get["/emailnotification"] = _ => EmailNotifications(); Get["/emailnotification"] = _ => EmailNotifications();
Post["/emailnotification"] = _ => SaveEmailNotifications(); Post["/emailnotification"] = _ => SaveEmailNotifications();
Post["/testemailnotification"] = _ => TestEmailNotifications();
Get["/status"] = _ => Status(); Get["/status"] = _ => Status();
Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Get["/pushbulletnotification"] = _ => PushbulletNotifications();
Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications();
Post["/testpushbulletnotification"] = _ => TestPushbulletNotifications();
Get["/pushovernotification"] = _ => PushoverNotifications(); Get["/pushovernotification"] = _ => PushoverNotifications();
Post["/pushovernotification"] = _ => SavePushoverNotifications(); Post["/pushovernotification"] = _ => SavePushoverNotifications();
Post["/testpushovernotification"] = _ => TestPushoverNotifications();
Get["/logs"] = _ => Logs(); Get["/logs"] = _ => Logs();
Get["/loglevel"] = _ => GetLogLevels(); Get["/loglevel"] = _ => GetLogLevels();
Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level); Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level);
Get["/loadlogs"] = _ => LoadLogs(); Get["/loadlogs"] = _ => LoadLogs();
Get["/headphones"] = _ => Headphones();
Post["/headphones"] = _ => SaveHeadphones();
} }
private Negotiator Authentication() private Negotiator Authentication()
@ -174,7 +186,7 @@ namespace PlexRequests.UI.Modules
private Negotiator Admin() private Negotiator Admin()
{ {
var settings = RpService.GetSettings(); var settings = PrService.GetSettings();
Log.Trace("Getting Settings:"); Log.Trace("Getting Settings:");
Log.Trace(settings.DumpJson()); Log.Trace(settings.DumpJson());
@ -185,7 +197,7 @@ namespace PlexRequests.UI.Modules
{ {
var model = this.Bind<PlexRequestSettings>(); var model = this.Bind<PlexRequestSettings>();
RpService.SaveSettings(model); PrService.SaveSettings(model);
return Context.GetRedirect("~/admin"); return Context.GetRedirect("~/admin");
@ -372,6 +384,37 @@ namespace PlexRequests.UI.Modules
return View["EmailNotifications", settings]; return View["EmailNotifications", settings];
} }
private Response TestEmailNotifications()
{
var settings = this.Bind<EmailNotificationSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
try
{
NotificationService.Subscribe(new EmailMessageNotification(EmailService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
Log.Info("Sent email notification test");
}
catch (Exception)
{
Log.Error("Failed to subscribe and publish test Email Notification");
}
finally
{
NotificationService.UnSubscribe(new EmailMessageNotification(EmailService));
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Email Notification!" });
}
private Response SaveEmailNotifications() private Response SaveEmailNotifications()
{ {
var settings = this.Bind<EmailNotificationSettings>(); var settings = this.Bind<EmailNotificationSettings>();
@ -440,6 +483,37 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
} }
private Response TestPushbulletNotifications()
{
var settings = this.Bind<PushbulletNotificationSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
try
{
NotificationService.Subscribe(new PushbulletNotification(PushbulletApi, PushbulletService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
Log.Info("Sent pushbullet notification test");
}
catch (Exception)
{
Log.Error("Failed to subscribe and publish test Pushbullet Notification");
}
finally
{
NotificationService.UnSubscribe(new PushbulletNotification(PushbulletApi, PushbulletService));
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Pushbullet Notification!" });
}
private Negotiator PushoverNotifications() private Negotiator PushoverNotifications()
{ {
var settings = PushoverService.GetSettings(); var settings = PushoverService.GetSettings();
@ -472,6 +546,37 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
} }
private Response TestPushoverNotifications()
{
var settings = this.Bind<PushoverNotificationSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
try
{
NotificationService.Subscribe(new PushoverNotification(PushoverApi, PushoverService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
Log.Info("Sent pushover notification test");
}
catch (Exception)
{
Log.Error("Failed to subscribe and publish test Pushover Notification");
}
finally
{
NotificationService.UnSubscribe(new PushoverNotification(PushoverApi, PushoverService));
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Pushover Notification!" });
}
private Response GetCpProfiles() private Response GetCpProfiles()
{ {
var settings = this.Bind<CouchPotatoSettings>(); var settings = this.Bind<CouchPotatoSettings>();
@ -509,5 +614,32 @@ namespace PlexRequests.UI.Modules
LoggingHelper.ReconfigureLogLevel(newLevel); LoggingHelper.ReconfigureLogLevel(newLevel);
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"}); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"});
} }
private Negotiator Headphones()
{
var settings = HeadphonesService.GetSettings();
return View["Headphones", settings];
}
private Response SaveHeadphones()
{
var settings = this.Bind<HeadphonesSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
var error = valid.SendJsonError();
Log.Info("Error validating Headphones settings, message: {0}", error.Message);
return Response.AsJson(error);
}
Log.Trace(settings.DumpJson());
var result = HeadphonesService.SaveSettings(settings);
Log.Info("Saved headphones settings, result: {0}", result);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Headphones!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
} }
} }

@ -57,6 +57,7 @@ namespace PlexRequests.UI.Modules
Post["/sonarr"] = _ => SonarrTest(); Post["/sonarr"] = _ => SonarrTest();
Post["/plex"] = _ => PlexTest(); Post["/plex"] = _ => PlexTest();
Post["/sickrage"] = _ => SickRageTest(); Post["/sickrage"] = _ => SickRageTest();
Post["/headphones"] = _ => HeadphonesTest();
} }
@ -168,5 +169,10 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); return Response.AsJson(new JsonResponseModel { Result = false, Message = message });
} }
} }
private Response HeadphonesTest()
{
throw new NotImplementedException(); //TODO
}
} }
} }

@ -61,6 +61,8 @@ namespace PlexRequests.UI.Modules
Post["/approve"] = parameters => Approve((int)Request.Form.requestid); Post["/approve"] = parameters => Approve((int)Request.Form.requestid);
Post["/approveall"] = x => ApproveAll(); Post["/approveall"] = x => ApproveAll();
Post["/approveallmovies"] = x => ApproveAllMovies();
Post["/approvealltvshows"] = x => ApproveAllTVShows();
} }
private IRequestService Service { get; } private IRequestService Service { get; }
@ -131,7 +133,7 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel return Response.AsJson(new JsonResponseModel
{ {
Result = false, Result = false,
Message = "Could not add the series to Sonarr" Message = result.ErrorMessage ?? "Could not add the series to Sonarr"
}); });
} }
@ -216,6 +218,56 @@ namespace PlexRequests.UI.Modules
}); });
} }
private Response ApproveAllMovies()
{
if (!Context.CurrentUser.IsAuthenticated())
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." });
}
var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.Movie);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no movie requests to approve. Please refresh." });
}
try
{
return UpdateRequests(requestedModels);
}
catch (Exception e)
{
Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
}
}
private Response ApproveAllTVShows()
{
if (!Context.CurrentUser.IsAuthenticated())
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." });
}
var requests = Service.GetAll().Where(x => x.CanApprove && x.Type == RequestType.TvShow);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no tv show requests to approve. Please refresh." });
}
try
{
return UpdateRequests(requestedModels);
}
catch (Exception e)
{
Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
}
}
/// <summary> /// <summary>
/// Approves all. /// Approves all.
/// </summary> /// </summary>
@ -227,23 +279,35 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." });
} }
var requests = Service.GetAll().Where(x => x.Approved == false); var requests = Service.GetAll().Where(x => x.CanApprove);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any()) if (!requestedModels.Any())
{ {
return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no requests to approve. Please refresh." }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no requests to approve. Please refresh." });
} }
var cpSettings = CpService.GetSettings(); try
{
return UpdateRequests(requestedModels);
}
catch (Exception e)
{
Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
}
}
private Response UpdateRequests(RequestedModel[] requestedModels)
{
var cpSettings = CpService.GetSettings();
var updatedRequests = new List<RequestedModel>(); var updatedRequests = new List<RequestedModel>();
foreach (var r in requestedModels) foreach (var r in requestedModels)
{ {
if (r.Type == RequestType.Movie) if (r.Type == RequestType.Movie)
{ {
var result = SendMovie(cpSettings, r, CpApi); var res = SendMovie(cpSettings, r, CpApi);
if (result) if (res)
{ {
r.Approved = true; r.Approved = true;
updatedRequests.Add(r); updatedRequests.Add(r);
@ -260,8 +324,8 @@ namespace PlexRequests.UI.Modules
var sonarr = SonarrSettings.GetSettings(); var sonarr = SonarrSettings.GetSettings();
if (sr.Enabled) if (sr.Enabled)
{ {
var result = sender.SendToSickRage(sr, r); var res = sender.SendToSickRage(sr, r);
if (result?.result == "success") if (res?.result == "success")
{ {
r.Approved = true; r.Approved = true;
updatedRequests.Add(r); updatedRequests.Add(r);
@ -269,14 +333,14 @@ namespace PlexRequests.UI.Modules
else else
{ {
Log.Error("Could not approve and send the TV {0} to SickRage!", r.Title); Log.Error("Could not approve and send the TV {0} to SickRage!", r.Title);
Log.Error("SickRage Message: {0}", result?.message); Log.Error("SickRage Message: {0}", res?.message);
} }
} }
if (sonarr.Enabled) if (sonarr.Enabled)
{ {
var result = sender.SendToSonarr(sonarr, r); var res = sender.SendToSonarr(sonarr, r);
if (result != null) if (!string.IsNullOrEmpty(res?.title))
{ {
r.Approved = true; r.Approved = true;
updatedRequests.Add(r); updatedRequests.Add(r);
@ -284,6 +348,7 @@ namespace PlexRequests.UI.Modules
else else
{ {
Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title); Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title);
Log.Error("Error message: {0}", res?.ErrorMessage);
} }
} }
} }
@ -291,17 +356,16 @@ namespace PlexRequests.UI.Modules
try try
{ {
var result = Service.BatchUpdate(updatedRequests); return Response.AsJson(result var result = Service.BatchUpdate(updatedRequests);
return Response.AsJson(result
? new JsonResponseModel { Result = true } ? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." }); : new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." });
}
}
catch (Exception e) catch (Exception e)
{ {
Log.Fatal(e); Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
} }
} }
private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp) private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp)

@ -28,14 +28,43 @@
using Nancy; using Nancy;
using Nancy.Extensions; using Nancy.Extensions;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using System;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class BaseModule : NancyModule public class BaseModule : NancyModule
{ {
private string _username;
private int _dateTimeOffset = -1;
protected string Username
{
get
{
if (string.IsNullOrEmpty(_username))
{
_username = Session[SessionKeys.UsernameKey].ToString();
}
return _username;
}
}
protected int DateTimeOffset
{
get
{
if (_dateTimeOffset == -1)
{
_dateTimeOffset = Session[SessionKeys.ClientDateTimeOffsetKey] != null ?
(int)Session[SessionKeys.ClientDateTimeOffsetKey] : (new DateTimeOffset().Offset).Minutes;
}
return _dateTimeOffset;
}
}
public BaseModule() public BaseModule()
{ {
Before += (ctx)=> CheckAuth(); Before += (ctx) => CheckAuth();
} }
public BaseModule(string modulePath) : base(modulePath) public BaseModule(string modulePath) : base(modulePath)

@ -60,6 +60,7 @@ namespace PlexRequests.UI.Modules
{ {
var username = (string)Request.Form.Username; var username = (string)Request.Form.Username;
var password = (string)Request.Form.Password; var password = (string)Request.Form.Password;
var dtOffset = (int)Request.Form.DateTimeOffset;
var userId = UserMapper.ValidateUser(username, password); var userId = UserMapper.ValidateUser(username, password);
@ -73,6 +74,7 @@ namespace PlexRequests.UI.Modules
expiry = DateTime.Now.AddDays(7); expiry = DateTime.Now.AddDays(7);
} }
Session[SessionKeys.UsernameKey] = username; Session[SessionKeys.UsernameKey] = username;
Session[SessionKeys.ClientDateTimeOffsetKey] = dtOffset;
return this.LoginAndRedirect(userId.Value, expiry); return this.LoginAndRedirect(userId.Value, expiry);
}; };

@ -40,12 +40,12 @@ using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using PlexRequests.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class RequestsModule : BaseModule public class RequestsModule : BaseModule
{ {
public RequestsModule(IRequestService service, ISettingsService<PlexRequestSettings> prSettings, ISettingsService<PlexSettings> plex, INotificationService notify) : base("requests") public RequestsModule(IRequestService service, ISettingsService<PlexRequestSettings> prSettings, ISettingsService<PlexSettings> plex, INotificationService notify) : base("requests")
{ {
Service = service; Service = service;
@ -56,6 +56,7 @@ namespace PlexRequests.UI.Modules
Get["/"] = _ => LoadRequests(); Get["/"] = _ => LoadRequests();
Get["/movies"] = _ => GetMovies(); Get["/movies"] = _ => GetMovies();
Get["/tvshows"] = _ => GetTvShows(); Get["/tvshows"] = _ => GetTvShows();
Get["/albums"] = _ => GetAlbumRequests();
Post["/delete"] = _ => DeleteRequest((int)Request.Form.id); Post["/delete"] = _ => DeleteRequest((int)Request.Form.id);
Post["/reportissue"] = _ => ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null); Post["/reportissue"] = _ => ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null);
Post["/reportissuecomment"] = _ => ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea); Post["/reportissuecomment"] = _ => ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea);
@ -79,28 +80,38 @@ namespace PlexRequests.UI.Modules
private Response GetMovies() private Response GetMovies()
{ {
var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated(); var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbMovies = Service.GetAll().Where(x => x.Type == RequestType.Movie); var dbMovies = Service.GetAll().Where(x => x.Type == RequestType.Movie);
var viewModel = dbMovies.Select(movie => new RequestViewModel if (settings.UsersCanViewOnlyOwnRequests && !isAdmin)
{ {
ProviderId = movie.ProviderId, dbMovies = dbMovies.Where(x => x.UserHasRequested(Username));
Type = movie.Type, }
Status = movie.Status,
ImdbId = movie.ImdbId, var viewModel = dbMovies.Select(movie => {
Id = movie.Id, return new RequestViewModel
PosterPath = movie.PosterPath, {
ReleaseDate = movie.ReleaseDate.Humanize(), ProviderId = movie.ProviderId,
RequestedDate = movie.RequestedDate.Humanize(), Type = movie.Type,
Approved = movie.Approved, Status = movie.Status,
Title = movie.Title, ImdbId = movie.ImdbId,
Overview = movie.Overview, Id = movie.Id,
RequestedBy = movie.RequestedBy, PosterPath = movie.PosterPath,
ReleaseYear = movie.ReleaseDate.Year.ToString(), ReleaseDate = movie.ReleaseDate.Humanize(),
Available = movie.Available, ReleaseDateTicks = movie.ReleaseDate.Ticks,
Admin = isAdmin, RequestedDate = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Humanize(),
Issues = movie.Issues.Humanize(LetterCasing.Title), RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks,
OtherMessage = movie.OtherMessage, Approved = movie.Available || movie.Approved,
AdminNotes = movie.AdminNote Title = movie.Title,
Overview = movie.Overview,
RequestedUsers = isAdmin ? movie.AllUsers.ToArray() : new string[] { },
ReleaseYear = movie.ReleaseDate.Year.ToString(),
Available = movie.Available,
Admin = isAdmin,
Issues = movie.Issues.Humanize(LetterCasing.Title),
OtherMessage = movie.OtherMessage,
AdminNotes = movie.AdminNote,
};
}).ToList(); }).ToList();
return Response.AsJson(viewModel); return Response.AsJson(viewModel);
@ -108,29 +119,80 @@ namespace PlexRequests.UI.Modules
private Response GetTvShows() private Response GetTvShows()
{ {
var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated(); var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbTv = Service.GetAll().Where(x => x.Type == RequestType.TvShow); var dbTv = Service.GetAll().Where(x => x.Type == RequestType.TvShow);
var viewModel = dbTv.Select(tv => new RequestViewModel if (settings.UsersCanViewOnlyOwnRequests && !isAdmin)
{
dbTv = dbTv.Where(x => x.UserHasRequested(Username));
}
var viewModel = dbTv.Select(tv => {
return new RequestViewModel
{
ProviderId = tv.ProviderId,
Type = tv.Type,
Status = tv.Status,
ImdbId = tv.ImdbId,
Id = tv.Id,
PosterPath = tv.PosterPath,
ReleaseDate = tv.ReleaseDate.Humanize(),
ReleaseDateTicks = tv.ReleaseDate.Ticks,
RequestedDate = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Humanize(),
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks,
Approved = tv.Available || tv.Approved,
Title = tv.Title,
Overview = tv.Overview,
RequestedUsers = isAdmin ? tv.AllUsers.ToArray() : new string[] { },
ReleaseYear = tv.ReleaseDate.Year.ToString(),
Available = tv.Available,
Admin = isAdmin,
Issues = tv.Issues.Humanize(LetterCasing.Title),
OtherMessage = tv.OtherMessage,
AdminNotes = tv.AdminNote,
TvSeriesRequestType = tv.SeasonsRequested
};
}).ToList();
return Response.AsJson(viewModel);
}
private Response GetAlbumRequests()
{
var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbAlbum = Service.GetAll().Where(x => x.Type == RequestType.Album);
if (settings.UsersCanViewOnlyOwnRequests && !isAdmin)
{ {
ProviderId = tv.ProviderId, dbAlbum = dbAlbum.Where(x => x.UserHasRequested(Username));
Type = tv.Type, }
Status = tv.Status,
ImdbId = tv.ImdbId, var viewModel = dbAlbum.Select(album => {
Id = tv.Id, return new RequestViewModel
PosterPath = tv.PosterPath, {
ReleaseDate = tv.ReleaseDate.Humanize(), ProviderId = album.ProviderId,
RequestedDate = tv.RequestedDate.Humanize(), Type = album.Type,
Approved = tv.Approved, Status = album.Status,
Title = tv.Title, ImdbId = album.ImdbId,
Overview = tv.Overview, Id = album.Id,
RequestedBy = tv.RequestedBy, PosterPath = album.PosterPath,
ReleaseYear = tv.ReleaseDate.Year.ToString(), ReleaseDate = album.ReleaseDate.Humanize(),
Available = tv.Available, ReleaseDateTicks = album.ReleaseDate.Ticks,
Admin = isAdmin, RequestedDate = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Humanize(),
Issues = tv.Issues.Humanize(LetterCasing.Title), RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Ticks,
OtherMessage = tv.OtherMessage, Approved = album.Available || album.Approved,
AdminNotes = tv.AdminNote, Title = album.Title,
TvSeriesRequestType = tv.SeasonsRequested Overview = album.Overview,
RequestedUsers = isAdmin ? album.AllUsers.ToArray() : new string[] { },
ReleaseYear = album.ReleaseDate.Year.ToString(),
Available = album.Available,
Admin = isAdmin,
Issues = album.Issues.Humanize(LetterCasing.Title),
OtherMessage = album.OtherMessage,
AdminNotes = album.AdminNote,
TvSeriesRequestType = album.SeasonsRequested,
MusicBrainzId = album.MusicBrainzId
};
}).ToList(); }).ToList();
return Response.AsJson(viewModel); return Response.AsJson(viewModel);
@ -165,7 +227,7 @@ namespace PlexRequests.UI.Modules
} }
originalRequest.Issues = issue; originalRequest.Issues = issue;
originalRequest.OtherMessage = !string.IsNullOrEmpty(comment) originalRequest.OtherMessage = !string.IsNullOrEmpty(comment)
? $"{Session[SessionKeys.UsernameKey]} - {comment}" ? $"{Username} - {comment}"
: string.Empty; : string.Empty;
@ -173,7 +235,7 @@ namespace PlexRequests.UI.Modules
var model = new NotificationModel var model = new NotificationModel
{ {
User = Session[SessionKeys.UsernameKey].ToString(), User = Username,
NotificationType = NotificationType.Issue, NotificationType = NotificationType.Issue,
Title = originalRequest.Title, Title = originalRequest.Title,
DateTime = DateTime.Now, DateTime = DateTime.Now,

@ -36,6 +36,7 @@ using NLog;
using PlexRequests.Api; using PlexRequests.Api;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Music;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
@ -54,7 +55,7 @@ namespace PlexRequests.UI.Modules
ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker, ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker,
IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings, IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi,
INotificationService notify) : base("search") INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService<HeadphonesSettings> hpService) : base("search")
{ {
CpService = cpSettings; CpService = cpSettings;
PrService = prSettings; PrService = prSettings;
@ -69,17 +70,23 @@ namespace PlexRequests.UI.Modules
SickRageService = sickRageService; SickRageService = sickRageService;
SickrageApi = srApi; SickrageApi = srApi;
NotificationService = notify; NotificationService = notify;
MusicBrainzApi = mbApi;
HeadphonesApi = hpApi;
HeadphonesService = hpService;
Get["/"] = parameters => RequestLoad(); Get["/"] = parameters => RequestLoad();
Get["movie/{searchTerm}"] = parameters => SearchMovie((string)parameters.searchTerm); Get["movie/{searchTerm}"] = parameters => SearchMovie((string)parameters.searchTerm);
Get["tv/{searchTerm}"] = parameters => SearchTvShow((string)parameters.searchTerm); Get["tv/{searchTerm}"] = parameters => SearchTvShow((string)parameters.searchTerm);
Get["music/{searchTerm}"] = parameters => SearchMusic((string)parameters.searchTerm);
Get["movie/upcoming"] = parameters => UpcomingMovies(); Get["movie/upcoming"] = parameters => UpcomingMovies();
Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); Get["movie/playing"] = parameters => CurrentlyPlayingMovies();
Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId); Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId);
Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId);
} }
private TheMovieDbApi MovieApi { get; } private TheMovieDbApi MovieApi { get; }
private INotificationService NotificationService { get; } private INotificationService NotificationService { get; }
@ -93,9 +100,11 @@ namespace PlexRequests.UI.Modules
private ISettingsService<PlexRequestSettings> PrService { get; } private ISettingsService<PlexRequestSettings> PrService { get; }
private ISettingsService<SonarrSettings> SonarrService { get; } private ISettingsService<SonarrSettings> SonarrService { get; }
private ISettingsService<SickRageSettings> SickRageService { get; } private ISettingsService<SickRageSettings> SickRageService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
private IAvailabilityChecker Checker { get; } private IAvailabilityChecker Checker { get; }
private IMusicBrainzApi MusicBrainzApi { get; }
private IHeadphonesApi HeadphonesApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
private string AuthToken => Cache.GetOrSet(CacheKeys.TvDbToken, TvApi.Authenticate, 50);
private Negotiator RequestLoad() private Negotiator RequestLoad()
{ {
@ -152,6 +161,30 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(model); return Response.AsJson(model);
} }
private Response SearchMusic(string searchTerm)
{
var albums = MusicBrainzApi.SearchAlbum(searchTerm);
var releases = albums.releases ?? new List<Release>();
var model = new List<SearchMusicViewModel>();
foreach (var a in releases)
{
var img = GetMusicBrainzCoverArt(a.id);
model.Add(new SearchMusicViewModel
{
Title = a.title,
Id = a.id,
Artist = a.ArtistCredit?.Select(x => x.artist?.name).FirstOrDefault(),
Overview = a.disambiguation,
ReleaseDate = a.date,
TrackCount = a.TrackCount,
CoverArtUrl = img,
ReleaseType = a.status,
Country = a.country
});
}
return Response.AsJson(model);
}
private Response UpcomingMovies() // TODO : Not used private Response UpcomingMovies() // TODO : Not used
{ {
var movies = MovieApi.GetUpcomingMovies(); var movies = MovieApi.GetUpcomingMovies();
@ -174,16 +207,26 @@ namespace PlexRequests.UI.Modules
{ {
var movieApi = new TheMovieDbApi(); var movieApi = new TheMovieDbApi();
var movieInfo = movieApi.GetMovieInformation(movieId).Result; var movieInfo = movieApi.GetMovieInformation(movieId).Result;
string fullMovieName = string.Format("{0}{1}", movieInfo.Title, movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty); var fullMovieName = $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}";
Log.Trace("Getting movie info from TheMovieDb"); Log.Trace("Getting movie info from TheMovieDb");
Log.Trace(movieInfo.DumpJson); Log.Trace(movieInfo.DumpJson);
//#if !DEBUG //#if !DEBUG
var settings = PrService.GetSettings();
// check if the movie has already been requested
Log.Info("Requesting movie with id {0}", movieId); Log.Info("Requesting movie with id {0}", movieId);
if (RequestService.CheckRequest(movieId)) var existingRequest = RequestService.CheckRequest(movieId);
if (existingRequest != null)
{ {
Log.Trace("movie with id {0} exists", movieId); // check if the current user is already marked as a requester for this movie, if not, add them
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} has already been requested!" }); if (!existingRequest.UserHasRequested(Username))
{
existingRequest.RequestedUsers.Add(Username);
RequestService.UpdateRequest(existingRequest);
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} was successfully added!" : $"{fullMovieName} has already been requested!" });
} }
Log.Debug("movie with id {0} doesnt exists", movieId); Log.Debug("movie with id {0} doesnt exists", movieId);
@ -211,16 +254,14 @@ namespace PlexRequests.UI.Modules
Title = movieInfo.Title, Title = movieInfo.Title,
ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue,
Status = movieInfo.Status, Status = movieInfo.Status,
RequestedDate = DateTime.Now, RequestedDate = DateTime.UtcNow,
Approved = false, Approved = false,
RequestedBy = Session[SessionKeys.UsernameKey].ToString(), RequestedUsers = new List<string>() { Username },
Issues = IssueState.None, Issues = IssueState.None,
}; };
var settings = PrService.GetSettings();
Log.Trace(settings.DumpJson()); Log.Trace(settings.DumpJson());
if (!settings.RequireMovieApproval) if (!settings.RequireMovieApproval || settings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase)))
{ {
var cpSettings = CpService.GetSettings(); var cpSettings = CpService.GetSettings();
@ -247,7 +288,7 @@ namespace PlexRequests.UI.Modules
}; };
NotificationService.Publish(notificationModel); NotificationService.Publish(notificationModel);
return Response.AsJson(new JsonResponseModel {Result = true}); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
} }
return return
Response.AsJson(new JsonResponseModel Response.AsJson(new JsonResponseModel
@ -272,7 +313,7 @@ namespace PlexRequests.UI.Modules
}; };
NotificationService.Publish(notificationModel); NotificationService.Publish(notificationModel);
return Response.AsJson(new JsonResponseModel { Result = true }); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
} }
} }
@ -310,9 +351,20 @@ namespace PlexRequests.UI.Modules
string fullShowName = $"{showInfo.name} ({firstAir.Year})"; string fullShowName = $"{showInfo.name} ({firstAir.Year})";
//#if !DEBUG //#if !DEBUG
if (RequestService.CheckRequest(showId)) var settings = PrService.GetSettings();
// check if the show has already been requested
Log.Info("Requesting tv show with id {0}", showId);
var existingRequest = RequestService.CheckRequest(showId);
if (existingRequest != null)
{ {
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} has already been requested!" }); // check if the current user is already marked as a requester for this show, if not, add them
if (!existingRequest.UserHasRequested(Username))
{
existingRequest.RequestedUsers.Add(Username);
RequestService.UpdateRequest(existingRequest);
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullShowName} was successfully added!" : $"{fullShowName} has already been requested!" });
} }
try try
@ -338,9 +390,9 @@ namespace PlexRequests.UI.Modules
Title = showInfo.name, Title = showInfo.name,
ReleaseDate = firstAir, ReleaseDate = firstAir,
Status = showInfo.status, Status = showInfo.status,
RequestedDate = DateTime.Now, RequestedDate = DateTime.UtcNow,
Approved = false, Approved = false,
RequestedBy = Session[SessionKeys.UsernameKey].ToString(), RequestedUsers = new List<string>() { Username },
Issues = IssueState.None, Issues = IssueState.None,
ImdbId = showInfo.externals?.imdb ?? string.Empty, ImdbId = showInfo.externals?.imdb ?? string.Empty,
SeasonCount = showInfo.seasonCount SeasonCount = showInfo.seasonCount
@ -363,26 +415,26 @@ namespace PlexRequests.UI.Modules
model.SeasonList = seasonsList.ToArray(); model.SeasonList = seasonsList.ToArray();
var settings = PrService.GetSettings(); if (!settings.RequireTvShowApproval || settings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase)))
if (!settings.RequireTvShowApproval)
{ {
var sonarrSettings = SonarrService.GetSettings(); var sonarrSettings = SonarrService.GetSettings();
var sender = new TvSender(SonarrApi, SickrageApi); var sender = new TvSender(SonarrApi, SickrageApi);
if (sonarrSettings.Enabled) if (sonarrSettings.Enabled)
{ {
var result = sender.SendToSonarr(sonarrSettings, model); var result = sender.SendToSonarr(sonarrSettings, model);
if (result != null) if (result != null && !string.IsNullOrEmpty(result.title))
{ {
model.Approved = true; model.Approved = true;
Log.Debug("Adding tv to database requests (No approval required & Sonarr)"); Log.Debug("Adding tv to database requests (No approval required & Sonarr)");
RequestService.AddRequest(model); RequestService.AddRequest(model);
var notify1 = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
NotificationService.Publish(notify1);
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" });
} }
var notify1 = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
NotificationService.Publish(notify1);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to Sonarr! Please check your settings." });
return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.ErrorMessage ?? "Something went wrong adding the movie to Sonarr! Please check your settings." });
} }
@ -421,5 +473,111 @@ namespace PlexRequests.UI.Modules
var result = Checker.IsAvailable(title, year); var result = Checker.IsAvailable(title, year);
return result; return result;
} }
private Response RequestAlbum(string releaseId)
{
var settings = PrService.GetSettings();
var existingRequest = RequestService.CheckRequest(releaseId);
Log.Debug("Checking for an existing request");
if (existingRequest != null)
{
Log.Debug("We do have an existing album request");
if (!existingRequest.UserHasRequested(Username))
{
Log.Debug("Not in the requested list so adding them and updating the request. User: {0}", Username);
existingRequest.RequestedUsers.Add(Username);
RequestService.UpdateRequest(existingRequest);
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{existingRequest.Title} was successfully added!" : $"{existingRequest.Title} has already been requested!" });
}
Log.Debug("This is a new request");
var albumInfo = MusicBrainzApi.GetAlbum(releaseId);
var img = GetMusicBrainzCoverArt(albumInfo.id);
Log.Trace("Album Details:");
Log.Trace(albumInfo.DumpJson());
Log.Trace("CoverArt Details:");
Log.Trace(img.DumpJson());
var model = new RequestedModel
{
Title = albumInfo.title,
MusicBrainzId = albumInfo.id,
Overview = albumInfo.disambiguation,
PosterPath = img,
Type = RequestType.Album,
ProviderId = 0,
RequestedUsers = new List<string>() { Username },
Status = albumInfo.status,
Issues = IssueState.None
};
if (!settings.RequireMusicApproval ||
settings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase)))
{
Log.Debug("We don't require approval OR the user is in the whitelist");
var hpSettings = HeadphonesService.GetSettings();
Log.Trace("Headphone Settings:");
Log.Trace(hpSettings.DumpJson());
if (!hpSettings.Enabled)
{
RequestService.AddRequest(model);
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{model.Title} was successfully added!"
});
}
var headphonesResult = HeadphonesApi.AddAlbum(hpSettings.ApiKey, hpSettings.FullUri, model.MusicBrainzId);
Log.Info("Result from adding album to Headphones = {0}", headphonesResult);
RequestService.AddRequest(model);
if (headphonesResult)
{
return
Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{model.Title} was successfully added!"
});
}
return
Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"There was a problem adding {model.Title}. Please contact your admin!"
});
}
var result = RequestService.AddRequest(model);
return Response.AsJson(new JsonResponseModel
{
Result = true,
Message = $"{model.Title} was successfully added!"
});
}
private string GetMusicBrainzCoverArt(string id)
{
var coverArt = MusicBrainzApi.GetCoverArt(id);
var firstImage = coverArt?.images?.FirstOrDefault();
var img = string.Empty;
if (firstImage != null)
{
img = firstImage.thumbnails?.small ?? firstImage.image;
}
return img;
}
} }
} }

@ -68,6 +68,7 @@ namespace PlexRequests.UI.Modules
private Response LoginUser() private Response LoginUser()
{ {
var dateTimeOffset = Request.Form.DateTimeOffset;
var username = Request.Form.username.Value; var username = Request.Form.username.Value;
Log.Debug("Username \"{0}\" attempting to login",username); Log.Debug("Username \"{0}\" attempting to login",username);
if (string.IsNullOrWhiteSpace(username)) if (string.IsNullOrWhiteSpace(username))
@ -138,6 +139,8 @@ namespace PlexRequests.UI.Modules
Session[SessionKeys.UsernameKey] = (string)username; Session[SessionKeys.UsernameKey] = (string)username;
} }
Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset;
return Response.AsJson(authenticated return Response.AsJson(authenticated
? new JsonResponseModel { Result = true } ? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "Incorrect User or Password"}); : new JsonResponseModel { Result = false, Message = "Incorrect User or Password"});
@ -170,7 +173,7 @@ namespace PlexRequests.UI.Modules
var users = Api.GetUsers(authToken); var users = Api.GetUsers(authToken);
Log.Debug("Plex Users: "); Log.Debug("Plex Users: ");
Log.Debug(users.DumpJson()); Log.Debug(users.DumpJson());
var allUsers = users.User?.Where(x => !string.IsNullOrEmpty(x.Username)); var allUsers = users?.User?.Where(x => !string.IsNullOrEmpty(x.Username));
return allUsers != null && allUsers.Any(x => x.Username.Equals(username, StringComparison.CurrentCultureIgnoreCase)); return allUsers != null && allUsers.Any(x => x.Username.Equals(username, StringComparison.CurrentCultureIgnoreCase));
} }

@ -171,6 +171,7 @@
<Compile Include="Helpers\TvSender.cs" /> <Compile Include="Helpers\TvSender.cs" />
<Compile Include="Helpers\ValidationHelper.cs" /> <Compile Include="Helpers\ValidationHelper.cs" />
<Compile Include="Models\DatatablesModel.cs" /> <Compile Include="Models\DatatablesModel.cs" />
<Compile Include="Models\SearchMusicViewModel.cs" />
<Compile Include="Validators\PushoverSettingsValidator.cs" /> <Compile Include="Validators\PushoverSettingsValidator.cs" />
<Compile Include="Validators\PushbulletSettingsValidator.cs" /> <Compile Include="Validators\PushbulletSettingsValidator.cs" />
<Compile Include="Validators\EmailNotificationSettingsValidator.cs" /> <Compile Include="Validators\EmailNotificationSettingsValidator.cs" />
@ -371,6 +372,9 @@
<Content Include="Views\Admin\PushoverNotifications.cshtml"> <Content Include="Views\Admin\PushoverNotifications.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Views\Admin\Headphones.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Web.Debug.config"> <None Include="Web.Debug.config">
<DependentUpon>web.config</DependentUpon> <DependentUpon>web.config</DependentUpon>
</None> </None>

@ -88,6 +88,11 @@
</div> </div>
</div> </div>
<div class="form-group">
<div>
<button id="testEmail" type="submit" class="btn btn-primary-outline">Test</button>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>
@ -128,7 +133,32 @@
}); });
}); });
$('#testEmail').click(function (e) {
e.preventDefault();
var port = $('#EmailPort').val();
if (isNaN(port)) {
generateNotify("You must specify a valid Port.", "warning");
return;
}
var $form = $("#mainForm");
$.ajax({
type: $form.prop("method"),
data: $form.serialize(),
url: '/admin/testemailnotification',
dataType: "json",
success: function (response) {
if (response.result === true) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
}); });

@ -0,0 +1,162 @@
@Html.Partial("_Sidebar")
@{
int port;
if (Model.Port == 0)
{
port = 8081;
}
else
{
port = Model.Port;
}
}
<div class="col-sm-8 col-sm-push-1">
<form class="form-horizontal" method="POST" id="mainForm">
<fieldset>
<legend>Headphones Settings</legend>
<div class="form-group">
<div class="checkbox">
<label>
@if (Model.Enabled)
{
<input type="checkbox" id="Enabled" name="Enabled" checked="checked"><text>Enabled</text>
}
else
{
<input type="checkbox" id="Enabled" name="Enabled"><text>Enabled</text>
}
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
@if (Model.Ssl)
{
<input type="checkbox" id="Ssl" name="Ssl" checked="checked"><text>SSL</text>
}
else
{
<input type="checkbox" id="Ssl" name="Ssl"><text>SSL</text>
}
</label>
</div>
</div>
<div class="form-group">
<label for="Ip" class="control-label">Headphones Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" value="@Model.Ip">
</div>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<div class="">
<input type="text" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" value="@port">
</div>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">Headphones API Key</label>
<div>
<input type="text" class="form-control form-control-custom " id="ApiKey" name="ApiKey" value="@Model.ApiKey">
</div>
</div>
<div class="form-group">
<label for="SubDir" class="control-label">Headphones SubDirectory</label>
<div>
<input type="text" class="form-control form-control-custom " id="SubDir" name="SubDir" value="@Model.SubDir">
</div>
</div>
<div class="form-group">
<div>
<button type="submit" id="getProfiles" class="btn btn-primary-outline">Get Quality Profiles</button>
</div>
</div>
<div class="form-group">
<div>
<button id="testHeadphones" type="submit" class="btn btn-primary-outline">Test Connectivity</button>
</div>
</div>
<div class="form-group">
<div>
<button id="save" type="submit" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</fieldset>
</form>
</div>
<script>
$(function() {
$('#testHeadphones').click(function (e) {
e.preventDefault();
var $form = $("#mainForm");
$.ajax({
type: $form.prop("method"),
url: "/test/headphones",
data: $form.serialize(),
dataType: "json",
success: function (response) {
console.log(response);
if (response.result === true) {
generateNotify(response.message, "success");
$('#authToken').val(response.authToken);
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
$('#save').click(function (e) {
e.preventDefault();
var port = $('#portNumber').val();
if (isNaN(port)) {
generateNotify("You must specify a Port.", "warning");
return;
}
var $form = $("#mainForm");
var qualityProfile = $("#profiles option:selected").val();
var data = $form.serialize();
data = data + "&profileId=" + qualityProfile;
$.ajax({
type: $form.prop("method"),
data: data,
url: $form.prop("action"),
dataType: "json",
success: function (response) {
if (response.result === true) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
});
</script>

@ -36,6 +36,12 @@
</div> </div>
</div> </div>
<div class="form-group">
<div>
<button id="testPushbullet" type="submit" class="btn btn-primary-outline">Test</button>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>
<button id="save" type="submit" class="btn btn-primary-outline">Submit</button> <button id="save" type="submit" class="btn btn-primary-outline">Submit</button>
@ -70,5 +76,28 @@
} }
}); });
}); });
$('#testPushbullet').click(function (e) {
e.preventDefault();
var $form = $("#mainForm");
$.ajax({
type: $form.prop("method"),
data: $form.serialize(),
url: '/admin/testpushbulletnotification',
dataType: "json",
success: function (response) {
if (response.result === true) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
}); });
</script> </script>

@ -36,6 +36,12 @@
</div> </div>
</div> </div>
<div class="form-group">
<div>
<button id="testPushover" type="submit" class="btn btn-primary-outline">Test</button>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>
<button id="save" type="submit" class="btn btn-primary-outline">Submit</button> <button id="save" type="submit" class="btn btn-primary-outline">Submit</button>
@ -70,5 +76,28 @@
} }
}); });
}); });
$('#testPushover').click(function (e) {
e.preventDefault();
var $form = $("#mainForm");
$.ajax({
type: $form.prop("method"),
data: $form.serialize(),
url: '/admin/testpushovernotification',
dataType: "json",
success: function (response) {
if (response.result === true) {
generateNotify(response.message, "success");
} else {
generateNotify(response.message, "warning");
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
});
}); });
</script> </script>

@ -52,6 +52,20 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
<label>
@if (Model.SearchForMusic)
{
<input type="checkbox" id="SearchForMusic" name="SearchForMusic" checked="checked"><text>Search for Music</text>
}
else
{
<input type="checkbox" id="SearchForMusic" name="SearchForMusic"><text>Search for Music</text>
}
</label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<label> <label>
@ -82,6 +96,45 @@
</div> </div>
</div> </div>
<div class="form-group">
<div class="checkbox">
<label>
@if (Model.RequireMusicApproval)
{
<input type="checkbox" id="RequireMusicApproval" name="RequireMusicApproval" checked="checked"><text>Require approval of Music requests</text>
}
else
{
<input type="checkbox" id="RequireMusicApproval" name="RequireMusicApproval"><text>Require approval of Music requests</text>
}
</label>
</div>
</div>
<p class="form-group">A comma separated list of users whose requests do not require approval.</p>
<div class="form-group">
<label for="noApprovalUsers" class="control-label">Users</label>
<div>
<input type="text" class="form-control-custom form-control " id="NoApprovalUsers" name="NoApprovalUsers" placeholder="e.g. John, Bobby" value="@Model.NoApprovalUsers">
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
@if (Model.UsersCanViewOnlyOwnRequests)
{
<input type="checkbox" id="UsersCanViewOnlyOwnRequests" name="UsersCanViewOnlyOwnRequests" checked="checked"><text>Users can view their own requests only</text>
}
else
{
<input type="checkbox" id="UsersCanViewOnlyOwnRequests" name="UsersCanViewOnlyOwnRequests"><text>Users can view their own requests only</text>
}
</label>
</div>
</div>
@*<div class="form-group"> @*<div class="form-group">
<label for="WeeklyRequestLimit" class="control-label">Weekly Request Limit</label> <label for="WeeklyRequestLimit" class="control-label">Weekly Request Limit</label>
@ -102,4 +155,3 @@
</fieldset> </fieldset>
</form> </form>
</div> </div>

@ -52,6 +52,14 @@
{ {
<a class="list-group-item" href="/admin/sickrage">SickRage</a> <a class="list-group-item" href="/admin/sickrage">SickRage</a>
} }
@if (Context.Request.Path == "/admin/headphones")
{
<a class="list-group-item active" href="/admin/headphones">Headphones</a>
}
else
{
<a class="list-group-item" href="/admin/headphones">Headphones</a>
}
@if (Context.Request.Path == "/admin/emailnotification") @if (Context.Request.Path == "/admin/emailnotification")
{ {

@ -4,8 +4,9 @@
Password <input class="form-control form-control-custom" name="Password" type="password"/> Password <input class="form-control form-control-custom" name="Password" type="password"/>
<br/> <br/>
Remember Me <input name="RememberMe" type="checkbox" value="True"/> Remember Me <input name="RememberMe" type="checkbox" value="True"/>
<br/> <br/><br/>
<input class="btn btn-success-outline" type="submit" value="Login"/> <input class="btn btn-success-outline" type="submit" value="Login"/>
<input type="hidden" id="DateTimeOffset" name="DateTimeOffset" />
</form> </form>
@if (!Model.AdminExists) @if (!Model.AdminExists)
{ {
@ -19,3 +20,9 @@
</div> </div>
} }
<script>
$(function () {
var dtOffset = new Date().getTimezoneOffset();
$('#DateTimeOffset').val(dtOffset);
});
</script>

@ -1,7 +1,7 @@
<form method="POST"> <form method="POST">
Username <input class="form-control" type="text" name="Username" /> Username <input class="form-control form-control-custom" type="text" name="Username" />
<br /> <br />
Password <input class="form-control" name="Password" type="password" /> Password <input class="form-control form-control-custom" name="Password" type="password" />
<br /> <br />
<br /> <br />
<input class="btn btn-success-outline" type="submit" value="Create User" /> <input class="btn btn-success-outline" type="submit" value="Create User" />

@ -2,12 +2,8 @@
<div> <div>
<h1>Requests</h1> <h1>Requests</h1>
<h4>Below you can see yours and all other requests, as well as their download and approval status.</h4> <h4>Below you can see yours and all other requests, as well as their download and approval status.</h4>
@if (Context.CurrentUser.IsAuthenticated()) <br />
{
<button id="approveAll" class="btn btn-success-outline" type="submit"><i class="fa fa-plus"></i> Approve All</button>
<br />
<br />
}
<!-- Nav tabs --> <!-- Nav tabs -->
<ul id="nav-tabs" class="nav nav-tabs" role="tablist"> <ul id="nav-tabs" class="nav nav-tabs" role="tablist">
@if (Model.SearchForMovies) @if (Model.SearchForMovies)
@ -18,42 +14,71 @@
{ {
<li role="presentation"><a href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab">TV Shows</a></li> <li role="presentation"><a href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab">TV Shows</a></li>
} }
@if (Model.SearchForMusic)
{
<li role="presentation"><a href="#MusicTab" aria-controls="profile" role="tab" data-toggle="tab">Albums</a></li>
}
</ul> </ul>
<br />
<!-- Tab panes --> <!-- Tab panes -->
<div class="tab-content contentList"> <div class="tab-content contentList">
<div class="btn-group col-sm-push-10"> <div class="row">
<a href="#" class="btn btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <div class="col-sm-12">
Filter <div class="pull-right">
<i class="fa fa-filter"></i> <div class="btn-group">
</a> @if (Context.CurrentUser.IsAuthenticated())
<ul class="dropdown-menu"> {
<li><a href="#" class="filter" data-filter="all">All</a></li> @if (Model.SearchForMovies)
<li><a href="#" class="filter" data-filter=".approved-true">Approved</a></li> {
<li><a href="#" class="filter" data-filter=".approved-false">Not Approved</a></li> <button id="approveMovies" class="btn btn-success-outline approve-category" type="submit"><i class="fa fa-plus"></i> Approve Movies</button>
<li><a href="#" class="filter" data-filter=".available-true">Available</a></li> }
<li><a href="#" class="filter" data-filter=".available-false">Not Available</a></li> @if (Model.SearchForTvShows)
</ul> {
</div> <button id="approveTVShows" class="btn btn-success-outline approve-category" type="submit" style="display: none;"><i class="fa fa-plus"></i> Approve TV Shows</button>
<div class="btn-group col-sm-push-10"> }
<a href="#" class="btn btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> @if (Model.SearchForMusic)
Order {
<i class="fa fa-sort"></i> <button id="approveMusic" class="btn btn-success-outline approve-category" type="submit" style="display: none;"><i class="fa fa-plus"></i> Approve Music</button>
</a> }
<ul class="dropdown-menu"> }
<li><a href="#" class="sort" data-sort="default">Default</a></li> </div>
<li><a href="#" class="sort" data-sort="requestorder:asc">Requested Date</a></li> <div class="btn-group">
</ul> <a href="#" class="btn btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Filter
<i class="fa fa-filter"></i>
</a>
<ul class="dropdown-menu">
<li><a href="#" class="filter" data-filter="all"><i class="fa fa-check-square"></i> All</a></li>
<li><a href="#" class="filter" data-filter=".approved-true"><i class="fa fa-square-o"></i> Approved</a></li>
<li><a href="#" class="filter" data-filter=".approved-false"><i class="fa fa-square-o"></i> Not Approved</a></li>
<li><a href="#" class="filter" data-filter=".available-true"><i class="fa fa-square-o"></i> Available</a></li>
<li><a href="#" class="filter" data-filter=".available-false"><i class="fa fa-square-o"></i> Not Available</a></li>
</ul>
</div>
<div class="btn-group">
<a href="#" class="btn btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Order
<i class="fa fa-sort"></i>
</a>
<ul class="dropdown-menu">
<li><a href="#" class="sort" data-sort="requestorder:desc"><i class="fa fa-check-square"></i> Latest Requests</a></li>
<li><a href="#" class="sort" data-sort="requestorder:asc"><i class="fa fa-square-o"></i> Oldest Requests</a></li>
<li><a href="#" class="sort" data-sort="releaseorder:desc"><i class="fa fa-square-o"></i> Latest Releases</a></li>
<li><a href="#" class="sort" data-sort="releaseorder:asc"><i class="fa fa-square-o"></i> Oldest Releases</a></li>
</ul>
</div>
</div>
</div>
</div> </div>
@if (Model.SearchForMovies) @if (Model.SearchForMovies)
{ {
<!-- Movie tab --> <!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab"> <div role="tabpanel" class="tab-pane active" id="MoviesTab">
<br/> <br />
<br/> <br />
<!-- Movie content --> <!-- Movie content -->
<div id="movieList"> <div id="movieList">
</div> </div>
@ -61,24 +86,37 @@
} }
@if (Model.SearchForTvShows) @if (Model.SearchForTvShows)
{ {
<!-- TV tab --> <!-- TV tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab"> <div role="tabpanel" class="tab-pane" id="TvShowTab">
<br/> <br />
<br/> <br />
<!-- TV content --> <!-- TV content -->
<div id="tvList"> <div id="tvList">
</div> </div>
</div> </div>
} }
@if (Model.SearchForMusic)
{
<!-- Music tab -->
<div role="tabpanel" class="tab-pane" id="MusicTab">
<br />
<br />
<!-- TV content -->
<div id="MusicList">
</div>
</div>
}
</div> </div>
</div> </div>
<script id="search-template" type="text/x-handlebars-template"> <script id="search-template" type="text/x-handlebars-template">
<div id="{{requestId}}Template" class="mix available-{{available}} approved-{{approved}}"> <div id="{{requestId}}Template" class="mix available-{{available}} approved-{{approved}}" data-requestorder="{{requestedDateTicks}}" data-releaseorder="{{releaseDateTicks}}">
<div class="row"> <div class="row">
<div class="col-sm-2"> <div class="col-sm-2">
{{#if_eq type "movie"}} {{#if_eq type "movie"}}
@ -122,7 +160,122 @@
{{#if_eq type "tv"}} {{#if_eq type "tv"}}
<div>Series Requested: {{seriesRequested}}</div> <div>Series Requested: {{seriesRequested}}</div>
{{/if_eq}} {{/if_eq}}
<div>Requested By: {{requestedBy}}</div> {{#if requestedUsers}}
<div>Requested By: {{requestedUsers}}</div>
{{/if}}
<div>Requested Date: {{requestedDate}}</div>
<div id="issueArea{{requestId}}">
{{#if otherMessage}}
<div>Message: {{otherMessage}}</div>
{{else}}
<div>Issue: {{issues}}</div>
{{/if}}
</div>
<div id="adminNotesArea{{requestId}}">
{{#if adminNote}}
<div>Note from Admin: {{adminNote}}</div>
{{/if}}
</div>
</div>
<div class="col-sm-2 col-sm-push-3">
{{#if_eq admin true}}
{{#if_eq approved false}}
<form method="POST" action="/approval/approve" id="approve{{requestId}}">
<input name="requestId" type="text" value="{{requestId}}" hidden="hidden" />
<button id="{{requestId}}" custom-button="{{requestId}}" style="text-align: right" class="btn btn-sm btn-success-outline approve" type="submit"><i class="fa fa-plus"></i> Approve</button>
</form>
{{/if_eq}}
<form method="POST" action="/requests/delete" id="delete{{requestId}}">
<input name="Id" type="text" value="{{requestId}}" hidden="hidden" />
<button id="{{requestId}}" style="text-align: right" class="btn btn-sm btn-danger-outline delete" type="submit"><i class="fa fa-minus"></i> Remove</button>
</form>
<form method="POST" action="/requests/clearissues" id="clear{{requestId}}">
<input name="Id" type="text" value="{{requestId}}" hidden="hidden" />
<button id="{{requestId}}" style="text-align: right" class="btn btn-sm btn-info-outline clear" type="submit"><i class="fa fa-check"></i> Clear Issues</button>
</form>
<form method="POST" action="/requests/changeavailability" id="change{{requestId}}">
<input name="Id" type="text" value="{{requestId}}" hidden="hidden" />
{{#if_eq available true}}
<button id="{{requestId}}" custom-availibility="{{requestId}}" style="text-align: right" value="false" class="btn btn-sm btn-info-outline change" type="submit"><i class="fa fa-minus"></i> Mark Unavailable</button>
{{else}}
<button id="{{requestId}}" custom-availibility="{{requestId}}" style="text-align: right" value="true" class="btn btn-sm btn-success-outline change" type="submit"><i class="fa fa-plus"></i> Mark Available</button>
{{/if_eq}}
</form>
{{/if_eq}}
<form method="POST" action="/requests/reportissue/" id="report{{requestId}}">
<input name="requestId" type="text" value="{{requestId}}" hidden="hidden" />
<div class="dropdown">
<button id="{{requestId}}" class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fa fa-plus"></i> Report Issue
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a id="{{requestId}}" issue-select="0" class="dropdownIssue" href="#">Wrong Audio</a></li>
<li><a id="{{requestId}}" issue-select="1" class="dropdownIssue" href="#">No Subtitles</a></li>
<li><a id="{{requestId}}" issue-select="2" class="dropdownIssue" href="#">Wrong Content</a></li>
<li><a id="{{requestId}}" issue-select="3" class="dropdownIssue" href="#">Playback Issues</a></li>
<li><a id="{{requestId}}" issue-select="4" class="dropdownIssue" data-identifier="{{requestId}}" href="#" data-toggle="modal" data-target="#myModal">Other</a></li>
{{#if_eq admin true}}
<li><a id="{{requestId}}" issue-select="4" class="note" data-identifier="{{requestId}}" href="#" data-toggle="modal" data-target="#noteModal">Add Note</a></li>
{{/if_eq}}
</ul>
</div>
</form>
</div>
</div>
<hr />
</div>
</script>
<script id="album-template" type="text/x-handlebars-template">
<div id="{{requestId}}Template" class="mix available-{{available}} approved-{{approved}}" data-requestorder="{{requestedDateTicks}}" data-releaseorder="{{releaseDateTicks}}">
<div class="row">
<div class="col-sm-2">
{{#if coverArtUrl}}
<img class="img-responsive" src="{{coverArtUrl}}" width="150" alt="poster">
{{/if}}
</div>
<div class="col-sm-5 ">
<div>
<a href="https://musicbrainz.org/release/{{id}}" target="_blank">
<h4>
{{artist}} - {{title}}
{{#if year}}
({{year}})
{{/if}}
</h4>
</a>
<span class="label label-success">{{status}}</span>
</div>
<br />
<div>Release Date: {{releaseDate}}</div>
<div>
Approved:
{{#if_eq approved false}}
<i id="{{requestId}}notapproved" class="fa fa-times"></i>
{{/if_eq}}
{{#if_eq approved true}}
<i class="fa fa-check"></i>
{{/if_eq}}
</div>
<div>
Available
{{#if_eq available false}}
<i id="availableIcon{{requestId}}" class="fa fa-times"></i>
{{/if_eq}}
{{#if_eq available true}}
<i id="availableIcon{{requestId}}" class="fa fa-check"></i>
{{/if_eq}}
</div>
{{#if requestedUsers}}
<div>Requested By: {{requestedUsers}}</div>
{{/if}}
<div>Requested Date: {{requestedDate}}</div> <div>Requested Date: {{requestedDate}}</div>
<div id="issueArea{{requestId}}"> <div id="issueArea{{requestId}}">
{{#if otherMessage}} {{#if otherMessage}}
@ -181,7 +334,7 @@
<li><a id="{{requestId}}" issue-select="4" class="dropdownIssue" data-identifier="{{requestId}}" href="#" data-toggle="modal" data-target="#myModal">Other</a></li> <li><a id="{{requestId}}" issue-select="4" class="dropdownIssue" data-identifier="{{requestId}}" href="#" data-toggle="modal" data-target="#myModal">Other</a></li>
{{#if_eq admin true}} {{#if_eq admin true}}
<li><a id="{{requestId}}" issue-select="4" class="note" data-identifier="{{requestId}}" href="#" data-toggle="modal" data-target="#noteModal">Add Note</a></li> <li><a id="{{requestId}}" issue-select="4" class="note" data-identifier="{{requestId}}" href="#" data-toggle="modal" data-target="#noteModal">Add Note</a></li>
{{/if_eq}} {{/if_eq}}
</ul> </ul>
</div> </div>

@ -1,6 +1,7 @@
<div> <div>
<h1>Search</h1> <h1>Search</h1>
<h4>Want to watch something that is not currently on Plex?! No problem! Just search for it below and request it!</h4> <h4>Want to watch something that is not currently on Plex?! No problem! Just search for it below and request it!</h4>
<br />
<!-- Nav tabs --> <!-- Nav tabs -->
<ul id="nav-tabs" class="nav nav-tabs" role="tablist"> <ul id="nav-tabs" class="nav nav-tabs" role="tablist">
@if (Model.SearchForMovies) @if (Model.SearchForMovies)
@ -11,6 +12,10 @@
{ {
<li role="presentation"><a href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab">TV Shows</a></li> <li role="presentation"><a href="#TvShowTab" aria-controls="profile" role="tab" data-toggle="tab">TV Shows</a></li>
} }
@if (Model.SearchForMusic)
{
<li role="presentation"><a href="#MusicTab" aria-controls="profile" role="tab" data-toggle="tab">Albums</a></li>
}
</ul> </ul>
@ -21,8 +26,18 @@
<!-- Movie tab --> <!-- Movie tab -->
<div role="tabpanel" class="tab-pane active" id="MoviesTab"> <div role="tabpanel" class="tab-pane active" id="MoviesTab">
<div class="input-group"> <div class="input-group">
<input id="movieSearchContent" type="text" class="form-control form-control-custom"> <input id="movieSearchContent" type="text" class="form-control form-control-custom form-control-search form-control-withbuttons">
<div class="input-group-addon"> <div class="input-group-addon">
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary-outline dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Suggestions
<i class="fa fa-chevron-down"></i>
</a>
<ul class="dropdown-menu">
<li><a id="moviesComingSoon" href="#">Coming Soon</a></li>
<li><a id="moviesInTheaters" href="#">In Theaters</a></li>
</ul>
</div>
<i id="movieSearchButton" class="fa fa-search"></i> <i id="movieSearchButton" class="fa fa-search"></i>
</div> </div>
</div> </div>
@ -40,7 +55,7 @@
<!-- TV tab --> <!-- TV tab -->
<div role="tabpanel" class="tab-pane" id="TvShowTab"> <div role="tabpanel" class="tab-pane" id="TvShowTab">
<div class="input-group"> <div class="input-group">
<input id="tvSearchContent" type="text" class="form-control form-control-custom"> <input id="tvSearchContent" type="text" class="form-control form-control-custom form-control-search">
<div class="input-group-addon"> <div class="input-group-addon">
<i id="tvSearchButton" class="fa fa-search"></i> <i id="tvSearchButton" class="fa fa-search"></i>
</div> </div>
@ -52,12 +67,30 @@
</div> </div>
</div> </div>
} }
@if (Model.SearchForMusic)
{
<!-- Music tab -->
<div role="tabpanel" class="tab-pane" id="MusicTab">
<div class="input-group">
<input id="musicSearchContent" type="text" class="form-control form-control-custom form-control-search">
<div class="input-group-addon">
<i id="musicSearchButton" class="fa fa-search"></i>
</div>
</div>
<br />
<br />
<!-- Music content -->
<div id="musicList">
</div>
</div>
}
</div> </div>
</div> </div>
<!-- Movie and TV Results template -->
<script id="search-template" type="text/x-handlebars-template"> <script id="search-template" type="text/x-handlebars-template">
<div class="row"> <div class="row">
<div class="col-sm-2"> <div class="col-sm-2">
@ -123,4 +156,42 @@
</script> </script>
<!-- Music Results template -->
<script id="music-template" type="text/x-handlebars-template">
<div class="row">
<div class="col-sm-2">
{{#if coverArtUrl}}
<img class="img-responsive" src="{{coverArtUrl}}" width="150" alt="poster">
{{/if}}
</div>
<div class="col-sm-5 ">
<div>
<a href="https://musicbrainz.org/release/{{id}}" target="_blank">
<h4>
{{artist}} - {{title}}
{{#if year}}
({{year}})
{{/if}}
</h4>
</a>
</div>
<p>{{overview}}</p>
</div>
<div class="col-sm-2 col-sm-push-3">
<form method="POST" action="/search/request/{{type}}" id="form{{id}}">
<input name="{{type}}Id" type="text" value="{{id}}" hidden="hidden" />
<button id="{{id}}" style="text-align: right" class="btn btn-primary-outline requestAlbum" type="submit"><i class="fa fa-plus"></i> Request</button>
<br />
<small class="row">Track Count: {{trackCount}}</small>
<small class="row">Country: {{country}}</small>
</form>
</div>
</div>
<hr />
</script>
<script src="/Content/search.js" type="text/javascript"></script> <script src="/Content/search.js" type="text/javascript"></script>

@ -7,8 +7,8 @@
<title>Plex Requests</title> <title>Plex Requests</title>
<!-- Styles --> <!-- Styles -->
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="~/Content/custom.min.css" type="text/css"/>
<link rel="stylesheet" href="~/Content/bootstrap.css" type="text/css"/> <link rel="stylesheet" href="~/Content/bootstrap.css" type="text/css"/>
<link rel="stylesheet" href="~/Content/custom.min.css" type="text/css" />
<link rel="stylesheet" href="~/Content/font-awesome.css" type="text/css"/> <link rel="stylesheet" href="~/Content/font-awesome.css" type="text/css"/>
<link rel="stylesheet" href="~/Content/pace.min.css" type="text/css"/> <link rel="stylesheet" href="~/Content/pace.min.css" type="text/css"/>
@ -87,8 +87,37 @@
</div> </div>
</nav> </nav>
<div class="container"> <div class="container">
@RenderBody() @RenderBody()
</div>
<div class="scroll-top-wrapper ">
<span class="scroll-top-inner">
<i class="fa fa-2x fa-arrow-circle-up"></i>
</span>
</div> </div>
</body> </body>
</html> </html>
<script>
$(function () {
$(document).on('scroll', function () {
if ($(window).scrollTop() > 100) {
$('.scroll-top-wrapper').addClass('show');
} else {
$('.scroll-top-wrapper').removeClass('show');
}
});
$('.scroll-top-wrapper').on('click', scrollToTop);
});
function scrollToTop() {
verticalOffset = typeof (verticalOffset) != 'undefined' ? verticalOffset : 0;
element = $('body');
offset = element.offset();
offsetTop = offset.top;
$('html, body').animate({ scrollTop: offsetTop }, 500, 'linear');
}
</script>

@ -38,10 +38,14 @@
$('#loginBtn').click(function (e) { $('#loginBtn').click(function (e) {
e.preventDefault(); e.preventDefault();
var $form = $("#loginForm"); var $form = $("#loginForm");
var formData = $form.serialize();
var dtOffset = new Date().getTimezoneOffset();
formData += ('&DateTimeOffset=' + dtOffset)
$.ajax({ $.ajax({
type: $form.prop("method"), type: $form.prop("method"),
url: $form.prop("action"), url: $form.prop("action"),
data: $form.serialize(), data: formData,
dataType: "json", dataType: "json",
success: function (response) { success: function (response) {
console.log(response); console.log(response);

@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.travis.yml = .travis.yml .travis.yml = .travis.yml
appveyor.yml = appveyor.yml appveyor.yml = appveyor.yml
.github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md
LICENSE = LICENSE LICENSE = LICENSE
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection

@ -10,19 +10,19 @@
This is based off [Plex Requests by lokenx](https://github.com/lokenx/plexrequests-meteor) so big props to that guy! This is based off [Plex Requests by lokenx](https://github.com/lokenx/plexrequests-meteor) so big props to that guy!
I wanted to write a similar application in .Net! I wanted to write a similar application in .Net!
#Features # Features
* Integration with [TheMovieDB](https://www.themoviedb.org/) for all Movies * Movie and TV Show searching, can't find something on Plex? Just request it!
* Integration with [TVMaze](www.tvmaze.com) for all TV shows! * Notifications! Get notified via Email, Pushbullet and Pushover for new requests and issue reports!
* Secure authentication * Send your TV Shows to either [Sonarr](https://sonarr.tv/) or [SickRage](http://www.sickrage.ca/)!
* [Sonarr](https://sonarr.tv/) integration (SickRage/Sickbeard TBD) * Secure authentication so you don't have to worry about those script kiddies
* [CouchPotato](https://couchpota.to/) integration * We check to see if the request is already in Plex, if it's already in Plex then why you requesting it?!
* [SickRage](https://sickrage.github.io/) integration * We have allowed the ability for a user to add a custom note on a request
* Email notifications * It automatically update the status of requests when they are available on Plex
* Pushbullet notifications * Sick, responsive and mobile friendly UI
* Pushover notifications * Headphones integration will be comming soon!
#Preview # Preview (Needs updating)
![Preview](http://i.imgur.com/ucCFUvd.gif) ![Preview](http://i.imgur.com/ucCFUvd.gif)
@ -31,10 +31,10 @@ Download the latest [Release](https://github.com/tidusjar/PlexRequests.Net/relea
Extract the .zip file (Unblock if on Windows! Right Click > Properties > Unblock). Extract the .zip file (Unblock if on Windows! Right Click > Properties > Unblock).
Just run `PlexRequests.exe`! (Mono compatible `mono PlexRequests.exe`) Just run `PlexRequests.exe`! (Mono compatible `mono PlexRequests.exe`)
#Configuration # FAQ
Do you have an issue or a question? if so check out our [FAQ!](https://github.com/tidusjar/PlexRequests.Net/wiki/FAQ)
To configure PlexRequests you need to register an admin user by clicking on Admin (top left) and press the Register link. # Docker
You will then have a admin menu option once registered where you can setup Sonarr, Couchpotato and any other settings.
Looking for a Docker Image? Well [rogueosb](https://github.com/rogueosb/) has created a docker image for us, You can find it [here](https://github.com/rogueosb/docker-plexrequestsnet) :smile: Looking for a Docker Image? Well [rogueosb](https://github.com/rogueosb/) has created a docker image for us, You can find it [here](https://github.com/rogueosb/docker-plexrequestsnet) :smile:
@ -84,7 +84,9 @@ end script
####Reboot, then open up your browser to check that it's running! ####Reboot, then open up your browser to check that it's running!
```sudo shutdown -r 00``` ```
sudo shutdown -r 00
```
# Contributors # Contributors
@ -95,14 +97,6 @@ Please feed free to submit a pull request!
# Donation # Donation
If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet) If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet)
###### A massive thanks to everyone below! ## A massive thanks to everyone below for all their help!
[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake)
# Sponsors [heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727), Majawat
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools!!!
- [ReSharper](http://www.jetbrains.com/resharper/)
- [dotTrace] (https://www.jetbrains.com/profiler/)
- [dotMemory] (https://www.jetbrains.com/dotmemory/)
- [dotCover] (https://www.jetbrains.com/dotcover/)

Loading…
Cancel
Save