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 @@ + True True @@ -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.dll True + + ..\packages\NLog.4.2.3\lib\net45\NLog.dll + True + ..\packages\Octokit.0.19.0\lib\net45\Octokit.dll True @@ -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; + } +} +
+
+
+ Headphones Settings +
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ +
+
+
+ +
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + + + +
+
+ +
+
+
+
+
+ + + + \ 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.

+
+ +
+ +
+
+ +
+
+ + +
+
@*
@@ -102,4 +155,3 @@
- diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index efb8ce36b..e6d21e3fc 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -52,6 +52,14 @@ { SickRage } + @if (Context.Request.Path == "/admin/headphones") + { + Headphones + } + else + { + Headphones + } @if (Context.Request.Path == "/admin/emailnotification") { diff --git a/PlexRequests.UI/Views/Login/Index.cshtml b/PlexRequests.UI/Views/Login/Index.cshtml index 6ca2e444c..0662e2cb2 100644 --- a/PlexRequests.UI/Views/Login/Index.cshtml +++ b/PlexRequests.UI/Views/Login/Index.cshtml @@ -4,8 +4,9 @@ Password
Remember Me -
+

+ @if (!Model.AdminExists) { @@ -19,3 +20,9 @@ } + diff --git a/PlexRequests.UI/Views/Login/Register.cshtml b/PlexRequests.UI/Views/Login/Register.cshtml index d8e8564e5..c53fa0193 100644 --- a/PlexRequests.UI/Views/Login/Register.cshtml +++ b/PlexRequests.UI/Views/Login/Register.cshtml @@ -1,7 +1,7 @@ 
- Username + Username
- Password + Password

diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index cd73d64d9..e2b3d9bd6 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -2,12 +2,8 @@

Requests

Below you can see yours and all other requests, as well as their download and approval status.

- @if (Context.CurrentUser.IsAuthenticated()) - { - -
-
- } +
+ +
- -
- - +
+
+
+
+ @if (Context.CurrentUser.IsAuthenticated()) + { + @if (Model.SearchForMovies) + { + + } + @if (Model.SearchForTvShows) + { + + } + @if (Model.SearchForMusic) + { + + } + } +
+ + +
+
@if (Model.SearchForMovies) - { + {
-
-
+
+
@@ -61,24 +86,37 @@ } @if (Model.SearchForTvShows) - { + {
-
-
+
+
} + + @if (Model.SearchForMusic) + { + +
+ +
+
+ +
+
+
+ }
+ + + + + + diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index 5245bdbdd..486acedd5 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -7,8 +7,8 @@ Plex Requests - + @@ -87,8 +87,37 @@
-
- @RenderBody() +
+ @RenderBody() +
+
+ + +
+ \ No newline at end of file diff --git a/PlexRequests.UI/Views/UserLogin/Index.cshtml b/PlexRequests.UI/Views/UserLogin/Index.cshtml index 414083139..cc9bf9c5d 100644 --- a/PlexRequests.UI/Views/UserLogin/Index.cshtml +++ b/PlexRequests.UI/Views/UserLogin/Index.cshtml @@ -38,10 +38,14 @@ $('#loginBtn').click(function (e) { e.preventDefault(); var $form = $("#loginForm"); + var formData = $form.serialize(); + var dtOffset = new Date().getTimezoneOffset(); + formData += ('&DateTimeOffset=' + dtOffset) + $.ajax({ type: $form.prop("method"), url: $form.prop("action"), - data: $form.serialize(), + data: formData, dataType: "json", success: function (response) { console.log(response); diff --git a/PlexRequests.sln b/PlexRequests.sln index 4cdca72cf..623a5a2e0 100644 --- a/PlexRequests.sln +++ b/PlexRequests.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .travis.yml = .travis.yml appveyor.yml = appveyor.yml + .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md LICENSE = LICENSE README.md = README.md EndProjectSection diff --git a/README.md b/README.md index 2376c1bae..efe061498 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,19 @@ This is based off [Plex Requests by lokenx](https://github.com/lokenx/plexrequests-meteor) so big props to that guy! I wanted to write a similar application in .Net! -#Features +# Features -* Integration with [TheMovieDB](https://www.themoviedb.org/) for all Movies -* Integration with [TVMaze](www.tvmaze.com) for all TV shows! -* Secure authentication -* [Sonarr](https://sonarr.tv/) integration (SickRage/Sickbeard TBD) -* [CouchPotato](https://couchpota.to/) integration -* [SickRage](https://sickrage.github.io/) integration -* Email notifications -* Pushbullet notifications -* Pushover notifications +* Movie and TV Show searching, can't find something on Plex? Just request it! +* Notifications! Get notified via Email, Pushbullet and Pushover for new requests and issue reports! +* Send your TV Shows to either [Sonarr](https://sonarr.tv/) or [SickRage](http://www.sickrage.ca/)! +* Secure authentication so you don't have to worry about those script kiddies +* We check to see if the request is already in Plex, if it's already in Plex then why you requesting it?! +* We have allowed the ability for a user to add a custom note on a request +* It automatically update the status of requests when they are available on Plex +* Sick, responsive and mobile friendly UI +* Headphones integration will be comming soon! -#Preview +# Preview (Needs updating) ![Preview](http://i.imgur.com/ucCFUvd.gif) @@ -31,10 +31,10 @@ Download the latest [Release](https://github.com/tidusjar/PlexRequests.Net/relea Extract the .zip file (Unblock if on Windows! Right Click > Properties > Unblock). Just run `PlexRequests.exe`! (Mono compatible `mono PlexRequests.exe`) -#Configuration +# FAQ +Do you have an issue or a question? if so check out our [FAQ!](https://github.com/tidusjar/PlexRequests.Net/wiki/FAQ) -To configure PlexRequests you need to register an admin user by clicking on Admin (top left) and press the Register link. -You will then have a admin menu option once registered where you can setup Sonarr, Couchpotato and any other settings. +# Docker Looking for a Docker Image? Well [rogueosb](https://github.com/rogueosb/) has created a docker image for us, You can find it [here](https://github.com/rogueosb/docker-plexrequestsnet) :smile: @@ -84,7 +84,9 @@ end script ####Reboot, then open up your browser to check that it's running! -```sudo shutdown -r 00``` +``` +sudo shutdown -r 00 +``` # Contributors @@ -95,14 +97,6 @@ Please feed free to submit a pull request! # Donation If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet) -###### A massive thanks to everyone below! - -[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake) - +## A massive thanks to everyone below for all their help! -# Sponsors -- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools!!! - - [ReSharper](http://www.jetbrains.com/resharper/) - - [dotTrace] (https://www.jetbrains.com/profiler/) - - [dotMemory] (https://www.jetbrains.com/dotmemory/) - - [dotCover] (https://www.jetbrains.com/dotcover/) +[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727), Majawat