diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..aa693030a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -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.
\ No newline at end of file
diff --git a/PlexRequests.Api.Interfaces/IHeadphonesApi.cs b/PlexRequests.Api.Interfaces/IHeadphonesApi.cs
new file mode 100644
index 000000000..2dee51d4f
--- /dev/null
+++ b/PlexRequests.Api.Interfaces/IHeadphonesApi.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Interfaces/IMusicBrainzApi.cs b/PlexRequests.Api.Interfaces/IMusicBrainzApi.cs
new file mode 100644
index 000000000..011c430e7
--- /dev/null
+++ b/PlexRequests.Api.Interfaces/IMusicBrainzApi.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj
index 7522c8156..f27825298 100644
--- a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj
+++ b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj
@@ -47,6 +47,8 @@
+
+
diff --git a/PlexRequests.Api.Models/Music/HeadphonesAlbumSearchResult.cs b/PlexRequests.Api.Models/Music/HeadphonesAlbumSearchResult.cs
new file mode 100644
index 000000000..8aa4684c6
--- /dev/null
+++ b/PlexRequests.Api.Models/Music/HeadphonesAlbumSearchResult.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; }
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Models/Music/HeadphonesArtistSearchResult.cs b/PlexRequests.Api.Models/Music/HeadphonesArtistSearchResult.cs
new file mode 100644
index 000000000..15c574277
--- /dev/null
+++ b/PlexRequests.Api.Models/Music/HeadphonesArtistSearchResult.cs
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Models/Music/MusicBrainzCoverArt.cs b/PlexRequests.Api.Models/Music/MusicBrainzCoverArt.cs
new file mode 100644
index 000000000..40cecba94
--- /dev/null
+++ b/PlexRequests.Api.Models/Music/MusicBrainzCoverArt.cs
@@ -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 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 images { get; set; }
+ public string release { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs b/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs
new file mode 100644
index 000000000..974703cda
--- /dev/null
+++ b/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs
@@ -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 ReleaseRvents { get; set; }
+ public string id { get; set; }
+ }
+
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs b/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs
new file mode 100644
index 000000000..658b8ca4b
--- /dev/null
+++ b/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs
@@ -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 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 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 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 { 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 ReleaseEvents { get; set; }
+ public string barcode { get; set; }
+ public string asin { get; set; }
+ [JsonProperty(PropertyName = "label-info")]
+ public List LabelInfo { get; set; }
+ [JsonProperty(PropertyName = "track-count")]
+ public int TrackCount { get; set; }
+ public List media { get; set; }
+ }
+
+ public class MusicBrainzSearchResults
+ {
+ public string created { get; set; }
+ public int count { get; set; }
+ public int offset { get; set; }
+ public List releases { get; set; }
+ }
+
+}
\ No newline at end of file
diff --git a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj
index 97cefa99c..f04835277 100644
--- a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj
+++ b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj
@@ -48,6 +48,11 @@
+
+
+
+
+
@@ -66,6 +71,7 @@
+
diff --git a/PlexRequests.Api.Models/Sonarr/SonarrAddSeries.cs b/PlexRequests.Api.Models/Sonarr/SonarrAddSeries.cs
index 534f3068f..23540521f 100644
--- a/PlexRequests.Api.Models/Sonarr/SonarrAddSeries.cs
+++ b/PlexRequests.Api.Models/Sonarr/SonarrAddSeries.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
+using Newtonsoft.Json;
+
namespace PlexRequests.Api.Models.Sonarr
{
public class Season
@@ -23,6 +25,8 @@ namespace PlexRequests.Api.Models.Sonarr
public string imdbId { get; set; }
public string titleSlug { get; set; }
public int id { get; set; }
+ [JsonIgnore]
+ public string ErrorMessage { get; set; }
}
public class AddOptions
diff --git a/PlexRequests.Api.Models/Sonarr/SonarrError.cs b/PlexRequests.Api.Models/Sonarr/SonarrError.cs
new file mode 100644
index 000000000..ae3fbdfca
--- /dev/null
+++ b/PlexRequests.Api.Models/Sonarr/SonarrError.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api/ApiRequest.cs b/PlexRequests.Api/ApiRequest.cs
index 62391386b..1b7975462 100644
--- a/PlexRequests.Api/ApiRequest.cs
+++ b/PlexRequests.Api/ApiRequest.cs
@@ -26,13 +26,9 @@
#endregion
using System;
using System.IO;
-using System.Net;
-using System.Text;
-using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using NLog;
@@ -96,20 +92,13 @@ namespace PlexRequests.Api
throw new ApplicationException(message, response.ErrorException);
}
- try
- {
- var json = JsonConvert.DeserializeObject(response.Content);
- return json;
- }
- catch (Exception e)
- {
- Log.Fatal(e);
- Log.Info(response.Content);
- throw;
- }
+
+ var json = JsonConvert.DeserializeObject(response.Content);
+
+ return json;
}
- public T DeserializeXml(string input)
+ private T DeserializeXml(string input)
where T : class
{
var ser = new XmlSerializer(typeof(T));
diff --git a/PlexRequests.Api/HeadphonesApi.cs b/PlexRequests.Api/HeadphonesApi.cs
new file mode 100644
index 000000000..ed0dac9c6
--- /dev/null
+++ b/PlexRequests.Api/HeadphonesApi.cs
@@ -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(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".
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api/MusicBrainzApi.cs b/PlexRequests.Api/MusicBrainzApi.cs
new file mode 100644
index 000000000..62b0771be
--- /dev/null
+++ b/PlexRequests.Api/MusicBrainzApi.cs
@@ -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(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(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(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".
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Api/PlexRequests.Api.csproj b/PlexRequests.Api/PlexRequests.Api.csproj
index 6422dfd6f..b655cc2be 100644
--- a/PlexRequests.Api/PlexRequests.Api.csproj
+++ b/PlexRequests.Api/PlexRequests.Api.csproj
@@ -66,6 +66,7 @@
+ TrueTrue
@@ -75,6 +76,7 @@
+
diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs
index 1d1a57f34..f9d68da31 100644
--- a/PlexRequests.Api/SonarrApi.cs
+++ b/PlexRequests.Api/SonarrApi.cs
@@ -27,9 +27,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+
+using Newtonsoft.Json;
+
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Sonarr;
+using PlexRequests.Helpers;
+
using RestSharp;
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)
{
-
+ Log.Debug("Adding series {0}", title);
+ Log.Debug("Seasons = {0}, out of {1} seasons", seasons.DumpJson(), seasonCount);
var request = new RestRequest
{
Resource = "/api/Series?",
@@ -74,7 +80,6 @@ namespace PlexRequests.Api
rootFolderPath = rootPath
};
-
for (var i = 1; i <= seasonCount; i++)
{
var season = new Season
@@ -85,12 +90,25 @@ namespace PlexRequests.Api
options.seasons.Add(season);
}
+ Log.Debug("Sonarr API Options:");
+ Log.Debug(options.DumpJson());
+
request.AddHeader("X-Api-Key", apiKey);
request.AddJsonBody(options);
- var obj = Api.ExecuteJson(request, baseUrl);
+ SonarrAddSeries result;
+ try
+ {
+ result = Api.ExecuteJson(request, baseUrl);
+ }
+ catch (JsonSerializationException jse)
+ {
+ Log.Error(jse);
+ var error = Api.ExecuteJson(request, baseUrl);
+ result = new SonarrAddSeries { ErrorMessage = error.errorMessage };
+ }
- return obj;
+ return result;
}
public SystemStatus SystemStatus(string apiKey, Uri baseUrl)
diff --git a/PlexRequests.Core/IRequestService.cs b/PlexRequests.Core/IRequestService.cs
index fb68fab37..342e83d05 100644
--- a/PlexRequests.Core/IRequestService.cs
+++ b/PlexRequests.Core/IRequestService.cs
@@ -33,7 +33,9 @@ namespace PlexRequests.Core
public interface IRequestService
{
long AddRequest(RequestedModel model);
- bool CheckRequest(int providerId);
+ RequestedModel CheckRequest(int providerId);
+ RequestedModel CheckRequest(string musicId);
+
void DeleteRequest(RequestedModel request);
bool UpdateRequest(RequestedModel model);
RequestedModel Get(int id);
diff --git a/PlexRequests.Core/JsonRequestService.cs b/PlexRequests.Core/JsonRequestService.cs
index 1504faed6..4808cde3b 100644
--- a/PlexRequests.Core/JsonRequestService.cs
+++ b/PlexRequests.Core/JsonRequestService.cs
@@ -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
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);
return result ? id : -1;
}
- public bool CheckRequest(int providerId)
+ public RequestedModel CheckRequest(int providerId)
{
var blobs = Repo.GetAll();
- return blobs.Any(x => x.ProviderId == providerId);
+ var blob = blobs.FirstOrDefault(x => x.ProviderId == providerId);
+ return blob != null ? ByteConverterHelper.ReturnObject(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(blob.Content) : null;
}
public void DeleteRequest(RequestedModel request)
@@ -79,6 +87,10 @@ namespace PlexRequests.Core
public RequestedModel Get(int id)
{
var blob = Repo.Get(id);
+ if (blob == null)
+ {
+ return new RequestedModel();
+ }
var model = ByteConverterHelper.ReturnObject(blob.Content);
return model;
}
diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj
index a1e0538f7..735938db4 100644
--- a/PlexRequests.Core/PlexRequests.Core.csproj
+++ b/PlexRequests.Core/PlexRequests.Core.csproj
@@ -46,6 +46,10 @@
..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dllTrue
+
+ ..\packages\NLog.4.2.3\lib\net45\NLog.dll
+ True
+ ..\packages\Octokit.0.19.0\lib\net45\Octokit.dllTrue
@@ -74,6 +78,7 @@
+
diff --git a/PlexRequests.Core/SettingModels/HeadphonesSettings.cs b/PlexRequests.Core/SettingModels/HeadphonesSettings.cs
new file mode 100644
index 000000000..96deeb821
--- /dev/null
+++ b/PlexRequests.Core/SettingModels/HeadphonesSettings.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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.Core/SettingModels/PlexRequestSettings.cs b/PlexRequests.Core/SettingModels/PlexRequestSettings.cs
index 51a6413fd..4b4009def 100644
--- a/PlexRequests.Core/SettingModels/PlexRequestSettings.cs
+++ b/PlexRequests.Core/SettingModels/PlexRequestSettings.cs
@@ -24,6 +24,10 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+
namespace PlexRequests.Core.SettingModels
{
public class PlexRequestSettings : Settings
@@ -32,8 +36,33 @@ namespace PlexRequests.Core.SettingModels
public bool SearchForMovies { get; set; }
public bool SearchForTvShows { get; set; }
+ public bool SearchForMusic { get; set; }
public bool RequireMovieApproval { get; set; }
public bool RequireTvShowApproval { get; set; }
+ public bool RequireMusicApproval { get; set; }
+ public bool UsersCanViewOnlyOwnRequests { get; set; }
public int WeeklyRequestLimit { get; set; }
+ public string NoApprovalUsers { get; set; }
+
+ [JsonIgnore]
+ public List ApprovalWhiteList
+ {
+ get
+ {
+ var users = new List();
+ 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;
+ }
+ }
}
}
diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs
index 4dea3a27a..69d3f9000 100644
--- a/PlexRequests.Core/Setup.cs
+++ b/PlexRequests.Core/Setup.cs
@@ -30,6 +30,7 @@ using System.Collections.Generic;
using System.Linq;
using Mono.Data.Sqlite;
+using NLog;
using PlexRequests.Api;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
@@ -40,6 +41,9 @@ namespace PlexRequests.Core
{
public class Setup
{
+ public const int SchemaVersion = 1;
+
+ private static Logger Log = LogManager.GetCurrentClassLogger();
private static DbConfiguration Db { get; set; }
public string SetupDb()
{
@@ -53,11 +57,40 @@ namespace PlexRequests.Core
}
MigrateDb();
+ CheckSchema();
return 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()
{
var defaultSettings = new PlexRequestSettings
@@ -72,8 +105,9 @@ namespace PlexRequests.Core
s.SaveSettings(defaultSettings);
}
- private void MigrateDb() // TODO: Remove when no longer needed
+ private void MigrateDb() // TODO: Remove in v1.7
{
+
var result = new List();
RequestedModel[] requestedModels;
var repo = new GenericRepository(Db, new MemoryCacheProvider());
@@ -121,7 +155,7 @@ namespace PlexRequests.Core
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);
result.Add(id);
diff --git a/PlexRequests.Core/packages.config b/PlexRequests.Core/packages.config
index ddcb2361b..6fae42bd4 100644
--- a/PlexRequests.Core/packages.config
+++ b/PlexRequests.Core/packages.config
@@ -3,6 +3,7 @@
+
\ No newline at end of file
diff --git a/PlexRequests.Helpers/DateTimeHelper.cs b/PlexRequests.Helpers/DateTimeHelper.cs
new file mode 100644
index 000000000..88103dcdf
--- /dev/null
+++ b/PlexRequests.Helpers/DateTimeHelper.cs
@@ -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);
+ }
+ }
+}
diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj
index ec3c70a8d..b2bf29a0a 100644
--- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj
+++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj
@@ -52,6 +52,7 @@
+
diff --git a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs
index 8e641181e..2fd0f4803 100644
--- a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs
+++ b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs
@@ -32,12 +32,12 @@ using Moq;
using NUnit.Framework;
using PlexRequests.Api.Interfaces;
-using PlexRequests.Api.Models;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces;
+using PlexRequests.Store;
namespace PlexRequests.Services.Tests
{
@@ -66,7 +66,7 @@ namespace PlexRequests.Services.Tests
var requestMock = new Mock();
var plexMock = new Mock();
- var searchResult = new PlexSearch {Video = new List
diff --git a/PlexRequests.Services/AvailabilityUpdateService.cs b/PlexRequests.Services/AvailabilityUpdateService.cs
index 0774307e6..19b23bbe0 100644
--- a/PlexRequests.Services/AvailabilityUpdateService.cs
+++ b/PlexRequests.Services/AvailabilityUpdateService.cs
@@ -48,9 +48,12 @@ namespace PlexRequests.Services
{
public AvailabilityUpdateService()
{
+ var memCache = new MemoryCacheProvider();
+ var dbConfig = new DbConfiguration(new SqliteFactory());
+ var repo = new SettingsJsonRepository(dbConfig, memCache);
+
ConfigurationReader = new ConfigurationReader();
- var repo = new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider());
- Checker = new PlexAvailabilityChecker(new SettingsServiceV2(repo), new SettingsServiceV2(repo), new JsonRequestService(new RequestJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())), new PlexApi());
+ Checker = new PlexAvailabilityChecker(new SettingsServiceV2(repo), new SettingsServiceV2(repo), new JsonRequestService(new RequestJsonRepository(dbConfig, memCache)), new PlexApi());
HostingEnvironment.RegisterObject(this);
}
diff --git a/PlexRequests.Services/Interfaces/INotification.cs b/PlexRequests.Services/Interfaces/INotification.cs
index 14b09f0e9..2e4e55ea4 100644
--- a/PlexRequests.Services/Interfaces/INotification.cs
+++ b/PlexRequests.Services/Interfaces/INotification.cs
@@ -27,6 +27,7 @@
using System.Threading.Tasks;
using PlexRequests.Services.Notification;
+using PlexRequests.Core.SettingModels;
namespace PlexRequests.Services.Interfaces
{
@@ -35,5 +36,7 @@ namespace PlexRequests.Services.Interfaces
string NotificationName { get; }
Task NotifyAsync(NotificationModel model);
+
+ Task NotifyAsync(NotificationModel model, Settings settings);
}
}
\ No newline at end of file
diff --git a/PlexRequests.Services/Interfaces/INotificationService.cs b/PlexRequests.Services/Interfaces/INotificationService.cs
index 59db3b509..91563c6de 100644
--- a/PlexRequests.Services/Interfaces/INotificationService.cs
+++ b/PlexRequests.Services/Interfaces/INotificationService.cs
@@ -27,12 +27,14 @@
using System.Threading.Tasks;
using PlexRequests.Services.Notification;
+using PlexRequests.Core.SettingModels;
namespace PlexRequests.Services.Interfaces
{
public interface INotificationService
{
Task Publish(NotificationModel model);
+ Task Publish(NotificationModel model, Settings settings);
void Subscribe(INotification notification);
void UnSubscribe(INotification notification);
diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs
index 472b6a069..4a359fb23 100644
--- a/PlexRequests.Services/Notification/EmailMessageNotification.cs
+++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs
@@ -46,24 +46,29 @@ namespace PlexRequests.Services.Notification
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private ISettingsService EmailNotificationSettings { get; }
- private EmailNotificationSettings Settings => GetConfiguration();
public string NotificationName => "EmailMessageNotification";
public async Task NotifyAsync(NotificationModel model)
{
var configuration = GetConfiguration();
- if (!ValidateConfiguration(configuration))
- {
- return;
- }
+ await NotifyAsync(model, configuration);
+ }
+
+ 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)
{
case NotificationType.NewRequest:
- await EmailNewRequest(model);
+ await EmailNewRequest(model, emailSettings);
break;
case NotificationType.Issue:
- await EmailIssue(model);
+ await EmailIssue(model, emailSettings);
break;
case NotificationType.RequestAvailable:
throw new NotImplementedException();
@@ -74,6 +79,10 @@ namespace PlexRequests.Services.Notification
case NotificationType.AdminNote:
throw new NotImplementedException();
+ case NotificationType.Test:
+ await EmailTest(model, emailSettings);
+ break;
+
default:
throw new ArgumentOutOfRangeException();
}
@@ -100,23 +109,23 @@ namespace PlexRequests.Services.Notification
return true;
}
- private async Task EmailNewRequest(NotificationModel model)
+ private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
{
var message = new MailMessage
{
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")}",
- From = new MailAddress(Settings.EmailSender),
+ From = new MailAddress(settings.EmailSender),
Subject = $"Plex Requests: New request for {model.Title}!"
};
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;
+ smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
+ smtp.EnableSsl = settings.Ssl;
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
{
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}!",
- From = new MailAddress(Settings.RecipientEmail),
+ From = new MailAddress(settings.RecipientEmail),
Subject = $"Plex Requests: New issue for {model.Title}!"
};
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.EnableSsl = Settings.Ssl;
+ smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
+ smtp.EnableSsl = settings.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false);
}
}
diff --git a/PlexRequests.Services/Notification/NotificationService.cs b/PlexRequests.Services/Notification/NotificationService.cs
index 116f5aef9..35e52fd7d 100644
--- a/PlexRequests.Services/Notification/NotificationService.cs
+++ b/PlexRequests.Services/Notification/NotificationService.cs
@@ -32,6 +32,7 @@ using System.Threading.Tasks;
using NLog;
using PlexRequests.Services.Interfaces;
+using PlexRequests.Core.SettingModels;
namespace PlexRequests.Services.Notification
{
@@ -47,6 +48,13 @@ namespace PlexRequests.Services.Notification
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)
{
Observers.TryAdd(notification.NotificationName, notification);
@@ -67,6 +75,19 @@ namespace PlexRequests.Services.Notification
{
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");
+ }
}
}
}
\ No newline at end of file
diff --git a/PlexRequests.Services/Notification/NotificationType.cs b/PlexRequests.Services/Notification/NotificationType.cs
index bf919fe39..22d0d29b1 100644
--- a/PlexRequests.Services/Notification/NotificationType.cs
+++ b/PlexRequests.Services/Notification/NotificationType.cs
@@ -33,5 +33,6 @@ namespace PlexRequests.Services.Notification
RequestAvailable,
RequestApproved,
AdminNote,
+ Test
}
}
diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs
index 4e2f02144..521855dca 100644
--- a/PlexRequests.Services/Notification/PushbulletNotification.cs
+++ b/PlexRequests.Services/Notification/PushbulletNotification.cs
@@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification
public string NotificationName => "PushbulletNotification";
public async Task NotifyAsync(NotificationModel model)
{
- if (!ValidateConfiguration())
- {
- return;
- }
+ var configuration = GetSettings();
+ await NotifyAsync(model, configuration);
+ }
+
+ 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)
{
case NotificationType.NewRequest:
- await PushNewRequestAsync(model);
+ await PushNewRequestAsync(model, pushSettings);
break;
case NotificationType.Issue:
- await PushIssueAsync(model);
+ await PushIssueAsync(model, pushSettings);
break;
case NotificationType.RequestAvailable:
break;
@@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification
break;
case NotificationType.AdminNote:
break;
+ case NotificationType.Test:
+ await PushTestAsync(model, pushSettings);
+ break;
default:
throw new ArgumentOutOfRangeException();
}
}
- private bool ValidateConfiguration()
+ private bool ValidateConfiguration(PushbulletNotificationSettings settings)
{
- if (!Settings.Enabled)
+ if (!settings.Enabled)
{
return false;
}
- if (string.IsNullOrEmpty(Settings.AccessToken))
+ if (string.IsNullOrEmpty(settings.AccessToken))
{
return false;
}
@@ -93,13 +103,13 @@ namespace PlexRequests.Services.Notification
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 pushTitle = $"Plex Requests: {model.Title} has been requested!";
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");
@@ -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 pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}";
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)
{
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
diff --git a/PlexRequests.Services/Notification/PushoverNotification.cs b/PlexRequests.Services/Notification/PushoverNotification.cs
index bca0b2c90..47854b1d5 100644
--- a/PlexRequests.Services/Notification/PushoverNotification.cs
+++ b/PlexRequests.Services/Notification/PushoverNotification.cs
@@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification
public string NotificationName => "PushoverNotification";
public async Task NotifyAsync(NotificationModel model)
{
- if (!ValidateConfiguration())
- {
- return;
- }
+ var configuration = GetSettings();
+ await NotifyAsync(model, configuration);
+ }
+
+ 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)
{
case NotificationType.NewRequest:
- await PushNewRequestAsync(model);
+ await PushNewRequestAsync(model, pushSettings);
break;
case NotificationType.Issue:
- await PushIssueAsync(model);
+ await PushIssueAsync(model, pushSettings);
break;
case NotificationType.RequestAvailable:
break;
@@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification
break;
case NotificationType.AdminNote:
break;
+ case NotificationType.Test:
+ await PushTestAsync(model, pushSettings);
+ break;
default:
throw new ArgumentOutOfRangeException();
}
}
- private bool ValidateConfiguration()
+ private bool ValidateConfiguration(PushoverNotificationSettings settings)
{
- if (!Settings.Enabled)
+ if (!settings.Enabled)
{
return false;
}
- if (string.IsNullOrEmpty(Settings.AccessToken) || string.IsNullOrEmpty(Settings.UserToken))
+ if (string.IsNullOrEmpty(settings.AccessToken) || string.IsNullOrEmpty(settings.UserToken))
{
return false;
}
@@ -93,12 +103,12 @@ namespace PlexRequests.Services.Notification
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}";
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");
@@ -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}";
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)
{
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
diff --git a/PlexRequests.Services/PlexAvailabilityChecker.cs b/PlexRequests.Services/PlexAvailabilityChecker.cs
index bc1cfb8e7..a71b75251 100644
--- a/PlexRequests.Services/PlexAvailabilityChecker.cs
+++ b/PlexRequests.Services/PlexAvailabilityChecker.cs
@@ -24,14 +24,17 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
+using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using PlexRequests.Api.Interfaces;
+using PlexRequests.Api.Models.Plex;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
+using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
@@ -52,33 +55,77 @@ namespace PlexRequests.Services
private ISettingsService Auth { get; }
private IRequestService RequestService { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
- private IPlexApi PlexApi { get; set; }
+ private IPlexApi PlexApi { get; }
public void CheckAndUpdateAll(long check)
{
+ Log.Trace("This is check no. {0}", check);
+ Log.Trace("Getting the settings");
var plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings();
+ Log.Trace("Getting all the requests");
var requests = RequestService.GetAll();
- var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
- if (!ValidateSettings(plexSettings, authSettings, requestedModels))
+ var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray();
+ 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;
}
var modifiedModel = new List();
foreach (var r in requestedModels)
{
- var results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri);
- var result = results.Video.FirstOrDefault(x => x.Title == r.Title);
- var originalRequest = RequestService.Get(r.Id);
+ Log.Trace("We are going to see if Plex has the following title: {0}", r.Title);
+ PlexSearch results;
+ 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;
- modifiedModel.Add(originalRequest);
+ var directoryResultVal = directoryResult ?? false;
+
+ 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);}
}
///
@@ -90,45 +137,31 @@ namespace PlexRequests.Services
/// The settings are not configured for Plex or Authentication
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 authSettings = Auth.GetSettings();
if (!ValidateSettings(plexSettings, authSettings))
{
+ Log.Warn("The settings are not configured");
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))
{
- var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
- var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title) && x.Year == year);
- var directoryTitle = results.Directory?.Title == title && results.Directory?.Year == year;
+ var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase) && x.Year == year);
+ var directoryTitle = string.Equals(results.Directory?.Title, title, StringComparison.CurrentCultureIgnoreCase) && results.Directory?.Year == year;
return result?.Title != null || directoryTitle;
}
else
{
- var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
- var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title));
- var directoryTitle = results.Directory?.Title == title;
+ var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase));
+ var directoryTitle = string.Equals(results.Directory?.Title, title, StringComparison.CurrentCultureIgnoreCase);
return result?.Title != null || directoryTitle;
}
}
- private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth, IEnumerable 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)
{
if (plex?.Ip == null || auth?.PlexAuthToken == null)
diff --git a/PlexRequests.Services/UpdateInterval.cs b/PlexRequests.Services/UpdateInterval.cs
index 876b9e379..92045563a 100644
--- a/PlexRequests.Services/UpdateInterval.cs
+++ b/PlexRequests.Services/UpdateInterval.cs
@@ -32,7 +32,7 @@ namespace PlexRequests.Services
{
public class UpdateInterval : IIntervals
{
- public TimeSpan Notification => TimeSpan.FromMinutes(5);
+ public TimeSpan Notification => TimeSpan.FromMinutes(10);
}
}
\ No newline at end of file
diff --git a/PlexRequests.Store/DbConfiguration.cs b/PlexRequests.Store/DbConfiguration.cs
index 7b6c6c244..7ddd60483 100644
--- a/PlexRequests.Store/DbConfiguration.cs
+++ b/PlexRequests.Store/DbConfiguration.cs
@@ -27,12 +27,11 @@
using System;
using System.Data;
using System.IO;
+using System.Windows.Forms;
using Mono.Data.Sqlite;
using NLog;
-using PlexRequests.Helpers;
-using PlexRequests.Store.Repository;
namespace PlexRequests.Store
{
@@ -44,12 +43,14 @@ namespace PlexRequests.Store
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()
{
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");
CreateDatabase();
@@ -59,7 +60,7 @@ namespace PlexRequests.Store
}
public string DbFile = "PlexRequests.sqlite";
-
+
///
/// Gets the database connection.
///
@@ -72,7 +73,7 @@ namespace PlexRequests.Store
{
throw new SqliteException("Factory returned null");
}
- fact.ConnectionString = "Data Source=" + DbFile;
+ fact.ConnectionString = "Data Source=" + CurrentPath;
return fact;
}
@@ -83,14 +84,16 @@ namespace PlexRequests.Store
{
try
{
- using (File.Create(DbFile))
+ using (File.Create(CurrentPath))
{
}
}
catch (Exception e)
{
- Console.WriteLine(e.Message);
+ Log.Error(e);
}
}
+
+
}
}
diff --git a/PlexRequests.Store/Models/RequestBlobs.cs b/PlexRequests.Store/Models/RequestBlobs.cs
index 3b1127b6a..f9af75a25 100644
--- a/PlexRequests.Store/Models/RequestBlobs.cs
+++ b/PlexRequests.Store/Models/RequestBlobs.cs
@@ -34,5 +34,6 @@ namespace PlexRequests.Store.Models
public int ProviderId { get; set; }
public byte[] Content { get; set; }
public RequestType Type { get; set; }
+ public string MusicId { get; set; }
}
}
\ No newline at end of file
diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj
index 2175ebc06..a896a0ee6 100644
--- a/PlexRequests.Store/PlexRequests.Store.csproj
+++ b/PlexRequests.Store/PlexRequests.Store.csproj
@@ -52,6 +52,7 @@
+
diff --git a/PlexRequests.Store/Repository/RequestJsonRepository.cs b/PlexRequests.Store/Repository/RequestJsonRepository.cs
index d02a111b0..872e07745 100644
--- a/PlexRequests.Store/Repository/RequestJsonRepository.cs
+++ b/PlexRequests.Store/Repository/RequestJsonRepository.cs
@@ -37,13 +37,11 @@ namespace PlexRequests.Store.Repository
public class RequestJsonRepository : IRequestRepository
{
private ICacheProvider Cache { get; }
-
- private string TypeName { get; }
+
public RequestJsonRepository(ISqliteConfiguration config, ICacheProvider cacheProvider)
{
Db = config;
Cache = cacheProvider;
- TypeName = typeof(RequestJsonRepository).Name;
}
private ISqliteConfiguration Db { get; }
@@ -60,7 +58,7 @@ namespace PlexRequests.Store.Repository
public IEnumerable GetAll()
{
- var key = TypeName + "GetAll";
+ var key = "GetAll";
var item = Cache.GetOrSet(key, () =>
{
using (var con = Db.DbConnection())
@@ -74,7 +72,7 @@ namespace PlexRequests.Store.Repository
public RequestBlobs Get(int id)
{
- var key = TypeName + "Get" + id;
+ var key = "Get" + id;
var item = Cache.GetOrSet(key, () =>
{
using (var con = Db.DbConnection())
@@ -107,7 +105,7 @@ namespace PlexRequests.Store.Repository
private void ResetCache()
{
Cache.Remove("Get");
- Cache.Remove(TypeName + "GetAll");
+ Cache.Remove("GetAll");
}
public bool UpdateAll(IEnumerable entity)
diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs
index 18ef216af..55fc3abac 100644
--- a/PlexRequests.Store/RequestedModel.cs
+++ b/PlexRequests.Store/RequestedModel.cs
@@ -1,13 +1,19 @@
using System;
-using System.Security.Cryptography;
-
using Dapper.Contrib.Extensions;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
namespace PlexRequests.Store
{
[Table("Requested")]
public class RequestedModel : Entity
{
+ public RequestedModel()
+ {
+ RequestedUsers = new List();
+ }
+
// ReSharper disable once IdentifierTypo
public int ProviderId { get; set; }
public string ImdbId { get; set; }
@@ -18,7 +24,10 @@ namespace PlexRequests.Store
public RequestType Type { get; set; }
public string Status { get; set; }
public bool Approved { get; set; }
+
+ [Obsolete("Use RequestedUsers")]
public string RequestedBy { get; set; }
+
public DateTime RequestedDate { get; set; }
public bool Available { get; set; }
public IssueState Issues { get; set; }
@@ -27,12 +36,48 @@ namespace PlexRequests.Store
public int[] SeasonList { get; set; }
public int SeasonCount { get; set; }
public string SeasonsRequested { get; set; }
+ public string MusicBrainzId { get; set; }
+ public List RequestedUsers { get; set; }
+
+ [JsonIgnore]
+ public List AllUsers
+ {
+ get
+ {
+ var u = new List();
+ 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
{
Movie,
- TvShow
+ TvShow,
+ Album
}
public enum IssueState
diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql
index f23b3c5fc..7392c4efc 100644
--- a/PlexRequests.Store/SqlTables.sql
+++ b/PlexRequests.Store/SqlTables.sql
@@ -25,7 +25,8 @@ CREATE TABLE IF NOT EXISTS RequestBlobs
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ProviderId 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);
@@ -40,3 +41,9 @@ CREATE TABLE IF NOT EXISTS Logs
Exception varchar(100) NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS Logs_Id ON Logs (Id);
+
+CREATE TABLE IF NOT EXISTS DBInfo
+(
+ SchemaVersion INTEGER
+
+);
\ No newline at end of file
diff --git a/PlexRequests.Store/TableCreation.cs b/PlexRequests.Store/TableCreation.cs
index 437105ffc..6b0e07044 100644
--- a/PlexRequests.Store/TableCreation.cs
+++ b/PlexRequests.Store/TableCreation.cs
@@ -25,7 +25,7 @@
// ***********************************************************************
#endregion
using System.Data;
-
+using System.Linq;
using Dapper;
using Dapper.Contrib.Extensions;
@@ -44,6 +44,57 @@ namespace PlexRequests.Store
connection.Close();
}
+ public static void AlterTable(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
+ {
+ connection.Open();
+ var result = connection.Query($"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("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")]
public class SqliteMasterTable
{
@@ -54,5 +105,17 @@ namespace PlexRequests.Store
public long rootpage { 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; }
+ }
+
+
}
}
diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs
index fc8686086..34b2af4a7 100644
--- a/PlexRequests.UI.Tests/AdminModuleTests.cs
+++ b/PlexRequests.UI.Tests/AdminModuleTests.cs
@@ -59,6 +59,7 @@ namespace PlexRequests.UI.Tests
private Mock> EmailMock { get; set; }
private Mock> PushbulletSettings { get; set; }
private Mock> PushoverSettings { get; set; }
+ private Mock> HeadphonesSettings { get; set; }
private Mock PlexMock { get; set; }
private Mock SonarrApiMock { get; set; }
private Mock PushbulletApi { get; set; }
@@ -94,6 +95,7 @@ namespace PlexRequests.UI.Tests
PushoverSettings = new Mock>();
PushoverApi = new Mock();
NotificationService = new Mock();
+ HeadphonesSettings = new Mock>();
Bootstrapper = new ConfigurableBootstrapper(with =>
{
@@ -114,6 +116,7 @@ namespace PlexRequests.UI.Tests
with.Dependency(PushoverSettings.Object);
with.Dependency(PushoverApi.Object);
with.Dependency(NotificationService.Object);
+ with.Dependency(HeadphonesSettings.Object);
with.RootPathProvider();
with.RequestStartup((container, pipelines, context) =>
{
diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs
index 5e4f24343..a4e13f18b 100644
--- a/PlexRequests.UI/Bootstrapper.cs
+++ b/PlexRequests.UI/Bootstrapper.cs
@@ -76,9 +76,9 @@ namespace PlexRequests.UI
container.Register, SettingsServiceV2>();
container.Register, SettingsServiceV2>();
container.Register, SettingsServiceV2>();
+ container.Register, SettingsServiceV2>();
// Repo's
- container.Register, GenericRepository>();
container.Register, GenericRepository>();
container.Register();
container.Register();
@@ -95,19 +95,21 @@ namespace PlexRequests.UI
container.Register();
container.Register();
container.Register();
+ container.Register();
+ container.Register();
// NotificationService
container.Register().AsSingleton();
SubscribeAllObservers(container);
base.ConfigureRequestContainer(container, context);
- }
- protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
- {
TaskManager.TaskFactory = new PlexTaskFactory();
TaskManager.Initialize(new PlexRegistry());
+ }
+ protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
+ {
CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default);
StaticConfiguration.DisableErrorTraces = false;
@@ -123,11 +125,12 @@ namespace PlexRequests.UI
FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
}
-
+
protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" };
diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css
index a517cc508..66153a87a 100644
--- a/PlexRequests.UI/Content/custom.css
+++ b/PlexRequests.UI/Content/custom.css
@@ -22,7 +22,9 @@
.form-control-custom {
background-color: #4e5d6c !important;
- color: white !important; }
+ color: white !important;
+ border-radius: 0;
+ box-shadow: 0 0 0 !important; }
h1 {
font-size: 3.5rem !important;
@@ -40,6 +42,22 @@ label {
margin-bottom: 0.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;
@@ -126,3 +144,68 @@ label {
#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: #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; }
+
diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css
index 45dc19440..9f60b69e2 100644
--- a/PlexRequests.UI/Content/custom.min.css
+++ b/PlexRequests.UI/Content/custom.min.css
@@ -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;}
\ No newline at end of file
+@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;}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss
index df29fcfc8..4c42a8dfc 100644
--- a/PlexRequests.UI/Content/custom.scss
+++ b/PlexRequests.UI/Content/custom.scss
@@ -1,11 +1,14 @@
$form-color: #4e5d6c;
+$form-color-lighter: #637689;
$primary-colour: #df691a;
$primary-colour-outline: #ff761b;
$info-colour: #5bc0de;
$warning-colour: #f0ad4e;
$danger-colour: #d9534f;
$success-colour: #5cb85c;
-$i:!important;
+$i:
+!important
+;
@media (min-width: 768px ) {
.row {
@@ -42,6 +45,8 @@ $i:!important;
.form-control-custom {
background-color: $form-color $i;
color: white $i;
+ border-radius: 0;
+ box-shadow: 0 0 0 !important;
}
@@ -65,6 +70,25 @@ label {
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 {
color: $danger-colour $i;
background-color: transparent;
@@ -156,9 +180,89 @@ label {
border-color: $success-colour $i;
}
-#movieList .mix{
- display: none;
+#movieList .mix {
+ 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;
+}
+
+.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;
}
-#tvList .mix{
- display: none;
-}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js
index 3872da2c2..611d89fba 100644
--- a/PlexRequests.UI/Content/requests.js
+++ b/PlexRequests.UI/Content/requests.js
@@ -6,72 +6,129 @@
});
var searchSource = $("#search-template").html();
+var albumSource = $("#album-template").html();
var searchTemplate = Handlebars.compile(searchSource);
+var albumTemplate = Handlebars.compile(albumSource);
var movieTimer = 0;
var tvimer = 0;
-movieLoad();
-tvLoad();
+var mixItUpDefault = {
+ 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) {
var target = $(e.target).attr('href');
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 ($('#tvList').mixItUp('isLoaded')) {
- activeState = $('#tvList').mixItUp('getState');
- $('#tvList').mixItUp('destroy');
- }
- if (!$('#movieList').mixItUp('isLoaded')) {
- $('#movieList').mixItUp({
- load: {
- filter: activeState.activeFilter || 'all',
- sort: activeState.activeSort || 'default:asc'
- },
- layout: {
- display: 'block'
- }
- });
+ $('#approveMovies').show();
+ if ($tvl.mixItUp('isLoaded')) {
+ activeState = $tvl.mixItUp('getState');
+ $tvl.mixItUp('destroy');
}
+ if ($ml.mixItUp('isLoaded')) $ml.mixItUp('destroy');
+ $ml.mixItUp(mixItUpConfig(activeState)); // init or reinit
}
+ //$('.mix[data-bound]').removeAttr('data-bound');
});
// 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({
+ type: 'post',
+ url: '/approval/approveallmovies',
+ dataType: "json",
+ success: function (response) {
+ if (checkJsonResponse(response)) {
+ generateNotify("Success! All Movie requests approved!", "success");
+ movieLoad();
+ }
+ },
+ error: function (e) {
+ console.log(e);
+ 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/approveall',
+ url: '/approval/approvealltvshows',
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
- generateNotify("Success! All requests approved!", "success");
+ 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
$(document).on("click", ".dropdownIssue", function (e) {
@@ -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() {
- $("#movieList").html("");
+ var $ml = $('#movieList');
+ if ($ml.mixItUp('isLoaded')) {
+ activeState = $ml.mixItUp('getState');
+ $ml.mixItUp('destroy');
+ }
+ $ml.html("");
$.ajax("/requests/movies/").success(function (results) {
- results.forEach(function (result) {
- var context = buildRequestContext(result, "movie");
-
- var html = searchTemplate(context);
- $("#movieList").append(html);
- });
- $('#movieList').mixItUp({
- layout: {
- display: 'block'
- },
- load: {
- filter: 'all'
- }
- });
+ if (results.length > 0) {
+ results.forEach(function (result) {
+ var context = buildRequestContext(result, "movie");
+ var html = searchTemplate(context);
+ $ml.append(html);
+ });
+ }
+ else {
+ $ml.html(noResultsHtml.format("movie"));
+ }
+ $ml.mixItUp(mixItUpConfig());
});
};
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) {
- results.forEach(function (result) {
- var context = buildRequestContext(result, "tv");
- var html = searchTemplate(context);
- $("#tvList").append(html);
- });
+ if (results.length > 0) {
+ results.forEach(function (result) {
+ var context = buildRequestContext(result, "tv");
+ 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,
status: result.status,
releaseDate: result.releaseDate,
+ releaseDateTicks: result.releaseDateTicks,
approved: result.approved,
- requestedBy: result.requestedBy,
+ requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '',
requestedDate: result.requestedDate,
+ requestedDateTicks: result.requestedDateTicks,
available: result.available,
admin: result.admin,
issues: result.issues,
@@ -369,20 +481,11 @@ function buildRequestContext(result, type) {
requestId: result.id,
adminNote: result.adminNotes,
imdb: result.imdbId,
- seriesRequested: result.tvSeriesRequestType
+ seriesRequested: result.tvSeriesRequestType,
+ coverArtUrl: result.coverArtUrl,
+
};
return context;
}
-function startFilter(elementId) {
- $('#'+element).mixItUp({
- load: {
- filter: activeState.activeFilter || 'all',
- sort: activeState.activeSort || 'default:asc'
- },
- layout: {
- display: 'block'
- }
- });
-}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js
index cea4eca66..1c2daa06d 100644
--- a/PlexRequests.UI/Content/search.js
+++ b/PlexRequests.UI/Content/search.js
@@ -6,27 +6,39 @@
});
var searchSource = $("#search-template").html();
+var musicSource = $("#music-template").html();
var searchTemplate = Handlebars.compile(searchSource);
-var movieTimer = 0;
-var tvimer = 0;
+var musicTemplate = Handlebars.compile(musicSource);
+
+var searchTimer = 0;
// Type in movie search
$("#movieSearchContent").on("input", function () {
- if (movieTimer) {
- clearTimeout(movieTimer);
+ if (searchTimer) {
+ clearTimeout(searchTimer);
}
$('#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
$("#tvSearchContent").on("input", function () {
- if (tvimer) {
- clearTimeout(tvimer);
+ if (searchTimer) {
+ clearTimeout(searchTimer);
}
$('#tvSearchButton').attr("class", "fa fa-spinner fa-spin");
- tvimer = setTimeout(tvSearch, 400);
+ searchTimer = setTimeout(tvSearch, 400);
});
// Click TV dropdown option
@@ -60,6 +72,16 @@ $(document).on("click", ".dropdownTv", function (e) {
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
$(document).on("click", ".requestMovie", function (e) {
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) {
$.ajax({
type: type,
@@ -112,10 +156,23 @@ function sendRequestAjax(data, type, url, buttonId) {
}
function movieSearch() {
- $("#movieList").html("");
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) {
results.forEach(function(result) {
var context = buildMovieContext(result);
@@ -124,15 +181,22 @@ function movieSearch() {
$("#movieList").append(html);
});
}
+ else {
+ $("#movieList").html(noResultsHtml);
+ }
$('#movieSearchButton').attr("class","fa fa-search");
});
};
function tvSearch() {
- $("#tvList").html("");
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) {
results.forEach(function(result) {
var context = buildTvShowContext(result);
@@ -140,10 +204,36 @@ function tvSearch() {
$("#tvList").append(html);
});
}
+ else {
+ $("#tvList").html(noResultsHtml);
+ }
$('#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) {
var date = new Date(result.releaseDate);
@@ -177,3 +267,21 @@ function buildTvShowContext(result) {
};
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;
+}
diff --git a/PlexRequests.UI/Content/site.js b/PlexRequests.UI/Content/site.js
index f05ca73d6..72ec831fa 100644
--- a/PlexRequests.UI/Content/site.js
+++ b/PlexRequests.UI/Content/site.js
@@ -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
$.notify({
// options
@@ -34,4 +44,9 @@ function finishLoading(elementId, originalCss, html) {
$('#' + elementId).removeClass("btn-primary-outline");
$('#' + elementId).addClass("btn-" + originalCss + "-outline");
$('#' + elementId).html(html);
-}
\ No newline at end of file
+}
+
+var noResultsHtml = "
" +
+ "
Sorry, we didn't find any results!
";
+var noResultsMusic = "
" +
+ "
Sorry, we didn't find any results!
";
\ No newline at end of file
diff --git a/PlexRequests.UI/Helpers/TvSender.cs b/PlexRequests.UI/Helpers/TvSender.cs
index d26611321..703c813fd 100644
--- a/PlexRequests.UI/Helpers/TvSender.cs
+++ b/PlexRequests.UI/Helpers/TvSender.cs
@@ -24,17 +24,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
-
-using Nancy;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage;
using PlexRequests.Api.Models.Sonarr;
-using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Store;
-using PlexRequests.UI.Models;
namespace PlexRequests.UI.Helpers
{
diff --git a/PlexRequests.UI/Jobs/PlexTaskFactory.cs b/PlexRequests.UI/Jobs/PlexTaskFactory.cs
index e2e51e637..5f5b88b89 100644
--- a/PlexRequests.UI/Jobs/PlexTaskFactory.cs
+++ b/PlexRequests.UI/Jobs/PlexTaskFactory.cs
@@ -12,12 +12,12 @@ namespace PlexRequests.UI.Jobs
//typeof(AvailabilityUpdateService);
var container = TinyIoCContainer.Current;
- var a= container.ResolveAll(typeof(T));
+ var a= container.Resolve(typeof(T));
object outT;
container.TryResolve(typeof(T), out outT);
- return (T)outT;
+ return (T)a;
}
}
}
\ No newline at end of file
diff --git a/PlexRequests.UI/Models/RequestViewModel.cs b/PlexRequests.UI/Models/RequestViewModel.cs
index 011b9977d..575681007 100644
--- a/PlexRequests.UI/Models/RequestViewModel.cs
+++ b/PlexRequests.UI/Models/RequestViewModel.cs
@@ -37,11 +37,13 @@ namespace PlexRequests.UI.Models
public string Title { get; set; }
public string PosterPath { get; set; }
public string ReleaseDate { get; set; }
+ public long ReleaseDateTicks { get; set; }
public RequestType Type { get; set; }
public string Status { get; set; }
public bool Approved { get; set; }
- public string RequestedBy { get; set; }
+ public string[] RequestedUsers { get; set; }
public string RequestedDate { get; set; }
+ public long RequestedDateTicks { get; set; }
public string ReleaseYear { get; set; }
public bool Available { get; set; }
public bool Admin { get; set; }
@@ -49,5 +51,6 @@ namespace PlexRequests.UI.Models
public string OtherMessage { get; set; }
public string AdminNotes { get; set; }
public string TvSeriesRequestType { get; set; }
+ public string MusicBrainzId { get; set; }
}
}
diff --git a/PlexRequests.UI/Models/SearchMusicViewModel.cs b/PlexRequests.UI/Models/SearchMusicViewModel.cs
new file mode 100644
index 000000000..94d3e6d1e
--- /dev/null
+++ b/PlexRequests.UI/Models/SearchMusicViewModel.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/PlexRequests.UI/Models/SessionKeys.cs b/PlexRequests.UI/Models/SessionKeys.cs
index 66c766039..949441650 100644
--- a/PlexRequests.UI/Models/SessionKeys.cs
+++ b/PlexRequests.UI/Models/SessionKeys.cs
@@ -29,5 +29,6 @@ namespace PlexRequests.UI.Models
public class SessionKeys
{
public const string UsernameKey = "Username";
+ public const string ClientDateTimeOffsetKey = "ClientDateTimeOffset";
}
}
diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs
index e8cd8961f..41716ba2f 100644
--- a/PlexRequests.UI/Modules/AdminModule.cs
+++ b/PlexRequests.UI/Modules/AdminModule.cs
@@ -28,6 +28,8 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
+using System.Web.UI.HtmlControls;
+
using Humanizer;
using MarkdownSharp;
@@ -51,12 +53,13 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
+using System;
namespace PlexRequests.UI.Modules
{
public class AdminModule : NancyModule
{
- private ISettingsService RpService { get; }
+ private ISettingsService PrService { get; }
private ISettingsService CpService { get; }
private ISettingsService AuthService { get; }
private ISettingsService PlexService { get; }
@@ -65,6 +68,7 @@ namespace PlexRequests.UI.Modules
private ISettingsService EmailService { get; }
private ISettingsService PushbulletService { get; }
private ISettingsService PushoverService { get; }
+ private ISettingsService HeadphonesService { get; }
private IPlexApi PlexApi { get; }
private ISonarrApi SonarrApi { get; }
private IPushbulletApi PushbulletApi { get; }
@@ -74,7 +78,7 @@ namespace PlexRequests.UI.Modules
private INotificationService NotificationService { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
- public AdminModule(ISettingsService rpService,
+ public AdminModule(ISettingsService prService,
ISettingsService cpService,
ISettingsService auth,
ISettingsService plex,
@@ -89,9 +93,10 @@ namespace PlexRequests.UI.Modules
ISettingsService pushoverSettings,
IPushoverApi pushoverApi,
IRepository logsRepo,
- INotificationService notify) : base("admin")
+ INotificationService notify,
+ ISettingsService headphones) : base("admin")
{
- RpService = rpService;
+ PrService = prService;
CpService = cpService;
AuthService = auth;
PlexService = plex;
@@ -107,6 +112,7 @@ namespace PlexRequests.UI.Modules
PushoverService = pushoverSettings;
PushoverApi = pushoverApi;
NotificationService = notify;
+ HeadphonesService = headphones;
#if !DEBUG
this.RequiresAuthentication();
@@ -139,18 +145,24 @@ namespace PlexRequests.UI.Modules
Get["/emailnotification"] = _ => EmailNotifications();
Post["/emailnotification"] = _ => SaveEmailNotifications();
+ Post["/testemailnotification"] = _ => TestEmailNotifications();
Get["/status"] = _ => Status();
Get["/pushbulletnotification"] = _ => PushbulletNotifications();
Post["/pushbulletnotification"] = _ => SavePushbulletNotifications();
+ Post["/testpushbulletnotification"] = _ => TestPushbulletNotifications();
Get["/pushovernotification"] = _ => PushoverNotifications();
Post["/pushovernotification"] = _ => SavePushoverNotifications();
+ Post["/testpushovernotification"] = _ => TestPushoverNotifications();
Get["/logs"] = _ => Logs();
Get["/loglevel"] = _ => GetLogLevels();
Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level);
Get["/loadlogs"] = _ => LoadLogs();
+
+ Get["/headphones"] = _ => Headphones();
+ Post["/headphones"] = _ => SaveHeadphones();
}
private Negotiator Authentication()
@@ -174,7 +186,7 @@ namespace PlexRequests.UI.Modules
private Negotiator Admin()
{
- var settings = RpService.GetSettings();
+ var settings = PrService.GetSettings();
Log.Trace("Getting Settings:");
Log.Trace(settings.DumpJson());
@@ -185,7 +197,7 @@ namespace PlexRequests.UI.Modules
{
var model = this.Bind();
- RpService.SaveSettings(model);
+ PrService.SaveSettings(model);
return Context.GetRedirect("~/admin");
@@ -372,6 +384,37 @@ namespace PlexRequests.UI.Modules
return View["EmailNotifications", settings];
}
+ private Response TestEmailNotifications()
+ {
+ var settings = this.Bind();
+ 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()
{
var settings = this.Bind();
@@ -440,6 +483,37 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
+ private Response TestPushbulletNotifications()
+ {
+ var settings = this.Bind();
+ 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()
{
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." });
}
+ private Response TestPushoverNotifications()
+ {
+ var settings = this.Bind();
+ 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()
{
var settings = this.Bind();
@@ -509,5 +614,32 @@ namespace PlexRequests.UI.Modules
LoggingHelper.ReconfigureLogLevel(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();
+
+ 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." });
+ }
}
}
\ No newline at end of file
diff --git a/PlexRequests.UI/Modules/ApplicationTesterModule.cs b/PlexRequests.UI/Modules/ApplicationTesterModule.cs
index 98d2bd591..98884d2f1 100644
--- a/PlexRequests.UI/Modules/ApplicationTesterModule.cs
+++ b/PlexRequests.UI/Modules/ApplicationTesterModule.cs
@@ -57,6 +57,7 @@ namespace PlexRequests.UI.Modules
Post["/sonarr"] = _ => SonarrTest();
Post["/plex"] = _ => PlexTest();
Post["/sickrage"] = _ => SickRageTest();
+ Post["/headphones"] = _ => HeadphonesTest();
}
@@ -168,5 +169,10 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = message });
}
}
+
+ private Response HeadphonesTest()
+ {
+ throw new NotImplementedException(); //TODO
+ }
}
}
\ No newline at end of file
diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs
index b2c6217bb..64d861c96 100644
--- a/PlexRequests.UI/Modules/ApprovalModule.cs
+++ b/PlexRequests.UI/Modules/ApprovalModule.cs
@@ -61,6 +61,8 @@ namespace PlexRequests.UI.Modules
Post["/approve"] = parameters => Approve((int)Request.Form.requestid);
Post["/approveall"] = x => ApproveAll();
+ Post["/approveallmovies"] = x => ApproveAllMovies();
+ Post["/approvealltvshows"] = x => ApproveAllTVShows();
}
private IRequestService Service { get; }
@@ -131,7 +133,7 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel
{
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!" });
+ }
+ }
+
///
/// Approves all.
///
@@ -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." });
}
- var requests = Service.GetAll().Where(x => x.Approved == false);
+ var requests = Service.GetAll().Where(x => x.CanApprove);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any())
{
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();
foreach (var r in requestedModels)
{
if (r.Type == RequestType.Movie)
{
- var result = SendMovie(cpSettings, r, CpApi);
- if (result)
+ var res = SendMovie(cpSettings, r, CpApi);
+ if (res)
{
r.Approved = true;
updatedRequests.Add(r);
@@ -260,8 +324,8 @@ namespace PlexRequests.UI.Modules
var sonarr = SonarrSettings.GetSettings();
if (sr.Enabled)
{
- var result = sender.SendToSickRage(sr, r);
- if (result?.result == "success")
+ var res = sender.SendToSickRage(sr, r);
+ if (res?.result == "success")
{
r.Approved = true;
updatedRequests.Add(r);
@@ -269,14 +333,14 @@ namespace PlexRequests.UI.Modules
else
{
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)
{
- var result = sender.SendToSonarr(sonarr, r);
- if (result != null)
+ var res = sender.SendToSonarr(sonarr, r);
+ if (!string.IsNullOrEmpty(res?.title))
{
r.Approved = true;
updatedRequests.Add(r);
@@ -284,6 +348,7 @@ namespace PlexRequests.UI.Modules
else
{
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
{
- 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 = false, Message = "We could not approve all of the requests. Please try again or check the logs." });
-
- }
+ }
catch (Exception e)
{
Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
}
-
}
private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp)
diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs
index 67dc1b553..a75766775 100644
--- a/PlexRequests.UI/Modules/BaseModule.cs
+++ b/PlexRequests.UI/Modules/BaseModule.cs
@@ -28,21 +28,50 @@
using Nancy;
using Nancy.Extensions;
using PlexRequests.UI.Models;
+using System;
namespace PlexRequests.UI.Modules
{
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()
{
- Before += (ctx)=> CheckAuth();
+ Before += (ctx) => CheckAuth();
}
public BaseModule(string modulePath) : base(modulePath)
{
Before += (ctx) => CheckAuth();
}
-
+
private Response CheckAuth()
{
diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs
index 578b9cf7f..6750b2c4c 100644
--- a/PlexRequests.UI/Modules/LoginModule.cs
+++ b/PlexRequests.UI/Modules/LoginModule.cs
@@ -60,6 +60,7 @@ namespace PlexRequests.UI.Modules
{
var username = (string)Request.Form.Username;
var password = (string)Request.Form.Password;
+ var dtOffset = (int)Request.Form.DateTimeOffset;
var userId = UserMapper.ValidateUser(username, password);
@@ -73,6 +74,7 @@ namespace PlexRequests.UI.Modules
expiry = DateTime.Now.AddDays(7);
}
Session[SessionKeys.UsernameKey] = username;
+ Session[SessionKeys.ClientDateTimeOffsetKey] = dtOffset;
return this.LoginAndRedirect(userId.Value, expiry);
};
diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs
index a951e0db8..7d7d7b8ad 100644
--- a/PlexRequests.UI/Modules/RequestsModule.cs
+++ b/PlexRequests.UI/Modules/RequestsModule.cs
@@ -40,12 +40,12 @@ using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.UI.Models;
+using PlexRequests.Helpers;
namespace PlexRequests.UI.Modules
{
public class RequestsModule : BaseModule
{
-
public RequestsModule(IRequestService service, ISettingsService prSettings, ISettingsService plex, INotificationService notify) : base("requests")
{
Service = service;
@@ -56,6 +56,7 @@ namespace PlexRequests.UI.Modules
Get["/"] = _ => LoadRequests();
Get["/movies"] = _ => GetMovies();
Get["/tvshows"] = _ => GetTvShows();
+ Get["/albums"] = _ => GetAlbumRequests();
Post["/delete"] = _ => DeleteRequest((int)Request.Form.id);
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);
@@ -79,28 +80,38 @@ namespace PlexRequests.UI.Modules
private Response GetMovies()
{
+ var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbMovies = Service.GetAll().Where(x => x.Type == RequestType.Movie);
- var viewModel = dbMovies.Select(movie => new RequestViewModel
+ if (settings.UsersCanViewOnlyOwnRequests && !isAdmin)
{
- ProviderId = movie.ProviderId,
- Type = movie.Type,
- Status = movie.Status,
- ImdbId = movie.ImdbId,
- Id = movie.Id,
- PosterPath = movie.PosterPath,
- ReleaseDate = movie.ReleaseDate.Humanize(),
- RequestedDate = movie.RequestedDate.Humanize(),
- Approved = movie.Approved,
- Title = movie.Title,
- Overview = movie.Overview,
- RequestedBy = movie.RequestedBy,
- ReleaseYear = movie.ReleaseDate.Year.ToString(),
- Available = movie.Available,
- Admin = isAdmin,
- Issues = movie.Issues.Humanize(LetterCasing.Title),
- OtherMessage = movie.OtherMessage,
- AdminNotes = movie.AdminNote
+ dbMovies = dbMovies.Where(x => x.UserHasRequested(Username));
+ }
+
+ var viewModel = dbMovies.Select(movie => {
+ return new RequestViewModel
+ {
+ ProviderId = movie.ProviderId,
+ Type = movie.Type,
+ Status = movie.Status,
+ ImdbId = movie.ImdbId,
+ Id = movie.Id,
+ PosterPath = movie.PosterPath,
+ ReleaseDate = movie.ReleaseDate.Humanize(),
+ ReleaseDateTicks = movie.ReleaseDate.Ticks,
+ RequestedDate = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Humanize(),
+ RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks,
+ Approved = movie.Available || movie.Approved,
+ 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();
return Response.AsJson(viewModel);
@@ -108,29 +119,80 @@ namespace PlexRequests.UI.Modules
private Response GetTvShows()
{
+ var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated();
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,
- Type = tv.Type,
- Status = tv.Status,
- ImdbId = tv.ImdbId,
- Id = tv.Id,
- PosterPath = tv.PosterPath,
- ReleaseDate = tv.ReleaseDate.Humanize(),
- RequestedDate = tv.RequestedDate.Humanize(),
- Approved = tv.Approved,
- Title = tv.Title,
- Overview = tv.Overview,
- RequestedBy = tv.RequestedBy,
- 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
+ dbAlbum = dbAlbum.Where(x => x.UserHasRequested(Username));
+ }
+
+ var viewModel = dbAlbum.Select(album => {
+ return new RequestViewModel
+ {
+ ProviderId = album.ProviderId,
+ Type = album.Type,
+ Status = album.Status,
+ ImdbId = album.ImdbId,
+ Id = album.Id,
+ PosterPath = album.PosterPath,
+ ReleaseDate = album.ReleaseDate.Humanize(),
+ ReleaseDateTicks = album.ReleaseDate.Ticks,
+ RequestedDate = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Humanize(),
+ RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Ticks,
+ Approved = album.Available || album.Approved,
+ Title = album.Title,
+ 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();
return Response.AsJson(viewModel);
@@ -165,7 +227,7 @@ namespace PlexRequests.UI.Modules
}
originalRequest.Issues = issue;
originalRequest.OtherMessage = !string.IsNullOrEmpty(comment)
- ? $"{Session[SessionKeys.UsernameKey]} - {comment}"
+ ? $"{Username} - {comment}"
: string.Empty;
@@ -173,7 +235,7 @@ namespace PlexRequests.UI.Modules
var model = new NotificationModel
{
- User = Session[SessionKeys.UsernameKey].ToString(),
+ User = Username,
NotificationType = NotificationType.Issue,
Title = originalRequest.Title,
DateTime = DateTime.Now,
diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs
index 2a98e8933..77783a7ba 100644
--- a/PlexRequests.UI/Modules/SearchModule.cs
+++ b/PlexRequests.UI/Modules/SearchModule.cs
@@ -36,6 +36,7 @@ using NLog;
using PlexRequests.Api;
using PlexRequests.Api.Interfaces;
+using PlexRequests.Api.Models.Music;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
@@ -54,7 +55,7 @@ namespace PlexRequests.UI.Modules
ISettingsService prSettings, IAvailabilityChecker checker,
IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings,
ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi,
- INotificationService notify) : base("search")
+ INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService) : base("search")
{
CpService = cpSettings;
PrService = prSettings;
@@ -69,17 +70,23 @@ namespace PlexRequests.UI.Modules
SickRageService = sickRageService;
SickrageApi = srApi;
NotificationService = notify;
+ MusicBrainzApi = mbApi;
+ HeadphonesApi = hpApi;
+ HeadphonesService = hpService;
+
Get["/"] = parameters => RequestLoad();
Get["movie/{searchTerm}"] = parameters => SearchMovie((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/playing"] = parameters => CurrentlyPlayingMovies();
Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId);
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 INotificationService NotificationService { get; }
@@ -93,9 +100,11 @@ namespace PlexRequests.UI.Modules
private ISettingsService PrService { get; }
private ISettingsService SonarrService { get; }
private ISettingsService SickRageService { get; }
+ private ISettingsService HeadphonesService { get; }
private IAvailabilityChecker Checker { get; }
+ private IMusicBrainzApi MusicBrainzApi { get; }
+ private IHeadphonesApi HeadphonesApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
- private string AuthToken => Cache.GetOrSet(CacheKeys.TvDbToken, TvApi.Authenticate, 50);
private Negotiator RequestLoad()
{
@@ -152,6 +161,30 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(model);
}
+ private Response SearchMusic(string searchTerm)
+ {
+ var albums = MusicBrainzApi.SearchAlbum(searchTerm);
+ var releases = albums.releases ?? new List();
+ var model = new List();
+ 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
{
var movies = MovieApi.GetUpcomingMovies();
@@ -174,16 +207,26 @@ namespace PlexRequests.UI.Modules
{
var movieApi = new TheMovieDbApi();
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(movieInfo.DumpJson);
//#if !DEBUG
+ var settings = PrService.GetSettings();
+
+ // check if the movie has already been requested
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);
- return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} has already been requested!" });
+ // check if the current user is already marked as a requester for this movie, 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 ? $"{fullMovieName} was successfully added!" : $"{fullMovieName} has already been requested!" });
}
Log.Debug("movie with id {0} doesnt exists", movieId);
@@ -211,16 +254,14 @@ namespace PlexRequests.UI.Modules
Title = movieInfo.Title,
ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue,
Status = movieInfo.Status,
- RequestedDate = DateTime.Now,
+ RequestedDate = DateTime.UtcNow,
Approved = false,
- RequestedBy = Session[SessionKeys.UsernameKey].ToString(),
+ RequestedUsers = new List() { Username },
Issues = IssueState.None,
};
-
- var settings = PrService.GetSettings();
Log.Trace(settings.DumpJson());
- if (!settings.RequireMovieApproval)
+ if (!settings.RequireMovieApproval || settings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase)))
{
var cpSettings = CpService.GetSettings();
@@ -247,7 +288,7 @@ namespace PlexRequests.UI.Modules
};
NotificationService.Publish(notificationModel);
- return Response.AsJson(new JsonResponseModel {Result = true});
+ return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
}
return
Response.AsJson(new JsonResponseModel
@@ -272,7 +313,7 @@ namespace PlexRequests.UI.Modules
};
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})";
//#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
@@ -328,7 +380,7 @@ namespace PlexRequests.UI.Modules
}
//#endif
-
+
var model = new RequestedModel
{
ProviderId = showInfo.externals?.thetvdb ?? 0,
@@ -338,9 +390,9 @@ namespace PlexRequests.UI.Modules
Title = showInfo.name,
ReleaseDate = firstAir,
Status = showInfo.status,
- RequestedDate = DateTime.Now,
+ RequestedDate = DateTime.UtcNow,
Approved = false,
- RequestedBy = Session[SessionKeys.UsernameKey].ToString(),
+ RequestedUsers = new List() { Username },
Issues = IssueState.None,
ImdbId = showInfo.externals?.imdb ?? string.Empty,
SeasonCount = showInfo.seasonCount
@@ -363,26 +415,26 @@ namespace PlexRequests.UI.Modules
model.SeasonList = seasonsList.ToArray();
- var settings = PrService.GetSettings();
- if (!settings.RequireTvShowApproval)
+ if (!settings.RequireTvShowApproval || settings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase)))
{
var sonarrSettings = SonarrService.GetSettings();
var sender = new TvSender(SonarrApi, SickrageApi);
if (sonarrSettings.Enabled)
{
var result = sender.SendToSonarr(sonarrSettings, model);
- if (result != null)
+ if (result != null && !string.IsNullOrEmpty(result.title))
{
model.Approved = true;
Log.Debug("Adding tv to database requests (No approval required & Sonarr)");
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!" });
}
- 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);
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() { 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;
+ }
}
}
\ No newline at end of file
diff --git a/PlexRequests.UI/Modules/UserLoginModule.cs b/PlexRequests.UI/Modules/UserLoginModule.cs
index cfadd2b8f..c1528bdd7 100644
--- a/PlexRequests.UI/Modules/UserLoginModule.cs
+++ b/PlexRequests.UI/Modules/UserLoginModule.cs
@@ -68,6 +68,7 @@ namespace PlexRequests.UI.Modules
private Response LoginUser()
{
+ var dateTimeOffset = Request.Form.DateTimeOffset;
var username = Request.Form.username.Value;
Log.Debug("Username \"{0}\" attempting to login",username);
if (string.IsNullOrWhiteSpace(username))
@@ -138,6 +139,8 @@ namespace PlexRequests.UI.Modules
Session[SessionKeys.UsernameKey] = (string)username;
}
+ Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset;
+
return Response.AsJson(authenticated
? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "Incorrect User or Password"});
@@ -170,7 +173,7 @@ namespace PlexRequests.UI.Modules
var users = Api.GetUsers(authToken);
Log.Debug("Plex Users: ");
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));
}
diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj
index b561ad36b..6d08abb12 100644
--- a/PlexRequests.UI/PlexRequests.UI.csproj
+++ b/PlexRequests.UI/PlexRequests.UI.csproj
@@ -171,6 +171,7 @@
+
@@ -371,6 +372,9 @@
Always
+
+ Always
+ web.config
diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml
index 41a667e51..86f5e2322 100644
--- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml
+++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml
@@ -88,6 +88,11 @@
+
+
+
+
+
@@ -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");
+ }
+ });
+ });
});
diff --git a/PlexRequests.UI/Views/Admin/Headphones.cshtml b/PlexRequests.UI/Views/Admin/Headphones.cshtml
new file mode 100644
index 000000000..30f45985f
--- /dev/null
+++ b/PlexRequests.UI/Views/Admin/Headphones.cshtml
@@ -0,0 +1,162 @@
+@Html.Partial("_Sidebar")
+@{
+ int port;
+ if (Model.Port == 0)
+ {
+ port = 8081;
+ }
+ else
+ {
+ port = Model.Port;
+ }
+}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml
index 4f658da53..73d28d87c 100644
--- a/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml
+++ b/PlexRequests.UI/Views/Admin/PushbulletNotifications.cshtml
@@ -36,6 +36,12 @@
+
+
+
+
+
+
@@ -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");
+ }
+ });
+ });
});
\ No newline at end of file
diff --git a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml
index 0877739d0..b5fcab7f5 100644
--- a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml
+++ b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml
@@ -36,6 +36,12 @@
+
+
+
+
+
+
@@ -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");
+ }
+ });
+ });
});
\ No newline at end of file
diff --git a/PlexRequests.UI/Views/Admin/Settings.cshtml b/PlexRequests.UI/Views/Admin/Settings.cshtml
index edbe6009c..d8e08431e 100644
--- a/PlexRequests.UI/Views/Admin/Settings.cshtml
+++ b/PlexRequests.UI/Views/Admin/Settings.cshtml
@@ -52,6 +52,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
A comma separated list of users whose requests do not require approval.