diff --git a/PlexRequests.Api.Interfaces/IApiRequest.cs b/PlexRequests.Api.Interfaces/IApiRequest.cs index a7e274752..f2fa72f44 100644 --- a/PlexRequests.Api.Interfaces/IApiRequest.cs +++ b/PlexRequests.Api.Interfaces/IApiRequest.cs @@ -33,6 +33,7 @@ namespace PlexRequests.Api.Interfaces public interface IApiRequest { T Execute(IRestRequest request, Uri baseUri) where T : new(); + IRestResponse Execute(IRestRequest request, Uri baseUri); T ExecuteXml(IRestRequest request, Uri baseUri) where T : class; T ExecuteJson(IRestRequest request, Uri baseUri) where T : new(); } diff --git a/PlexRequests.Api.Interfaces/IHeadphonesApi.cs b/PlexRequests.Api.Interfaces/IHeadphonesApi.cs new file mode 100644 index 000000000..a895f1689 --- /dev/null +++ b/PlexRequests.Api.Interfaces/IHeadphonesApi.cs @@ -0,0 +1,44 @@ +#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; +using System.Collections.Generic; +using System.Threading.Tasks; + +using PlexRequests.Api.Models.Music; + +namespace PlexRequests.Api.Interfaces +{ + public interface IHeadphonesApi + { + Task AddAlbum(string apiKey, Uri baseUrl, string albumId); + HeadphonesVersion GetVersion(string apiKey, Uri baseUrl); + Task AddArtist(string apiKey, Uri baseUrl, string artistId); + Task QueueAlbum(string apiKey, Uri baseUrl, string albumId); + Task> GetIndex(string apiKey, Uri baseUrl); + Task RefreshArtist(string apiKey, Uri baseUrl, string artistId); + } +} \ 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/HeadphonesGetIndex.cs b/PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs new file mode 100644 index 000000000..7f4d46c9e --- /dev/null +++ b/PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs @@ -0,0 +1,49 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: HeadphonesGetIndex.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 HeadphonesGetIndex + { + public string Status { get; set; } + public string ThumbURL { get; set; } + public string DateAdded { get; set; } + public string MetaCritic { get; set; } + public int? TotalTracks { get; set; } + public object Type { get; set; } + public int? IncludeExtras { get; set; } + public string ArtistName { get; set; } + public string LastUpdated { get; set; } + public string ReleaseDate { get; set; } + public string AlbumID { get; set; } + public string ArtistID { get; set; } + public string ArtworkURL { get; set; } + public string Extras { get; set; } + public int? HaveTracks { get; set; } + public string LatestAlbum { get; set; } + public string ArtistSortName { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Api.Models/Music/HeadphonesVersion.cs b/PlexRequests.Api.Models/Music/HeadphonesVersion.cs new file mode 100644 index 000000000..1ee6e16e8 --- /dev/null +++ b/PlexRequests.Api.Models/Music/HeadphonesVersion.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: HeadphonesVersion.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 HeadphonesVersion + { + public string install_type { get; set; } + public object current_version { get; set; } + public string git_path { get; set; } + public string latest_version { get; set; } + public int commits_behind { get; set; } + } +} \ 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..b3ca2ee62 --- /dev/null +++ b/PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs @@ -0,0 +1,68 @@ +#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 + { + [JsonProperty(PropertyName = "artist-credit")] + public List ArtistCredits { get; set; } + 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 ReleaseEvents { 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..157bd5861 --- /dev/null +++ b/PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs @@ -0,0 +1,154 @@ +#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 string name { get; set; } + public string joinphrase { 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/Plex/PlexSearch.cs b/PlexRequests.Api.Models/Plex/PlexSearch.cs index f95efce64..b9fcd89a9 100644 --- a/PlexRequests.Api.Models/Plex/PlexSearch.cs +++ b/PlexRequests.Api.Models/Plex/PlexSearch.cs @@ -303,6 +303,8 @@ public class Directory1 public string AddedAt { get; set; } [XmlAttribute(AttributeName = "updatedAt")] public string UpdatedAt { get; set; } + [XmlAttribute(AttributeName = "parentTitle")] + public string ParentTitle { get; set; } } @@ -310,7 +312,7 @@ public class Directory1 public class PlexSearch { [XmlElement(ElementName = "Directory")] - public Directory1 Directory { get; set; } + public List Directory { get; set; } [XmlElement(ElementName = "Video")] public List + True True @@ -75,6 +76,7 @@ + diff --git a/PlexRequests.Api/SickrageApi.cs b/PlexRequests.Api/SickrageApi.cs index fc4714279..000ffd130 100644 --- a/PlexRequests.Api/SickrageApi.cs +++ b/PlexRequests.Api/SickrageApi.cs @@ -74,17 +74,18 @@ public async Task AddSeries(int tvdbId, int seasonCount, int[] se var obj = Api.Execute(request, baseUrl); - if (obj.result != "failure") { var sw = new Stopwatch(); sw.Start(); - // Check to see if it's been added yet. - var showInfo = new SickRageShowInformation { message = "Show not found" }; - while (showInfo.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase)) + var seasonIncrement = 0; + var seasonList = new SickRageSeasonList(); + while (seasonIncrement < seasonCount) { - showInfo = CheckShowHasBeenAdded(tvdbId, apiKey, baseUrl); + seasonList = VerifyShowHasLoaded(tvdbId, apiKey, baseUrl); + seasonIncrement = seasonList.data?.Length ?? 0; + if (sw.ElapsedMilliseconds > 30000) // Break out after 30 seconds, it's not going to get added { Log.Warn("Couldn't find out if the show had been added after 10 seconds. I doubt we can change the status to wanted."); @@ -93,8 +94,6 @@ public async Task AddSeries(int tvdbId, int seasonCount, int[] se } sw.Stop(); } - - if (seasons.Length > 0) { //handle the seasons requested @@ -123,36 +122,42 @@ public SickRagePing Ping(string apiKey, Uri baseUrl) return obj; } - public async Task AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl) + public SickRageSeasonList VerifyShowHasLoaded(int tvdbId, string apiKey, Uri baseUrl) { var request = new RestRequest { - Resource = "/api/{apiKey}/?cmd=episode.setstatus", + Resource = "/api/{apiKey}/?cmd=show.seasonlist", Method = Method.GET }; request.AddUrlSegment("apiKey", apiKey); request.AddQueryParameter("tvdbid", tvdbId.ToString()); - request.AddQueryParameter("season", season.ToString()); - request.AddQueryParameter("status", SickRageStatus.Wanted); - await Task.Run(() => Thread.Sleep(2000)); - return await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false); + try + { + var obj = Api.ExecuteJson(request, baseUrl); + return obj; + } + catch (Exception e) + { + Log.Error(e); + return new SickRageSeasonList(); + } } - - public SickRageShowInformation CheckShowHasBeenAdded(int tvdbId, string apiKey, Uri baseUrl) + public async Task AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl) { var request = new RestRequest { - Resource = "/api/{apiKey}/?cmd=show", + Resource = "/api/{apiKey}/?cmd=episode.setstatus", Method = Method.GET }; request.AddUrlSegment("apiKey", apiKey); request.AddQueryParameter("tvdbid", tvdbId.ToString()); + request.AddQueryParameter("season", season.ToString()); + request.AddQueryParameter("status", SickRageStatus.Wanted); - var obj = Api.Execute(request, baseUrl); - - return obj; + await Task.Run(() => Thread.Sleep(2000)); + return await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false); } } } \ No newline at end of file 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 @@ public List GetProfiles(string apiKey, Uri baseUrl) public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl) { - + Log.Debug("Adding series {0}", title); + Log.Debug("Seasons = {0}, out of {1} seasons", seasons.DumpJson(), seasonCount); var request = new RestRequest { Resource = "/api/Series?", @@ -74,7 +80,6 @@ public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool s rootFolderPath = rootPath }; - for (var i = 1; i <= seasonCount; i++) { var season = new Season @@ -85,12 +90,25 @@ public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool s 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/CacheKeys.cs b/PlexRequests.Core/CacheKeys.cs index c76e20034..a40291b91 100644 --- a/PlexRequests.Core/CacheKeys.cs +++ b/PlexRequests.Core/CacheKeys.cs @@ -29,5 +29,8 @@ namespace PlexRequests.Core public class CacheKeys { public const string TvDbToken = "TheTvDbApiToken"; + public const string SonarrQualityProfiles = "SonarrQualityProfiles"; + public const string SickRageQualityProfiles = "SickRageQualityProfiles"; + public const string CouchPotatoQualityProfiles = "CouchPotatoQualityProfiles"; } } \ No newline at end of file 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 @@ public long AddRequest(RequestedModel model) // 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 @@ public bool UpdateRequest(RequestedModel model) 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/Models/StatusModel.cs b/PlexRequests.Core/Models/StatusModel.cs index bea494dfe..1e04b35e6 100644 --- a/PlexRequests.Core/Models/StatusModel.cs +++ b/PlexRequests.Core/Models/StatusModel.cs @@ -25,11 +25,20 @@ // ************************************************************************/ #endregion +using System.Text.RegularExpressions; + namespace PlexRequests.Core.Models { public class StatusModel { public string Version { get; set; } + public int DBVersion { + get + { + string trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(Version, string.Empty).PadRight(4, '0'); + return int.Parse(trimStatus); + } + } public bool UpdateAvailable { get; set; } public string UpdateUri { get; set; } public string DownloadUri { get; set; } diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index a1e0538f7..4b588aa82 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 @@ + @@ -95,6 +100,10 @@ + + {95834072-a675-415d-aa8f-877c91623810} + PlexRequests.Api.Interfaces + {CB37A5F8-6DFC-4554-99D3-A42B502E4591} PlexRequests.Api.Models 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 @@ public class PlexRequestSettings : Settings 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/SettingModels/SickRageSettings.cs b/PlexRequests.Core/SettingModels/SickRageSettings.cs index 657de3b50..d7c734667 100644 --- a/PlexRequests.Core/SettingModels/SickRageSettings.cs +++ b/PlexRequests.Core/SettingModels/SickRageSettings.cs @@ -28,6 +28,7 @@ using System; using Newtonsoft.Json; using PlexRequests.Helpers; +using System.Collections.Generic; namespace PlexRequests.Core.SettingModels { @@ -40,6 +41,23 @@ public class SickRageSettings : Settings public string QualityProfile { get; set; } public bool Ssl { get; set; } public string SubDir { get; set; } + public Dictionary Qualities + { + get + { + return new Dictionary() { + { "default", "Use Deafult" }, + { "sdtv", "SD TV" }, + { "sddvd", "SD DVD" }, + { "hdtv", "HD TV" }, + { "rawhdtv", "Raw HD TV" }, + { "hdwebdl", "HD Web DL" }, + { "fullhdwebdl", "Full HD Web DL" }, + { "hdbluray", "HD Bluray" }, + { "fullhdbluray", "Full HD Bluray" } + }; + } + } [JsonIgnore] public Uri FullUri diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 4dea3a27a..bbf41039e 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -30,16 +30,19 @@ using System.Linq; using Mono.Data.Sqlite; +using NLog; using PlexRequests.Api; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Store; using PlexRequests.Store.Repository; +using System.Threading.Tasks; namespace PlexRequests.Core { public class Setup { + private static Logger Log = LogManager.GetCurrentClassLogger(); private static DbConfiguration Db { get; set; } public string SetupDb() { @@ -51,13 +54,41 @@ public string SetupDb() { CreateDefaultSettingsPage(); } + + var version = CheckSchema(); + if (version > 0) + { + if (version > 1300 && version <= 1699) + { + MigrateDbFrom1300(); + UpdateRequestBlobsTable(); + } + } - MigrateDb(); return Db.DbConnection().ConnectionString; } public static string ConnectionString => Db.DbConnection().ConnectionString; + + private int CheckSchema() + { + var checker = new StatusChecker(); + var status = checker.GetStatus(); + + var connection = Db.DbConnection(); + var schema = connection.GetSchemaVersion(); + if (schema == null) + { + connection.CreateSchema(status.DBVersion); // Set the default. + schema = connection.GetSchemaVersion(); + } + + var version = schema.SchemaVersion; + + return version; + } + private void CreateDefaultSettingsPage() { var defaultSettings = new PlexRequestSettings @@ -72,8 +103,82 @@ private void CreateDefaultSettingsPage() s.SaveSettings(defaultSettings); } - private void MigrateDb() // TODO: Remove when no longer needed + public void CacheQualityProfiles() { + var mc = new MemoryCacheProvider(); + + try + { + Task.Run(() => { CacheSonarrQualityProfiles(mc); }); + Task.Run(() => { CacheCouchPotatoQualityProfiles(mc); }); + // we don't need to cache sickrage profiles, those are static + // TODO: cache headphones profiles? + } + catch (Exception) + { + Log.Error("Failed to cache quality profiles on startup!"); + } + } + + private void CacheSonarrQualityProfiles(MemoryCacheProvider cacheProvider) + { + try + { + Log.Info("Executing GetSettings call to Sonarr for quality profiles"); + var sonarrSettingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())); + var sonarrSettings = sonarrSettingsService.GetSettings(); + if (sonarrSettings.Enabled) + { + Log.Info("Begin executing GetProfiles call to Sonarr for quality profiles"); + SonarrApi sonarrApi = new SonarrApi(); + var profiles = sonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri); + cacheProvider.Set(CacheKeys.SonarrQualityProfiles, profiles); + Log.Info("Finished executing GetProfiles call to Sonarr for quality profiles"); + } + } + catch (Exception ex) + { + Log.Error("Failed to cache Sonarr quality profiles!", ex); + } + } + + private void CacheCouchPotatoQualityProfiles(MemoryCacheProvider cacheProvider) + { + try + { + Log.Info("Executing GetSettings call to CouchPotato for quality profiles"); + var cpSettingsService = new SettingsServiceV2(new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())); + var cpSettings = cpSettingsService.GetSettings(); + if (cpSettings.Enabled) + { + Log.Info("Begin executing GetProfiles call to CouchPotato for quality profiles"); + CouchPotatoApi cpApi = new CouchPotatoApi(); + var profiles = cpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey); + cacheProvider.Set(CacheKeys.CouchPotatoQualityProfiles, profiles); + Log.Info("Finished executing GetProfiles call to CouchPotato for quality profiles"); + } + } + catch (Exception ex) + { + Log.Error("Failed to cache CouchPotato quality profiles!", ex); + } + } + + private void UpdateRequestBlobsTable() // TODO: Remove in v1.7 + { + try + { + TableCreation.AlterTable(Db.DbConnection(), "RequestBlobs", "ADD COLUMN", "MusicId", false, "TEXT"); + } + catch (Exception e) + { + Log.Error("Tried updating the schema to alter the request blobs table"); + Log.Error(e); + } + } + private void MigrateDbFrom1300() // TODO: Remove in v1.7 + { + var result = new List(); RequestedModel[] requestedModels; var repo = new GenericRepository(Db, new MemoryCacheProvider()); @@ -113,7 +218,7 @@ private void MigrateDb() // TODO: Remove when no longer needed Issues = r.Issues, OtherMessage = r.OtherMessage, Overview = show.summary.RemoveHtml(), - RequestedBy = r.RequestedBy, + RequestedUsers = r.AllUsers, // should pull in the RequestedBy property and merge with RequestedUsers RequestedDate = r.ReleaseDate, Status = show.status }; @@ -121,7 +226,7 @@ private void MigrateDb() // TODO: Remove when no longer needed 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/StatusChecker.cs b/PlexRequests.Core/StatusChecker.cs index 94a745535..476a55a45 100644 --- a/PlexRequests.Core/StatusChecker.cs +++ b/PlexRequests.Core/StatusChecker.cs @@ -26,8 +26,6 @@ #endregion using System; using System.Linq; -using System.Reflection; -using System.Runtime.Versioning; using System.Threading.Tasks; using Octokit; @@ -62,7 +60,10 @@ public StatusModel GetStatus() }; var latestRelease = GetLatestRelease(); - + if (latestRelease.Result == null) + { + return new StatusModel { Version = "Unknown" }; + } var latestVersionArray = latestRelease.Result.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries); var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty; 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..1c4277c4a --- /dev/null +++ b/PlexRequests.Helpers/DateTimeHelper.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +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); + } + + public static void CustomParse(string date, out DateTime dt) + { + // Try and parse it + if (DateTime.TryParse(date, out dt)) + { + return; + } + + // Maybe it's only a year? + if (DateTime.TryParseExact(date, "yyyy", CultureInfo.CurrentCulture, DateTimeStyles.None, out dt)) + { + return; + } + } + + private static TimeZoneInfo FindTimeZoneFromOffset(int minuteOffset) + { + var tzc = TimeZoneInfo.GetSystemTimeZones(); + return tzc.FirstOrDefault(x => x.BaseUtcOffset.TotalMinutes == -minuteOffset); + } + } +} diff --git a/PlexRequests.Helpers/ICacheProvider.cs b/PlexRequests.Helpers/ICacheProvider.cs index 3e3a920bb..fa957315d 100644 --- a/PlexRequests.Helpers/ICacheProvider.cs +++ b/PlexRequests.Helpers/ICacheProvider.cs @@ -55,7 +55,7 @@ public interface ICacheProvider /// The key. /// The object we want to store. /// The amount of time we want to cache the object. - void Set(string key, object data, int cacheTime); + void Set(string key, object data, int cacheTime = 20); /// /// Removes the specified object from the cache. diff --git a/PlexRequests.Helpers/MemoryCacheProvider.cs b/PlexRequests.Helpers/MemoryCacheProvider.cs index 30863c19f..6e513502c 100644 --- a/PlexRequests.Helpers/MemoryCacheProvider.cs +++ b/PlexRequests.Helpers/MemoryCacheProvider.cs @@ -83,7 +83,7 @@ public T Get(string key) where T : class /// The key. /// The object we want to store. /// The amount of time we want to cache the object. - public void Set(string key, object data, int cacheTime) + public void Set(string key, object data, int cacheTime = 20) { var policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime) }; Cache.Add(new CacheItem(key, data), policy); 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..e8a63474a 100644 --- a/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs +++ b/PlexRequests.Services.Tests/PlexAvailabilityCheckerTests.cs @@ -32,12 +32,12 @@ 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 { @@ -55,7 +55,7 @@ public void IsAvailableWithEmptySettingsTest() var plexMock = new Mock(); Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object); - Assert.Throws(() => Checker.IsAvailable("title", "2013"), "We should be throwing an exception since we cannot talk to the services."); + Assert.Throws(() => Checker.IsAvailable("title", "2013", null, PlexType.TvShow), "We should be throwing an exception since we cannot talk to the services."); } [Test] @@ -66,7 +66,7 @@ public void IsAvailableTest() 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 @@ public class AvailabilityUpdateService : ITask, IRegisteredObject, IAvailability { 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/IAvailabilityChecker.cs b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs index 02d8e8d57..55e914bd2 100644 --- a/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs +++ b/PlexRequests.Services/Interfaces/IAvailabilityChecker.cs @@ -29,6 +29,6 @@ namespace PlexRequests.Services.Interfaces public interface IAvailabilityChecker { void CheckAndUpdateAll(long check); - bool IsAvailable(string title, string year); + bool IsAvailable(string title, string year, string artist, PlexType type); } } \ No newline at end of file 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 @@ public interface INotification 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 @@ public EmailMessageNotification(ISettingsService sett 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 @@ public async Task NotifyAsync(NotificationModel model) case NotificationType.AdminNote: throw new NotImplementedException(); + case NotificationType.Test: + await EmailTest(model, emailSettings); + break; + default: throw new ArgumentOutOfRangeException(); } @@ -100,23 +109,23 @@ private bool ValidateConfiguration(EmailNotificationSettings settings) 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 @@ private async Task EmailNewRequest(NotificationModel model) } } - 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 NLog; using PlexRequests.Services.Interfaces; +using PlexRequests.Core.SettingModels; namespace PlexRequests.Services.Notification { @@ -47,6 +48,13 @@ public async Task Publish(NotificationModel model) 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 @@ private static async Task NotifyAsync(INotification notification, NotificationMo { 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 @@ public enum NotificationType 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 @@ public PushbulletNotification(IPushbulletApi pushbulletApi, ISettingsService "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 @@ public async Task NotifyAsync(NotificationModel model) 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 @@ private PushbulletNotificationSettings GetSettings() return SettingsService.GetSettings(); } - private async Task PushNewRequestAsync(NotificationModel model) + private async Task PushNewRequestAsync(NotificationModel model, PushbulletNotificationSettings settings) { var message = $"{model.Title} has been requested by user: {model.User}"; var 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 @@ private async Task PushNewRequestAsync(NotificationModel model) } } - 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 @@ public PushoverNotification(IPushoverApi pushoverApi, ISettingsService "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 @@ public async Task NotifyAsync(NotificationModel model) 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 @@ private PushoverNotificationSettings GetSettings() return SettingsService.GetSettings(); } - private async Task PushNewRequestAsync(NotificationModel model) + private async Task PushNewRequestAsync(NotificationModel model, PushoverNotificationSettings settings) { var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}"; 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 @@ private async Task PushNewRequestAsync(NotificationModel model) } } - 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..8cfa35b04 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,81 +55,200 @@ public PlexAvailabilityChecker(ISettingsService plexSettings, ISet 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()); + bool matchResult; + var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy"); + switch (r.Type) + { + case RequestType.Movie: + matchResult = MovieTvSearch(results, r.Title, releaseDate); + break; + case RequestType.TvShow: + matchResult = MovieTvSearch(results, r.Title, releaseDate); + break; + case RequestType.Album: + matchResult = AlbumSearch(results, r.Title, r.ArtistName); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (matchResult) + { + r.Available = true; + modifiedModel.Add(r); + continue; + } - originalRequest.Available = result != null; - modifiedModel.Add(originalRequest); + 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); + } } /// - /// Determines whether the specified search term is available. + /// Determines whether the specified title is available. /// - /// The search term. + /// The title. /// The year. + /// The artist. + /// The type. /// /// The settings are not configured for Plex or Authentication - public bool IsAvailable(string title, string year) + /// null + public bool IsAvailable(string title, string year, string artist, PlexType type) { + 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"); } - if (!string.IsNullOrEmpty(year)) + var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri); + + switch (type) + { + case PlexType.Movie: + return MovieTvSearch(results, title, year); + case PlexType.TvShow: + return MovieTvSearch(results, title, year); + case PlexType.Music: + return AlbumSearch(results, title, artist); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + /// + /// Searches the movies and TV shows on Plex. + /// + /// The results. + /// The title. + /// The year. + /// + private bool MovieTvSearch(PlexSearch results, string title, string year) + { + try { - 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; - return result?.Title != null || directoryTitle; + + if (!string.IsNullOrEmpty(year)) + { + var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase) && x.Year == year); + + var directoryResult = false; + if (results.Directory != null) + { + if (results.Directory.Any(d => d.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && d.Year == year)) + { + directoryResult = true; + } + } + return result?.Title != null || directoryResult; + } + else + { + var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase)); + var directoryResult = false; + if (results.Directory != null) + { + if (results.Directory.Any(d => d.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase))) + { + directoryResult = true; + } + } + return result?.Title != null || directoryResult; + } } - else + catch (Exception e) { - 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; - return result?.Title != null || directoryTitle; + Log.Error("Could not finish the Movie/TV check in Plex because of an exception:"); + Log.Error(e); + return false; } - } - private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth, IEnumerable requests) + /// + /// Searches the music on Plex. + /// + /// The results. + /// The title. + /// The artist. + /// + private bool AlbumSearch(PlexSearch results, string title, string artist) { - if (plex.Ip == null || auth.PlexAuthToken == null || requests == null) + try { - Log.Warn("A setting is null, Ensure Plex is configured correctly, and we have a Plex Auth token."); - return false; + foreach (var r in results.Directory) + { + var titleMatch = r.Title.Contains(title); + var artistMatch = r.ParentTitle.Equals(artist, StringComparison.CurrentCultureIgnoreCase); + if (titleMatch && artistMatch) + { + return true; + } + } } - if (!requests.Any()) + catch (Exception e) { - Log.Info("We have no requests to check if they are available on Plex."); - return false; + Log.Error("Could not finish the Album check in Plex because of an exception:"); + Log.Error(e); } - return true; + return false; } private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth) diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index bbb334ca9..dba3cfce0 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -86,6 +86,7 @@ + diff --git a/PlexRequests.Services/PlexType.cs b/PlexRequests.Services/PlexType.cs new file mode 100644 index 000000000..27cd0d1da --- /dev/null +++ b/PlexRequests.Services/PlexType.cs @@ -0,0 +1,35 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexType.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.Services +{ + public enum PlexType + { + Movie, + TvShow, + Music + } +} \ No newline at end of file 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 @@ public DbConfiguration(SqliteFactory provider) Factory = provider; } - private SqliteFactory Factory { get; set; } + private SqliteFactory Factory { get; } + private string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile); public virtual bool CheckDb() { 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 @@ public virtual bool CheckDb() } public string DbFile = "PlexRequests.sqlite"; - + /// /// Gets the database connection. /// @@ -72,7 +73,7 @@ public virtual IDbConnection DbConnection() { throw new SqliteException("Factory returned null"); } - fact.ConnectionString = "Data Source=" + DbFile; + fact.ConnectionString = "Data Source=" + CurrentPath; return fact; } @@ -83,14 +84,16 @@ public virtual void CreateDatabase() { 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 @@ public class RequestBlobs : Entity 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 @@ public long Insert(RequestBlobs entity) public IEnumerable GetAll() { - var key = TypeName + "GetAll"; + var key = "GetAll"; var item = Cache.GetOrSet(key, () => { using (var con = Db.DbConnection()) @@ -74,7 +72,7 @@ public IEnumerable GetAll() 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 @@ public bool Update(RequestBlobs entity) 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..4b6f288cf 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 @@ public class RequestedModel : Entity 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,50 @@ public class RequestedModel : Entity 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; } + public string ArtistName { get; set; } + public string ArtistId { 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..717bd2dd9 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 @@ public static void CreateTables(IDbConnection connection) 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, int version) + { + con.Open(); + con.Query(string.Format("INSERT INTO DBInfo (SchemaVersion) values ({0})", version)); + con.Close(); + } + + + + [Table("DBInfo")] + public class DbInfo + { + public int SchemaVersion { get; set; } + } + [Table("sqlite_master")] public class SqliteMasterTable { @@ -54,5 +105,17 @@ public class SqliteMasterTable 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..130000db9 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -44,6 +44,7 @@ using PlexRequests.Store.Repository; using PlexRequests.UI.Models; using PlexRequests.UI.Modules; +using PlexRequests.Helpers; namespace PlexRequests.UI.Tests { @@ -59,6 +60,7 @@ public class AdminModuleTests 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; } @@ -66,6 +68,7 @@ public class AdminModuleTests private Mock CpApi { get; set; } private Mock> LogRepo { get; set; } private Mock NotificationService { get; set; } + private Mock Cache { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; } @@ -94,6 +97,8 @@ public void Setup() PushoverSettings = new Mock>(); PushoverApi = new Mock(); NotificationService = new Mock(); + HeadphonesSettings = new Mock>(); + Cache = new Mock(); Bootstrapper = new ConfigurableBootstrapper(with => { @@ -114,6 +119,8 @@ public void Setup() with.Dependency(PushoverSettings.Object); with.Dependency(PushoverApi.Object); with.Dependency(NotificationService.Object); + with.Dependency(HeadphonesSettings.Object); + with.Dependencies(Cache.Object); with.RootPathProvider(); with.RequestStartup((container, pipelines, context) => { diff --git a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj index e79b01dc4..12950fc2c 100644 --- a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj +++ b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj @@ -113,6 +113,10 @@ {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} PlexRequests.Core + + {1252336d-42a3-482a-804c-836e60173dfa} + PlexRequests.Helpers + {566EFA49-68F8-4716-9693-A6B3F2624DEA} PlexRequests.Services 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 @@ protected override void ConfigureRequestContainer(TinyIoCContainer container, Na 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 @@ protected override void ConfigureRequestContainer(TinyIoCContainer container, Na 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 @@ protected override void ApplicationStartup(TinyIoCContainer container, IPipeline 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..cd5e95112 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,78 @@ 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; } + +.btn-split .btn { + border-radius: 0 !important; } + +.btn-split .btn:not(.dropdown-toggle) { + border-radius: 0.25rem 0 0 0.25rem !important; } + +.btn-split .btn.dropdown-toggle { + border-radius: 0 0.25rem 0.25rem 0 !important; + padding: 12px 8px; } + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 45dc19440..cd21703d4 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;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index df29fcfc8..77a7ca1d9 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,102 @@ 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; } -#tvList .mix{ - display: none; + +.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; +} + +.btn-split .btn { + border-radius: 0 !important; +} + +.btn-split .btn:not(.dropdown-toggle) { + border-radius: .25rem 0 0 .25rem $i; +} + +.btn-split .btn.dropdown-toggle { + border-radius: 0 .25rem .25rem 0 $i; + padding: 12px 8px; } \ No newline at end of file diff --git a/PlexRequests.UI/Content/moment.min.es5.js b/PlexRequests.UI/Content/moment.min.es5.js new file mode 100644 index 000000000..de2fcded6 --- /dev/null +++ b/PlexRequests.UI/Content/moment.min.es5.js @@ -0,0 +1,822 @@ +//! moment.js +//! version : 2.12.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +"use strict"; + +!(function (a, b) { + "object" == typeof exports && "undefined" != typeof module ? module.exports = b() : "function" == typeof define && define.amd ? define(b) : a.moment = b(); +})(undefined, function () { + "use strict";function a() { + return Zc.apply(null, arguments); + }function b(a) { + Zc = a; + }function c(a) { + return a instanceof Array || "[object Array]" === Object.prototype.toString.call(a); + }function d(a) { + return a instanceof Date || "[object Date]" === Object.prototype.toString.call(a); + }function e(a, b) { + var c, + d = [];for (c = 0; c < a.length; ++c) d.push(b(a[c], c));return d; + }function f(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); + }function g(a, b) { + for (var c in b) f(b, c) && (a[c] = b[c]);return f(b, "toString") && (a.toString = b.toString), f(b, "valueOf") && (a.valueOf = b.valueOf), a; + }function h(a, b, c, d) { + return Ia(a, b, c, d, !0).utc(); + }function i() { + return { empty: !1, unusedTokens: [], unusedInput: [], overflow: -2, charsLeftOver: 0, nullInput: !1, invalidMonth: null, invalidFormat: !1, userInvalidated: !1, iso: !1 }; + }function j(a) { + return null == a._pf && (a._pf = i()), a._pf; + }function k(a) { + if (null == a._isValid) { + var b = j(a);a._isValid = !(isNaN(a._d.getTime()) || !(b.overflow < 0) || b.empty || b.invalidMonth || b.invalidWeekday || b.nullInput || b.invalidFormat || b.userInvalidated), a._strict && (a._isValid = a._isValid && 0 === b.charsLeftOver && 0 === b.unusedTokens.length && void 0 === b.bigHour); + }return a._isValid; + }function l(a) { + var b = h(NaN);return null != a ? g(j(b), a) : j(b).userInvalidated = !0, b; + }function m(a) { + return void 0 === a; + }function n(a, b) { + var c, d, e;if ((m(b._isAMomentObject) || (a._isAMomentObject = b._isAMomentObject), m(b._i) || (a._i = b._i), m(b._f) || (a._f = b._f), m(b._l) || (a._l = b._l), m(b._strict) || (a._strict = b._strict), m(b._tzm) || (a._tzm = b._tzm), m(b._isUTC) || (a._isUTC = b._isUTC), m(b._offset) || (a._offset = b._offset), m(b._pf) || (a._pf = j(b)), m(b._locale) || (a._locale = b._locale), $c.length > 0)) for (c in $c) d = $c[c], e = b[d], m(e) || (a[d] = e);return a; + }function o(b) { + n(this, b), this._d = new Date(null != b._d ? b._d.getTime() : NaN), _c === !1 && (_c = !0, a.updateOffset(this), _c = !1); + }function p(a) { + return a instanceof o || null != a && null != a._isAMomentObject; + }function q(a) { + return 0 > a ? Math.ceil(a) : Math.floor(a); + }function r(a) { + var b = +a, + c = 0;return 0 !== b && isFinite(b) && (c = q(b)), c; + }function s(a, b, c) { + var d, + e = Math.min(a.length, b.length), + f = Math.abs(a.length - b.length), + g = 0;for (d = 0; e > d; d++) (c && a[d] !== b[d] || !c && r(a[d]) !== r(b[d])) && g++;return g + f; + }function t(b) { + a.suppressDeprecationWarnings === !1 && "undefined" != typeof console && console.warn && console.warn("Deprecation warning: " + b); + }function u(a, b) { + var c = !0;return g(function () { + return c && (t(a + "\nArguments: " + Array.prototype.slice.call(arguments).join(", ") + "\n" + new Error().stack), c = !1), b.apply(this, arguments); + }, b); + }function v(a, b) { + ad[a] || (t(b), ad[a] = !0); + }function w(a) { + return a instanceof Function || "[object Function]" === Object.prototype.toString.call(a); + }function x(a) { + return "[object Object]" === Object.prototype.toString.call(a); + }function y(a) { + var b, c;for (c in a) b = a[c], w(b) ? this[c] = b : this["_" + c] = b;this._config = a, this._ordinalParseLenient = new RegExp(this._ordinalParse.source + "|" + /\d{1,2}/.source); + }function z(a, b) { + var c, + d = g({}, a);for (c in b) f(b, c) && (x(a[c]) && x(b[c]) ? (d[c] = {}, g(d[c], a[c]), g(d[c], b[c])) : null != b[c] ? d[c] = b[c] : delete d[c]);return d; + }function A(a) { + null != a && this.set(a); + }function B(a) { + return a ? a.toLowerCase().replace("_", "-") : a; + }function C(a) { + for (var b, c, d, e, f = 0; f < a.length;) { + for (e = B(a[f]).split("-"), b = e.length, c = B(a[f + 1]), c = c ? c.split("-") : null; b > 0;) { + if (d = D(e.slice(0, b).join("-"))) return d;if (c && c.length >= b && s(e, c, !0) >= b - 1) break;b--; + }f++; + }return null; + }function D(a) { + var b = null;if (!cd[a] && "undefined" != typeof module && module && module.exports) try { + b = bd._abbr, require("./locale/" + a), E(b); + } catch (c) {}return cd[a]; + }function E(a, b) { + var c;return a && (c = m(b) ? H(a) : F(a, b), c && (bd = c)), bd._abbr; + }function F(a, b) { + return null !== b ? (b.abbr = a, null != cd[a] ? (v("defineLocaleOverride", "use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"), b = z(cd[a]._config, b)) : null != b.parentLocale && (null != cd[b.parentLocale] ? b = z(cd[b.parentLocale]._config, b) : v("parentLocaleUndefined", "specified parentLocale is not defined yet")), cd[a] = new A(b), E(a), cd[a]) : (delete cd[a], null); + }function G(a, b) { + if (null != b) { + var c;null != cd[a] && (b = z(cd[a]._config, b)), c = new A(b), c.parentLocale = cd[a], cd[a] = c, E(a); + } else null != cd[a] && (null != cd[a].parentLocale ? cd[a] = cd[a].parentLocale : null != cd[a] && delete cd[a]);return cd[a]; + }function H(a) { + var b;if ((a && a._locale && a._locale._abbr && (a = a._locale._abbr), !a)) return bd;if (!c(a)) { + if (b = D(a)) return b;a = [a]; + }return C(a); + }function I() { + return Object.keys(cd); + }function J(a, b) { + var c = a.toLowerCase();dd[c] = dd[c + "s"] = dd[b] = a; + }function K(a) { + return "string" == typeof a ? dd[a] || dd[a.toLowerCase()] : void 0; + }function L(a) { + var b, + c, + d = {};for (c in a) f(a, c) && (b = K(c), b && (d[b] = a[c]));return d; + }function M(b, c) { + return function (d) { + return null != d ? (O(this, b, d), a.updateOffset(this, c), this) : N(this, b); + }; + }function N(a, b) { + return a.isValid() ? a._d["get" + (a._isUTC ? "UTC" : "") + b]() : NaN; + }function O(a, b, c) { + a.isValid() && a._d["set" + (a._isUTC ? "UTC" : "") + b](c); + }function P(a, b) { + var c;if ("object" == typeof a) for (c in a) this.set(c, a[c]);else if ((a = K(a), w(this[a]))) return this[a](b);return this; + }function Q(a, b, c) { + var d = "" + Math.abs(a), + e = b - d.length, + f = a >= 0;return (f ? c ? "+" : "" : "-") + Math.pow(10, Math.max(0, e)).toString().substr(1) + d; + }function R(a, b, c, d) { + var e = d;"string" == typeof d && (e = function () { + return this[d](); + }), a && (hd[a] = e), b && (hd[b[0]] = function () { + return Q(e.apply(this, arguments), b[1], b[2]); + }), c && (hd[c] = function () { + return this.localeData().ordinal(e.apply(this, arguments), a); + }); + }function S(a) { + return a.match(/\[[\s\S]/) ? a.replace(/^\[|\]$/g, "") : a.replace(/\\/g, ""); + }function T(a) { + var b, + c, + d = a.match(ed);for (b = 0, c = d.length; c > b; b++) hd[d[b]] ? d[b] = hd[d[b]] : d[b] = S(d[b]);return function (e) { + var f = "";for (b = 0; c > b; b++) f += d[b] instanceof Function ? d[b].call(e, a) : d[b];return f; + }; + }function U(a, b) { + return a.isValid() ? (b = V(b, a.localeData()), gd[b] = gd[b] || T(b), gd[b](a)) : a.localeData().invalidDate(); + }function V(a, b) { + function c(a) { + return b.longDateFormat(a) || a; + }var d = 5;for (fd.lastIndex = 0; d >= 0 && fd.test(a);) a = a.replace(fd, c), fd.lastIndex = 0, d -= 1;return a; + }function W(a, b, c) { + zd[a] = w(b) ? b : function (a, d) { + return a && c ? c : b; + }; + }function X(a, b) { + return f(zd, a) ? zd[a](b._strict, b._locale) : new RegExp(Y(a)); + }function Y(a) { + return Z(a.replace("\\", "").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (a, b, c, d, e) { + return b || c || d || e; + })); + }function Z(a) { + return a.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + }function $(a, b) { + var c, + d = b;for ("string" == typeof a && (a = [a]), "number" == typeof b && (d = function (a, c) { + c[b] = r(a); + }), c = 0; c < a.length; c++) Ad[a[c]] = d; + }function _(a, b) { + $(a, function (a, c, d, e) { + d._w = d._w || {}, b(a, d._w, d, e); + }); + }function aa(a, b, c) { + null != b && f(Ad, a) && Ad[a](b, c._a, c, a); + }function ba(a, b) { + return new Date(Date.UTC(a, b + 1, 0)).getUTCDate(); + }function ca(a, b) { + return c(this._months) ? this._months[a.month()] : this._months[Kd.test(b) ? "format" : "standalone"][a.month()]; + }function da(a, b) { + return c(this._monthsShort) ? this._monthsShort[a.month()] : this._monthsShort[Kd.test(b) ? "format" : "standalone"][a.month()]; + }function ea(a, b, c) { + var d, e, f;for (this._monthsParse || (this._monthsParse = [], this._longMonthsParse = [], this._shortMonthsParse = []), d = 0; 12 > d; d++) { + if ((e = h([2e3, d]), c && !this._longMonthsParse[d] && (this._longMonthsParse[d] = new RegExp("^" + this.months(e, "").replace(".", "") + "$", "i"), this._shortMonthsParse[d] = new RegExp("^" + this.monthsShort(e, "").replace(".", "") + "$", "i")), c || this._monthsParse[d] || (f = "^" + this.months(e, "") + "|^" + this.monthsShort(e, ""), this._monthsParse[d] = new RegExp(f.replace(".", ""), "i")), c && "MMMM" === b && this._longMonthsParse[d].test(a))) return d;if (c && "MMM" === b && this._shortMonthsParse[d].test(a)) return d;if (!c && this._monthsParse[d].test(a)) return d; + } + }function fa(a, b) { + var c;if (!a.isValid()) return a;if ("string" == typeof b) if (/^\d+$/.test(b)) b = r(b);else if ((b = a.localeData().monthsParse(b), "number" != typeof b)) return a;return c = Math.min(a.date(), ba(a.year(), b)), a._d["set" + (a._isUTC ? "UTC" : "") + "Month"](b, c), a; + }function ga(b) { + return null != b ? (fa(this, b), a.updateOffset(this, !0), this) : N(this, "Month"); + }function ha() { + return ba(this.year(), this.month()); + }function ia(a) { + return this._monthsParseExact ? (f(this, "_monthsRegex") || ka.call(this), a ? this._monthsShortStrictRegex : this._monthsShortRegex) : this._monthsShortStrictRegex && a ? this._monthsShortStrictRegex : this._monthsShortRegex; + }function ja(a) { + return this._monthsParseExact ? (f(this, "_monthsRegex") || ka.call(this), a ? this._monthsStrictRegex : this._monthsRegex) : this._monthsStrictRegex && a ? this._monthsStrictRegex : this._monthsRegex; + }function ka() { + function a(a, b) { + return b.length - a.length; + }var b, + c, + d = [], + e = [], + f = [];for (b = 0; 12 > b; b++) c = h([2e3, b]), d.push(this.monthsShort(c, "")), e.push(this.months(c, "")), f.push(this.months(c, "")), f.push(this.monthsShort(c, ""));for (d.sort(a), e.sort(a), f.sort(a), b = 0; 12 > b; b++) d[b] = Z(d[b]), e[b] = Z(e[b]), f[b] = Z(f[b]);this._monthsRegex = new RegExp("^(" + f.join("|") + ")", "i"), this._monthsShortRegex = this._monthsRegex, this._monthsStrictRegex = new RegExp("^(" + e.join("|") + ")$", "i"), this._monthsShortStrictRegex = new RegExp("^(" + d.join("|") + ")$", "i"); + }function la(a) { + var b, + c = a._a;return c && -2 === j(a).overflow && (b = c[Cd] < 0 || c[Cd] > 11 ? Cd : c[Dd] < 1 || c[Dd] > ba(c[Bd], c[Cd]) ? Dd : c[Ed] < 0 || c[Ed] > 24 || 24 === c[Ed] && (0 !== c[Fd] || 0 !== c[Gd] || 0 !== c[Hd]) ? Ed : c[Fd] < 0 || c[Fd] > 59 ? Fd : c[Gd] < 0 || c[Gd] > 59 ? Gd : c[Hd] < 0 || c[Hd] > 999 ? Hd : -1, j(a)._overflowDayOfYear && (Bd > b || b > Dd) && (b = Dd), j(a)._overflowWeeks && -1 === b && (b = Id), j(a)._overflowWeekday && -1 === b && (b = Jd), j(a).overflow = b), a; + }function ma(a) { + var b, + c, + d, + e, + f, + g, + h = a._i, + i = Pd.exec(h) || Qd.exec(h);if (i) { + for (j(a).iso = !0, b = 0, c = Sd.length; c > b; b++) if (Sd[b][1].exec(i[1])) { + e = Sd[b][0], d = Sd[b][2] !== !1;break; + }if (null == e) return void (a._isValid = !1);if (i[3]) { + for (b = 0, c = Td.length; c > b; b++) if (Td[b][1].exec(i[3])) { + f = (i[2] || " ") + Td[b][0];break; + }if (null == f) return void (a._isValid = !1); + }if (!d && null != f) return void (a._isValid = !1);if (i[4]) { + if (!Rd.exec(i[4])) return void (a._isValid = !1);g = "Z"; + }a._f = e + (f || "") + (g || ""), Ba(a); + } else a._isValid = !1; + }function na(b) { + var c = Ud.exec(b._i);return null !== c ? void (b._d = new Date(+c[1])) : (ma(b), void (b._isValid === !1 && (delete b._isValid, a.createFromInputFallback(b)))); + }function oa(a, b, c, d, e, f, g) { + var h = new Date(a, b, c, d, e, f, g);return 100 > a && a >= 0 && isFinite(h.getFullYear()) && h.setFullYear(a), h; + }function pa(a) { + var b = new Date(Date.UTC.apply(null, arguments));return 100 > a && a >= 0 && isFinite(b.getUTCFullYear()) && b.setUTCFullYear(a), b; + }function qa(a) { + return ra(a) ? 366 : 365; + }function ra(a) { + return a % 4 === 0 && a % 100 !== 0 || a % 400 === 0; + }function sa() { + return ra(this.year()); + }function ta(a, b, c) { + var d = 7 + b - c, + e = (7 + pa(a, 0, d).getUTCDay() - b) % 7;return -e + d - 1; + }function ua(a, b, c, d, e) { + var f, + g, + h = (7 + c - d) % 7, + i = ta(a, d, e), + j = 1 + 7 * (b - 1) + h + i;return 0 >= j ? (f = a - 1, g = qa(f) + j) : j > qa(a) ? (f = a + 1, g = j - qa(a)) : (f = a, g = j), { year: f, dayOfYear: g }; + }function va(a, b, c) { + var d, + e, + f = ta(a.year(), b, c), + g = Math.floor((a.dayOfYear() - f - 1) / 7) + 1;return 1 > g ? (e = a.year() - 1, d = g + wa(e, b, c)) : g > wa(a.year(), b, c) ? (d = g - wa(a.year(), b, c), e = a.year() + 1) : (e = a.year(), d = g), { week: d, year: e }; + }function wa(a, b, c) { + var d = ta(a, b, c), + e = ta(a + 1, b, c);return (qa(a) - d + e) / 7; + }function xa(a, b, c) { + return null != a ? a : null != b ? b : c; + }function ya(b) { + var c = new Date(a.now());return b._useUTC ? [c.getUTCFullYear(), c.getUTCMonth(), c.getUTCDate()] : [c.getFullYear(), c.getMonth(), c.getDate()]; + }function za(a) { + var b, + c, + d, + e, + f = [];if (!a._d) { + for (d = ya(a), a._w && null == a._a[Dd] && null == a._a[Cd] && Aa(a), a._dayOfYear && (e = xa(a._a[Bd], d[Bd]), a._dayOfYear > qa(e) && (j(a)._overflowDayOfYear = !0), c = pa(e, 0, a._dayOfYear), a._a[Cd] = c.getUTCMonth(), a._a[Dd] = c.getUTCDate()), b = 0; 3 > b && null == a._a[b]; ++b) a._a[b] = f[b] = d[b];for (; 7 > b; b++) a._a[b] = f[b] = null == a._a[b] ? 2 === b ? 1 : 0 : a._a[b];24 === a._a[Ed] && 0 === a._a[Fd] && 0 === a._a[Gd] && 0 === a._a[Hd] && (a._nextDay = !0, a._a[Ed] = 0), a._d = (a._useUTC ? pa : oa).apply(null, f), null != a._tzm && a._d.setUTCMinutes(a._d.getUTCMinutes() - a._tzm), a._nextDay && (a._a[Ed] = 24); + } + }function Aa(a) { + var b, c, d, e, f, g, h, i;b = a._w, null != b.GG || null != b.W || null != b.E ? (f = 1, g = 4, c = xa(b.GG, a._a[Bd], va(Ja(), 1, 4).year), d = xa(b.W, 1), e = xa(b.E, 1), (1 > e || e > 7) && (i = !0)) : (f = a._locale._week.dow, g = a._locale._week.doy, c = xa(b.gg, a._a[Bd], va(Ja(), f, g).year), d = xa(b.w, 1), null != b.d ? (e = b.d, (0 > e || e > 6) && (i = !0)) : null != b.e ? (e = b.e + f, (b.e < 0 || b.e > 6) && (i = !0)) : e = f), 1 > d || d > wa(c, f, g) ? j(a)._overflowWeeks = !0 : null != i ? j(a)._overflowWeekday = !0 : (h = ua(c, d, e, f, g), a._a[Bd] = h.year, a._dayOfYear = h.dayOfYear); + }function Ba(b) { + if (b._f === a.ISO_8601) return void ma(b);b._a = [], j(b).empty = !0;var c, + d, + e, + f, + g, + h = "" + b._i, + i = h.length, + k = 0;for (e = V(b._f, b._locale).match(ed) || [], c = 0; c < e.length; c++) f = e[c], d = (h.match(X(f, b)) || [])[0], d && (g = h.substr(0, h.indexOf(d)), g.length > 0 && j(b).unusedInput.push(g), h = h.slice(h.indexOf(d) + d.length), k += d.length), hd[f] ? (d ? j(b).empty = !1 : j(b).unusedTokens.push(f), aa(f, d, b)) : b._strict && !d && j(b).unusedTokens.push(f);j(b).charsLeftOver = i - k, h.length > 0 && j(b).unusedInput.push(h), j(b).bigHour === !0 && b._a[Ed] <= 12 && b._a[Ed] > 0 && (j(b).bigHour = void 0), b._a[Ed] = Ca(b._locale, b._a[Ed], b._meridiem), za(b), la(b); + }function Ca(a, b, c) { + var d;return null == c ? b : null != a.meridiemHour ? a.meridiemHour(b, c) : null != a.isPM ? (d = a.isPM(c), d && 12 > b && (b += 12), d || 12 !== b || (b = 0), b) : b; + }function Da(a) { + var b, c, d, e, f;if (0 === a._f.length) return j(a).invalidFormat = !0, void (a._d = new Date(NaN));for (e = 0; e < a._f.length; e++) f = 0, b = n({}, a), null != a._useUTC && (b._useUTC = a._useUTC), b._f = a._f[e], Ba(b), k(b) && (f += j(b).charsLeftOver, f += 10 * j(b).unusedTokens.length, j(b).score = f, (null == d || d > f) && (d = f, c = b));g(a, c || b); + }function Ea(a) { + if (!a._d) { + var b = L(a._i);a._a = e([b.year, b.month, b.day || b.date, b.hour, b.minute, b.second, b.millisecond], function (a) { + return a && parseInt(a, 10); + }), za(a); + } + }function Fa(a) { + var b = new o(la(Ga(a)));return b._nextDay && (b.add(1, "d"), b._nextDay = void 0), b; + }function Ga(a) { + var b = a._i, + e = a._f;return a._locale = a._locale || H(a._l), null === b || void 0 === e && "" === b ? l({ nullInput: !0 }) : ("string" == typeof b && (a._i = b = a._locale.preparse(b)), p(b) ? new o(la(b)) : (c(e) ? Da(a) : e ? Ba(a) : d(b) ? a._d = b : Ha(a), k(a) || (a._d = null), a)); + }function Ha(b) { + var f = b._i;void 0 === f ? b._d = new Date(a.now()) : d(f) ? b._d = new Date(+f) : "string" == typeof f ? na(b) : c(f) ? (b._a = e(f.slice(0), function (a) { + return parseInt(a, 10); + }), za(b)) : "object" == typeof f ? Ea(b) : "number" == typeof f ? b._d = new Date(f) : a.createFromInputFallback(b); + }function Ia(a, b, c, d, e) { + var f = {};return "boolean" == typeof c && (d = c, c = void 0), f._isAMomentObject = !0, f._useUTC = f._isUTC = e, f._l = c, f._i = a, f._f = b, f._strict = d, Fa(f); + }function Ja(a, b, c, d) { + return Ia(a, b, c, d, !1); + }function Ka(a, b) { + var d, e;if ((1 === b.length && c(b[0]) && (b = b[0]), !b.length)) return Ja();for (d = b[0], e = 1; e < b.length; ++e) (!b[e].isValid() || b[e][a](d)) && (d = b[e]);return d; + }function La() { + var a = [].slice.call(arguments, 0);return Ka("isBefore", a); + }function Ma() { + var a = [].slice.call(arguments, 0);return Ka("isAfter", a); + }function Na(a) { + var b = L(a), + c = b.year || 0, + d = b.quarter || 0, + e = b.month || 0, + f = b.week || 0, + g = b.day || 0, + h = b.hour || 0, + i = b.minute || 0, + j = b.second || 0, + k = b.millisecond || 0;this._milliseconds = +k + 1e3 * j + 6e4 * i + 36e5 * h, this._days = +g + 7 * f, this._months = +e + 3 * d + 12 * c, this._data = {}, this._locale = H(), this._bubble(); + }function Oa(a) { + return a instanceof Na; + }function Pa(a, b) { + R(a, 0, 0, function () { + var a = this.utcOffset(), + c = "+";return 0 > a && (a = -a, c = "-"), c + Q(~ ~(a / 60), 2) + b + Q(~ ~a % 60, 2); + }); + }function Qa(a, b) { + var c = (b || "").match(a) || [], + d = c[c.length - 1] || [], + e = (d + "").match(Zd) || ["-", 0, 0], + f = +(60 * e[1]) + r(e[2]);return "+" === e[0] ? f : -f; + }function Ra(b, c) { + var e, f;return c._isUTC ? (e = c.clone(), f = (p(b) || d(b) ? +b : +Ja(b)) - +e, e._d.setTime(+e._d + f), a.updateOffset(e, !1), e) : Ja(b).local(); + }function Sa(a) { + return 15 * -Math.round(a._d.getTimezoneOffset() / 15); + }function Ta(b, c) { + var d, + e = this._offset || 0;return this.isValid() ? null != b ? ("string" == typeof b ? b = Qa(wd, b) : Math.abs(b) < 16 && (b = 60 * b), !this._isUTC && c && (d = Sa(this)), this._offset = b, this._isUTC = !0, null != d && this.add(d, "m"), e !== b && (!c || this._changeInProgress ? ib(this, cb(b - e, "m"), 1, !1) : this._changeInProgress || (this._changeInProgress = !0, a.updateOffset(this, !0), this._changeInProgress = null)), this) : this._isUTC ? e : Sa(this) : null != b ? this : NaN; + }function Ua(a, b) { + return null != a ? ("string" != typeof a && (a = -a), this.utcOffset(a, b), this) : -this.utcOffset(); + }function Va(a) { + return this.utcOffset(0, a); + }function Wa(a) { + return this._isUTC && (this.utcOffset(0, a), this._isUTC = !1, a && this.subtract(Sa(this), "m")), this; + }function Xa() { + return this._tzm ? this.utcOffset(this._tzm) : "string" == typeof this._i && this.utcOffset(Qa(vd, this._i)), this; + }function Ya(a) { + return this.isValid() ? (a = a ? Ja(a).utcOffset() : 0, (this.utcOffset() - a) % 60 === 0) : !1; + }function Za() { + return this.utcOffset() > this.clone().month(0).utcOffset() || this.utcOffset() > this.clone().month(5).utcOffset(); + }function $a() { + if (!m(this._isDSTShifted)) return this._isDSTShifted;var a = {};if ((n(a, this), a = Ga(a), a._a)) { + var b = a._isUTC ? h(a._a) : Ja(a._a);this._isDSTShifted = this.isValid() && s(a._a, b.toArray()) > 0; + } else this._isDSTShifted = !1;return this._isDSTShifted; + }function _a() { + return this.isValid() ? !this._isUTC : !1; + }function ab() { + return this.isValid() ? this._isUTC : !1; + }function bb() { + return this.isValid() ? this._isUTC && 0 === this._offset : !1; + }function cb(a, b) { + var c, + d, + e, + g = a, + h = null;return Oa(a) ? g = { ms: a._milliseconds, d: a._days, M: a._months } : "number" == typeof a ? (g = {}, b ? g[b] = a : g.milliseconds = a) : (h = $d.exec(a)) ? (c = "-" === h[1] ? -1 : 1, g = { y: 0, d: r(h[Dd]) * c, h: r(h[Ed]) * c, m: r(h[Fd]) * c, s: r(h[Gd]) * c, ms: r(h[Hd]) * c }) : (h = _d.exec(a)) ? (c = "-" === h[1] ? -1 : 1, g = { y: db(h[2], c), M: db(h[3], c), w: db(h[4], c), d: db(h[5], c), h: db(h[6], c), m: db(h[7], c), s: db(h[8], c) }) : null == g ? g = {} : "object" == typeof g && ("from" in g || "to" in g) && (e = fb(Ja(g.from), Ja(g.to)), g = {}, g.ms = e.milliseconds, g.M = e.months), d = new Na(g), Oa(a) && f(a, "_locale") && (d._locale = a._locale), d; + }function db(a, b) { + var c = a && parseFloat(a.replace(",", "."));return (isNaN(c) ? 0 : c) * b; + }function eb(a, b) { + var c = { milliseconds: 0, months: 0 };return c.months = b.month() - a.month() + 12 * (b.year() - a.year()), a.clone().add(c.months, "M").isAfter(b) && --c.months, c.milliseconds = +b - +a.clone().add(c.months, "M"), c; + }function fb(a, b) { + var c;return a.isValid() && b.isValid() ? (b = Ra(b, a), a.isBefore(b) ? c = eb(a, b) : (c = eb(b, a), c.milliseconds = -c.milliseconds, c.months = -c.months), c) : { milliseconds: 0, months: 0 }; + }function gb(a) { + return 0 > a ? -1 * Math.round(-1 * a) : Math.round(a); + }function hb(a, b) { + return function (c, d) { + var e, f;return null === d || isNaN(+d) || (v(b, "moment()." + b + "(period, number) is deprecated. Please use moment()." + b + "(number, period)."), f = c, c = d, d = f), c = "string" == typeof c ? +c : c, e = cb(c, d), ib(this, e, a), this; + }; + }function ib(b, c, d, e) { + var f = c._milliseconds, + g = gb(c._days), + h = gb(c._months);b.isValid() && (e = null == e ? !0 : e, f && b._d.setTime(+b._d + f * d), g && O(b, "Date", N(b, "Date") + g * d), h && fa(b, N(b, "Month") + h * d), e && a.updateOffset(b, g || h)); + }function jb(a, b) { + var c = a || Ja(), + d = Ra(c, this).startOf("day"), + e = this.diff(d, "days", !0), + f = -6 > e ? "sameElse" : -1 > e ? "lastWeek" : 0 > e ? "lastDay" : 1 > e ? "sameDay" : 2 > e ? "nextDay" : 7 > e ? "nextWeek" : "sameElse", + g = b && (w(b[f]) ? b[f]() : b[f]);return this.format(g || this.localeData().calendar(f, this, Ja(c))); + }function kb() { + return new o(this); + }function lb(a, b) { + var c = p(a) ? a : Ja(a);return this.isValid() && c.isValid() ? (b = K(m(b) ? "millisecond" : b), "millisecond" === b ? +this > +c : +c < +this.clone().startOf(b)) : !1; + }function mb(a, b) { + var c = p(a) ? a : Ja(a);return this.isValid() && c.isValid() ? (b = K(m(b) ? "millisecond" : b), "millisecond" === b ? +c > +this : +this.clone().endOf(b) < +c) : !1; + }function nb(a, b, c) { + return this.isAfter(a, c) && this.isBefore(b, c); + }function ob(a, b) { + var c, + d = p(a) ? a : Ja(a);return this.isValid() && d.isValid() ? (b = K(b || "millisecond"), "millisecond" === b ? +this === +d : (c = +d, +this.clone().startOf(b) <= c && c <= +this.clone().endOf(b))) : !1; + }function pb(a, b) { + return this.isSame(a, b) || this.isAfter(a, b); + }function qb(a, b) { + return this.isSame(a, b) || this.isBefore(a, b); + }function rb(a, b, c) { + var d, e, f, g;return this.isValid() ? (d = Ra(a, this), d.isValid() ? (e = 6e4 * (d.utcOffset() - this.utcOffset()), b = K(b), "year" === b || "month" === b || "quarter" === b ? (g = sb(this, d), "quarter" === b ? g /= 3 : "year" === b && (g /= 12)) : (f = this - d, g = "second" === b ? f / 1e3 : "minute" === b ? f / 6e4 : "hour" === b ? f / 36e5 : "day" === b ? (f - e) / 864e5 : "week" === b ? (f - e) / 6048e5 : f), c ? g : q(g)) : NaN) : NaN; + }function sb(a, b) { + var c, + d, + e = 12 * (b.year() - a.year()) + (b.month() - a.month()), + f = a.clone().add(e, "months");return 0 > b - f ? (c = a.clone().add(e - 1, "months"), d = (b - f) / (f - c)) : (c = a.clone().add(e + 1, "months"), d = (b - f) / (c - f)), -(e + d); + }function tb() { + return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + }function ub() { + var a = this.clone().utc();return 0 < a.year() && a.year() <= 9999 ? w(Date.prototype.toISOString) ? this.toDate().toISOString() : U(a, "YYYY-MM-DD[T]HH:mm:ss.SSS[Z]") : U(a, "YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]"); + }function vb(b) { + var c = U(this, b || a.defaultFormat);return this.localeData().postformat(c); + }function wb(a, b) { + return this.isValid() && (p(a) && a.isValid() || Ja(a).isValid()) ? cb({ to: this, from: a }).locale(this.locale()).humanize(!b) : this.localeData().invalidDate(); + }function xb(a) { + return this.from(Ja(), a); + }function yb(a, b) { + return this.isValid() && (p(a) && a.isValid() || Ja(a).isValid()) ? cb({ from: this, to: a }).locale(this.locale()).humanize(!b) : this.localeData().invalidDate(); + }function zb(a) { + return this.to(Ja(), a); + }function Ab(a) { + var b;return void 0 === a ? this._locale._abbr : (b = H(a), null != b && (this._locale = b), this); + }function Bb() { + return this._locale; + }function Cb(a) { + switch (a = K(a)) {case "year": + this.month(0);case "quarter":case "month": + this.date(1);case "week":case "isoWeek":case "day": + this.hours(0);case "hour": + this.minutes(0);case "minute": + this.seconds(0);case "second": + this.milliseconds(0);}return "week" === a && this.weekday(0), "isoWeek" === a && this.isoWeekday(1), "quarter" === a && this.month(3 * Math.floor(this.month() / 3)), this; + }function Db(a) { + return a = K(a), void 0 === a || "millisecond" === a ? this : this.startOf(a).add(1, "isoWeek" === a ? "week" : a).subtract(1, "ms"); + }function Eb() { + return +this._d - 6e4 * (this._offset || 0); + }function Fb() { + return Math.floor(+this / 1e3); + }function Gb() { + return this._offset ? new Date(+this) : this._d; + }function Hb() { + var a = this;return [a.year(), a.month(), a.date(), a.hour(), a.minute(), a.second(), a.millisecond()]; + }function Ib() { + var a = this;return { years: a.year(), months: a.month(), date: a.date(), hours: a.hours(), minutes: a.minutes(), seconds: a.seconds(), milliseconds: a.milliseconds() }; + }function Jb() { + return this.isValid() ? this.toISOString() : null; + }function Kb() { + return k(this); + }function Lb() { + return g({}, j(this)); + }function Mb() { + return j(this).overflow; + }function Nb() { + return { input: this._i, format: this._f, locale: this._locale, isUTC: this._isUTC, strict: this._strict }; + }function Ob(a, b) { + R(0, [a, a.length], 0, b); + }function Pb(a) { + return Tb.call(this, a, this.week(), this.weekday(), this.localeData()._week.dow, this.localeData()._week.doy); + }function Qb(a) { + return Tb.call(this, a, this.isoWeek(), this.isoWeekday(), 1, 4); + }function Rb() { + return wa(this.year(), 1, 4); + }function Sb() { + var a = this.localeData()._week;return wa(this.year(), a.dow, a.doy); + }function Tb(a, b, c, d, e) { + var f;return null == a ? va(this, d, e).year : (f = wa(a, d, e), b > f && (b = f), Ub.call(this, a, b, c, d, e)); + }function Ub(a, b, c, d, e) { + var f = ua(a, b, c, d, e), + g = pa(f.year, 0, f.dayOfYear);return this.year(g.getUTCFullYear()), this.month(g.getUTCMonth()), this.date(g.getUTCDate()), this; + }function Vb(a) { + return null == a ? Math.ceil((this.month() + 1) / 3) : this.month(3 * (a - 1) + this.month() % 3); + }function Wb(a) { + return va(a, this._week.dow, this._week.doy).week; + }function Xb() { + return this._week.dow; + }function Yb() { + return this._week.doy; + }function Zb(a) { + var b = this.localeData().week(this);return null == a ? b : this.add(7 * (a - b), "d"); + }function $b(a) { + var b = va(this, 1, 4).week;return null == a ? b : this.add(7 * (a - b), "d"); + }function _b(a, b) { + return "string" != typeof a ? a : isNaN(a) ? (a = b.weekdaysParse(a), "number" == typeof a ? a : null) : parseInt(a, 10); + }function ac(a, b) { + return c(this._weekdays) ? this._weekdays[a.day()] : this._weekdays[this._weekdays.isFormat.test(b) ? "format" : "standalone"][a.day()]; + }function bc(a) { + return this._weekdaysShort[a.day()]; + }function cc(a) { + return this._weekdaysMin[a.day()]; + }function dc(a, b, c) { + var d, e, f;for (this._weekdaysParse || (this._weekdaysParse = [], this._minWeekdaysParse = [], this._shortWeekdaysParse = [], this._fullWeekdaysParse = []), d = 0; 7 > d; d++) { + if ((e = Ja([2e3, 1]).day(d), c && !this._fullWeekdaysParse[d] && (this._fullWeekdaysParse[d] = new RegExp("^" + this.weekdays(e, "").replace(".", ".?") + "$", "i"), this._shortWeekdaysParse[d] = new RegExp("^" + this.weekdaysShort(e, "").replace(".", ".?") + "$", "i"), this._minWeekdaysParse[d] = new RegExp("^" + this.weekdaysMin(e, "").replace(".", ".?") + "$", "i")), this._weekdaysParse[d] || (f = "^" + this.weekdays(e, "") + "|^" + this.weekdaysShort(e, "") + "|^" + this.weekdaysMin(e, ""), this._weekdaysParse[d] = new RegExp(f.replace(".", ""), "i")), c && "dddd" === b && this._fullWeekdaysParse[d].test(a))) return d;if (c && "ddd" === b && this._shortWeekdaysParse[d].test(a)) return d;if (c && "dd" === b && this._minWeekdaysParse[d].test(a)) return d;if (!c && this._weekdaysParse[d].test(a)) return d; + } + }function ec(a) { + if (!this.isValid()) return null != a ? this : NaN;var b = this._isUTC ? this._d.getUTCDay() : this._d.getDay();return null != a ? (a = _b(a, this.localeData()), this.add(a - b, "d")) : b; + }function fc(a) { + if (!this.isValid()) return null != a ? this : NaN;var b = (this.day() + 7 - this.localeData()._week.dow) % 7;return null == a ? b : this.add(a - b, "d"); + }function gc(a) { + return this.isValid() ? null == a ? this.day() || 7 : this.day(this.day() % 7 ? a : a - 7) : null != a ? this : NaN; + }function hc(a) { + var b = Math.round((this.clone().startOf("day") - this.clone().startOf("year")) / 864e5) + 1;return null == a ? b : this.add(a - b, "d"); + }function ic() { + return this.hours() % 12 || 12; + }function jc(a, b) { + R(a, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), b); + }); + }function kc(a, b) { + return b._meridiemParse; + }function lc(a) { + return "p" === (a + "").toLowerCase().charAt(0); + }function mc(a, b, c) { + return a > 11 ? c ? "pm" : "PM" : c ? "am" : "AM"; + }function nc(a, b) { + b[Hd] = r(1e3 * ("0." + a)); + }function oc() { + return this._isUTC ? "UTC" : ""; + }function pc() { + return this._isUTC ? "Coordinated Universal Time" : ""; + }function qc(a) { + return Ja(1e3 * a); + }function rc() { + return Ja.apply(null, arguments).parseZone(); + }function sc(a, b, c) { + var d = this._calendar[a];return w(d) ? d.call(b, c) : d; + }function tc(a) { + var b = this._longDateFormat[a], + c = this._longDateFormat[a.toUpperCase()];return b || !c ? b : (this._longDateFormat[a] = c.replace(/MMMM|MM|DD|dddd/g, function (a) { + return a.slice(1); + }), this._longDateFormat[a]); + }function uc() { + return this._invalidDate; + }function vc(a) { + return this._ordinal.replace("%d", a); + }function wc(a) { + return a; + }function xc(a, b, c, d) { + var e = this._relativeTime[c];return w(e) ? e(a, b, c, d) : e.replace(/%d/i, a); + }function yc(a, b) { + var c = this._relativeTime[a > 0 ? "future" : "past"];return w(c) ? c(b) : c.replace(/%s/i, b); + }function zc(a, b, c, d) { + var e = H(), + f = h().set(d, b);return e[c](f, a); + }function Ac(a, b, c, d, e) { + if (("number" == typeof a && (b = a, a = void 0), a = a || "", null != b)) return zc(a, b, c, e);var f, + g = [];for (f = 0; d > f; f++) g[f] = zc(a, f, c, e);return g; + }function Bc(a, b) { + return Ac(a, b, "months", 12, "month"); + }function Cc(a, b) { + return Ac(a, b, "monthsShort", 12, "month"); + }function Dc(a, b) { + return Ac(a, b, "weekdays", 7, "day"); + }function Ec(a, b) { + return Ac(a, b, "weekdaysShort", 7, "day"); + }function Fc(a, b) { + return Ac(a, b, "weekdaysMin", 7, "day"); + }function Gc() { + var a = this._data;return this._milliseconds = xe(this._milliseconds), this._days = xe(this._days), this._months = xe(this._months), a.milliseconds = xe(a.milliseconds), a.seconds = xe(a.seconds), a.minutes = xe(a.minutes), a.hours = xe(a.hours), a.months = xe(a.months), a.years = xe(a.years), this; + }function Hc(a, b, c, d) { + var e = cb(b, c);return a._milliseconds += d * e._milliseconds, a._days += d * e._days, a._months += d * e._months, a._bubble(); + }function Ic(a, b) { + return Hc(this, a, b, 1); + }function Jc(a, b) { + return Hc(this, a, b, -1); + }function Kc(a) { + return 0 > a ? Math.floor(a) : Math.ceil(a); + }function Lc() { + var a, + b, + c, + d, + e, + f = this._milliseconds, + g = this._days, + h = this._months, + i = this._data;return f >= 0 && g >= 0 && h >= 0 || 0 >= f && 0 >= g && 0 >= h || (f += 864e5 * Kc(Nc(h) + g), g = 0, h = 0), i.milliseconds = f % 1e3, a = q(f / 1e3), i.seconds = a % 60, b = q(a / 60), i.minutes = b % 60, c = q(b / 60), i.hours = c % 24, g += q(c / 24), e = q(Mc(g)), h += e, g -= Kc(Nc(e)), d = q(h / 12), h %= 12, i.days = g, i.months = h, i.years = d, this; + }function Mc(a) { + return 4800 * a / 146097; + }function Nc(a) { + return 146097 * a / 4800; + }function Oc(a) { + var b, + c, + d = this._milliseconds;if ((a = K(a), "month" === a || "year" === a)) return b = this._days + d / 864e5, c = this._months + Mc(b), "month" === a ? c : c / 12;switch ((b = this._days + Math.round(Nc(this._months)), a)) {case "week": + return b / 7 + d / 6048e5;case "day": + return b + d / 864e5;case "hour": + return 24 * b + d / 36e5;case "minute": + return 1440 * b + d / 6e4;case "second": + return 86400 * b + d / 1e3;case "millisecond": + return Math.floor(864e5 * b) + d;default: + throw new Error("Unknown unit " + a);} + }function Pc() { + return this._milliseconds + 864e5 * this._days + this._months % 12 * 2592e6 + 31536e6 * r(this._months / 12); + }function Qc(a) { + return function () { + return this.as(a); + }; + }function Rc(a) { + return a = K(a), this[a + "s"](); + }function Sc(a) { + return function () { + return this._data[a]; + }; + }function Tc() { + return q(this.days() / 7); + }function Uc(a, b, c, d, e) { + return e.relativeTime(b || 1, !!c, a, d); + }function Vc(a, b, c) { + var d = cb(a).abs(), + e = Ne(d.as("s")), + f = Ne(d.as("m")), + g = Ne(d.as("h")), + h = Ne(d.as("d")), + i = Ne(d.as("M")), + j = Ne(d.as("y")), + k = e < Oe.s && ["s", e] || 1 >= f && ["m"] || f < Oe.m && ["mm", f] || 1 >= g && ["h"] || g < Oe.h && ["hh", g] || 1 >= h && ["d"] || h < Oe.d && ["dd", h] || 1 >= i && ["M"] || i < Oe.M && ["MM", i] || 1 >= j && ["y"] || ["yy", j];return k[2] = b, k[3] = +a > 0, k[4] = c, Uc.apply(null, k); + }function Wc(a, b) { + return void 0 === Oe[a] ? !1 : void 0 === b ? Oe[a] : (Oe[a] = b, !0); + }function Xc(a) { + var b = this.localeData(), + c = Vc(this, !a, b);return a && (c = b.pastFuture(+this, c)), b.postformat(c); + }function Yc() { + var a, + b, + c, + d = Pe(this._milliseconds) / 1e3, + e = Pe(this._days), + f = Pe(this._months);a = q(d / 60), b = q(a / 60), d %= 60, a %= 60, c = q(f / 12), f %= 12;var g = c, + h = f, + i = e, + j = b, + k = a, + l = d, + m = this.asSeconds();return m ? (0 > m ? "-" : "") + "P" + (g ? g + "Y" : "") + (h ? h + "M" : "") + (i ? i + "D" : "") + (j || k || l ? "T" : "") + (j ? j + "H" : "") + (k ? k + "M" : "") + (l ? l + "S" : "") : "P0D"; + }var Zc, + $c = a.momentProperties = [], + _c = !1, + ad = {};a.suppressDeprecationWarnings = !1;var bd, + cd = {}, + dd = {}, + ed = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, + fd = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, + gd = {}, + hd = {}, + id = /\d/, + jd = /\d\d/, + kd = /\d{3}/, + ld = /\d{4}/, + md = /[+-]?\d{6}/, + nd = /\d\d?/, + od = /\d\d\d\d?/, + pd = /\d\d\d\d\d\d?/, + qd = /\d{1,3}/, + rd = /\d{1,4}/, + sd = /[+-]?\d{1,6}/, + td = /\d+/, + ud = /[+-]?\d+/, + vd = /Z|[+-]\d\d:?\d\d/gi, + wd = /Z|[+-]\d\d(?::?\d\d)?/gi, + xd = /[+-]?\d+(\.\d{1,3})?/, + yd = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, + zd = {}, + Ad = {}, + Bd = 0, + Cd = 1, + Dd = 2, + Ed = 3, + Fd = 4, + Gd = 5, + Hd = 6, + Id = 7, + Jd = 8;R("M", ["MM", 2], "Mo", function () { + return this.month() + 1; + }), R("MMM", 0, 0, function (a) { + return this.localeData().monthsShort(this, a); + }), R("MMMM", 0, 0, function (a) { + return this.localeData().months(this, a); + }), J("month", "M"), W("M", nd), W("MM", nd, jd), W("MMM", function (a, b) { + return b.monthsShortRegex(a); + }), W("MMMM", function (a, b) { + return b.monthsRegex(a); + }), $(["M", "MM"], function (a, b) { + b[Cd] = r(a) - 1; + }), $(["MMM", "MMMM"], function (a, b, c, d) { + var e = c._locale.monthsParse(a, d, c._strict);null != e ? b[Cd] = e : j(c).invalidMonth = a; + });var Kd = /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/, + Ld = "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), + Md = "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), + Nd = yd, + Od = yd, + Pd = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/, + Qd = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/, + Rd = /Z|[+-]\d\d(?::?\d\d)?/, + Sd = [["YYYYYY-MM-DD", /[+-]\d{6}-\d\d-\d\d/], ["YYYY-MM-DD", /\d{4}-\d\d-\d\d/], ["GGGG-[W]WW-E", /\d{4}-W\d\d-\d/], ["GGGG-[W]WW", /\d{4}-W\d\d/, !1], ["YYYY-DDD", /\d{4}-\d{3}/], ["YYYY-MM", /\d{4}-\d\d/, !1], ["YYYYYYMMDD", /[+-]\d{10}/], ["YYYYMMDD", /\d{8}/], ["GGGG[W]WWE", /\d{4}W\d{3}/], ["GGGG[W]WW", /\d{4}W\d{2}/, !1], ["YYYYDDD", /\d{7}/]], + Td = [["HH:mm:ss.SSSS", /\d\d:\d\d:\d\d\.\d+/], ["HH:mm:ss,SSSS", /\d\d:\d\d:\d\d,\d+/], ["HH:mm:ss", /\d\d:\d\d:\d\d/], ["HH:mm", /\d\d:\d\d/], ["HHmmss.SSSS", /\d\d\d\d\d\d\.\d+/], ["HHmmss,SSSS", /\d\d\d\d\d\d,\d+/], ["HHmmss", /\d\d\d\d\d\d/], ["HHmm", /\d\d\d\d/], ["HH", /\d\d/]], + Ud = /^\/?Date\((\-?\d+)/i;a.createFromInputFallback = u("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.", function (a) { + a._d = new Date(a._i + (a._useUTC ? " UTC" : "")); + }), R("Y", 0, 0, function () { + var a = this.year();return 9999 >= a ? "" + a : "+" + a; + }), R(0, ["YY", 2], 0, function () { + return this.year() % 100; + }), R(0, ["YYYY", 4], 0, "year"), R(0, ["YYYYY", 5], 0, "year"), R(0, ["YYYYYY", 6, !0], 0, "year"), J("year", "y"), W("Y", ud), W("YY", nd, jd), W("YYYY", rd, ld), W("YYYYY", sd, md), W("YYYYYY", sd, md), $(["YYYYY", "YYYYYY"], Bd), $("YYYY", function (b, c) { + c[Bd] = 2 === b.length ? a.parseTwoDigitYear(b) : r(b); + }), $("YY", function (b, c) { + c[Bd] = a.parseTwoDigitYear(b); + }), $("Y", function (a, b) { + b[Bd] = parseInt(a, 10); + }), a.parseTwoDigitYear = function (a) { + return r(a) + (r(a) > 68 ? 1900 : 2e3); + };var Vd = M("FullYear", !1);a.ISO_8601 = function () {};var Wd = u("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548", function () { + var a = Ja.apply(null, arguments);return this.isValid() && a.isValid() ? this > a ? this : a : l(); + }), + Xd = u("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548", function () { + var a = Ja.apply(null, arguments);return this.isValid() && a.isValid() ? a > this ? this : a : l(); + }), + Yd = function Yd() { + return Date.now ? Date.now() : +new Date(); + };Pa("Z", ":"), Pa("ZZ", ""), W("Z", wd), W("ZZ", wd), $(["Z", "ZZ"], function (a, b, c) { + c._useUTC = !0, c._tzm = Qa(wd, a); + });var Zd = /([\+\-]|\d\d)/gi;a.updateOffset = function () {};var $d = /^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/, + _d = /^(-)?P(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)W)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?$/;cb.fn = Na.prototype;var ae = hb(1, "add"), + be = hb(-1, "subtract");a.defaultFormat = "YYYY-MM-DDTHH:mm:ssZ";var ce = u("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.", function (a) { + return void 0 === a ? this.localeData() : this.locale(a); + });R(0, ["gg", 2], 0, function () { + return this.weekYear() % 100; + }), R(0, ["GG", 2], 0, function () { + return this.isoWeekYear() % 100; + }), Ob("gggg", "weekYear"), Ob("ggggg", "weekYear"), Ob("GGGG", "isoWeekYear"), Ob("GGGGG", "isoWeekYear"), J("weekYear", "gg"), J("isoWeekYear", "GG"), W("G", ud), W("g", ud), W("GG", nd, jd), W("gg", nd, jd), W("GGGG", rd, ld), W("gggg", rd, ld), W("GGGGG", sd, md), W("ggggg", sd, md), _(["gggg", "ggggg", "GGGG", "GGGGG"], function (a, b, c, d) { + b[d.substr(0, 2)] = r(a); + }), _(["gg", "GG"], function (b, c, d, e) { + c[e] = a.parseTwoDigitYear(b); + }), R("Q", 0, "Qo", "quarter"), J("quarter", "Q"), W("Q", id), $("Q", function (a, b) { + b[Cd] = 3 * (r(a) - 1); + }), R("w", ["ww", 2], "wo", "week"), R("W", ["WW", 2], "Wo", "isoWeek"), J("week", "w"), J("isoWeek", "W"), W("w", nd), W("ww", nd, jd), W("W", nd), W("WW", nd, jd), _(["w", "ww", "W", "WW"], function (a, b, c, d) { + b[d.substr(0, 1)] = r(a); + });var de = { dow: 0, doy: 6 };R("D", ["DD", 2], "Do", "date"), J("date", "D"), W("D", nd), W("DD", nd, jd), W("Do", function (a, b) { + return a ? b._ordinalParse : b._ordinalParseLenient; + }), $(["D", "DD"], Dd), $("Do", function (a, b) { + b[Dd] = r(a.match(nd)[0], 10); + });var ee = M("Date", !0);R("d", 0, "do", "day"), R("dd", 0, 0, function (a) { + return this.localeData().weekdaysMin(this, a); + }), R("ddd", 0, 0, function (a) { + return this.localeData().weekdaysShort(this, a); + }), R("dddd", 0, 0, function (a) { + return this.localeData().weekdays(this, a); + }), R("e", 0, 0, "weekday"), R("E", 0, 0, "isoWeekday"), J("day", "d"), J("weekday", "e"), J("isoWeekday", "E"), W("d", nd), W("e", nd), W("E", nd), W("dd", yd), W("ddd", yd), W("dddd", yd), _(["dd", "ddd", "dddd"], function (a, b, c, d) { + var e = c._locale.weekdaysParse(a, d, c._strict);null != e ? b.d = e : j(c).invalidWeekday = a; + }), _(["d", "e", "E"], function (a, b, c, d) { + b[d] = r(a); + });var fe = "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), + ge = "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), + he = "Su_Mo_Tu_We_Th_Fr_Sa".split("_");R("DDD", ["DDDD", 3], "DDDo", "dayOfYear"), J("dayOfYear", "DDD"), W("DDD", qd), W("DDDD", kd), $(["DDD", "DDDD"], function (a, b, c) { + c._dayOfYear = r(a); + }), R("H", ["HH", 2], 0, "hour"), R("h", ["hh", 2], 0, ic), R("hmm", 0, 0, function () { + return "" + ic.apply(this) + Q(this.minutes(), 2); + }), R("hmmss", 0, 0, function () { + return "" + ic.apply(this) + Q(this.minutes(), 2) + Q(this.seconds(), 2); + }), R("Hmm", 0, 0, function () { + return "" + this.hours() + Q(this.minutes(), 2); + }), R("Hmmss", 0, 0, function () { + return "" + this.hours() + Q(this.minutes(), 2) + Q(this.seconds(), 2); + }), jc("a", !0), jc("A", !1), J("hour", "h"), W("a", kc), W("A", kc), W("H", nd), W("h", nd), W("HH", nd, jd), W("hh", nd, jd), W("hmm", od), W("hmmss", pd), W("Hmm", od), W("Hmmss", pd), $(["H", "HH"], Ed), $(["a", "A"], function (a, b, c) { + c._isPm = c._locale.isPM(a), c._meridiem = a; + }), $(["h", "hh"], function (a, b, c) { + b[Ed] = r(a), j(c).bigHour = !0; + }), $("hmm", function (a, b, c) { + var d = a.length - 2;b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d)), j(c).bigHour = !0; + }), $("hmmss", function (a, b, c) { + var d = a.length - 4, + e = a.length - 2;b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d, 2)), b[Gd] = r(a.substr(e)), j(c).bigHour = !0; + }), $("Hmm", function (a, b, c) { + var d = a.length - 2;b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d)); + }), $("Hmmss", function (a, b, c) { + var d = a.length - 4, + e = a.length - 2;b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d, 2)), b[Gd] = r(a.substr(e)); + });var ie = /[ap]\.?m?\.?/i, + je = M("Hours", !0);R("m", ["mm", 2], 0, "minute"), J("minute", "m"), W("m", nd), W("mm", nd, jd), $(["m", "mm"], Fd);var ke = M("Minutes", !1);R("s", ["ss", 2], 0, "second"), J("second", "s"), W("s", nd), W("ss", nd, jd), $(["s", "ss"], Gd);var le = M("Seconds", !1);R("S", 0, 0, function () { + return ~ ~(this.millisecond() / 100); + }), R(0, ["SS", 2], 0, function () { + return ~ ~(this.millisecond() / 10); + }), R(0, ["SSS", 3], 0, "millisecond"), R(0, ["SSSS", 4], 0, function () { + return 10 * this.millisecond(); + }), R(0, ["SSSSS", 5], 0, function () { + return 100 * this.millisecond(); + }), R(0, ["SSSSSS", 6], 0, function () { + return 1e3 * this.millisecond(); + }), R(0, ["SSSSSSS", 7], 0, function () { + return 1e4 * this.millisecond(); + }), R(0, ["SSSSSSSS", 8], 0, function () { + return 1e5 * this.millisecond(); + }), R(0, ["SSSSSSSSS", 9], 0, function () { + return 1e6 * this.millisecond(); + }), J("millisecond", "ms"), W("S", qd, id), W("SS", qd, jd), W("SSS", qd, kd);var me;for (me = "SSSS"; me.length <= 9; me += "S") W(me, td);for (me = "S"; me.length <= 9; me += "S") $(me, nc);var ne = M("Milliseconds", !1);R("z", 0, 0, "zoneAbbr"), R("zz", 0, 0, "zoneName");var oe = o.prototype;oe.add = ae, oe.calendar = jb, oe.clone = kb, oe.diff = rb, oe.endOf = Db, oe.format = vb, oe.from = wb, oe.fromNow = xb, oe.to = yb, oe.toNow = zb, oe.get = P, oe.invalidAt = Mb, oe.isAfter = lb, oe.isBefore = mb, oe.isBetween = nb, oe.isSame = ob, oe.isSameOrAfter = pb, oe.isSameOrBefore = qb, oe.isValid = Kb, oe.lang = ce, oe.locale = Ab, oe.localeData = Bb, oe.max = Xd, oe.min = Wd, oe.parsingFlags = Lb, oe.set = P, oe.startOf = Cb, oe.subtract = be, oe.toArray = Hb, oe.toObject = Ib, oe.toDate = Gb, oe.toISOString = ub, oe.toJSON = Jb, oe.toString = tb, oe.unix = Fb, oe.valueOf = Eb, oe.creationData = Nb, oe.year = Vd, oe.isLeapYear = sa, oe.weekYear = Pb, oe.isoWeekYear = Qb, oe.quarter = oe.quarters = Vb, oe.month = ga, oe.daysInMonth = ha, oe.week = oe.weeks = Zb, oe.isoWeek = oe.isoWeeks = $b, oe.weeksInYear = Sb, oe.isoWeeksInYear = Rb, oe.date = ee, oe.day = oe.days = ec, oe.weekday = fc, oe.isoWeekday = gc, oe.dayOfYear = hc, oe.hour = oe.hours = je, oe.minute = oe.minutes = ke, oe.second = oe.seconds = le, oe.millisecond = oe.milliseconds = ne, oe.utcOffset = Ta, oe.utc = Va, oe.local = Wa, oe.parseZone = Xa, oe.hasAlignedHourOffset = Ya, oe.isDST = Za, oe.isDSTShifted = $a, oe.isLocal = _a, oe.isUtcOffset = ab, oe.isUtc = bb, oe.isUTC = bb, oe.zoneAbbr = oc, oe.zoneName = pc, oe.dates = u("dates accessor is deprecated. Use date instead.", ee), oe.months = u("months accessor is deprecated. Use month instead", ga), oe.years = u("years accessor is deprecated. Use year instead", Vd), oe.zone = u("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779", Ua);var pe = oe, + qe = { sameDay: "[Today at] LT", nextDay: "[Tomorrow at] LT", nextWeek: "dddd [at] LT", lastDay: "[Yesterday at] LT", lastWeek: "[Last] dddd [at] LT", sameElse: "L" }, + re = { LTS: "h:mm:ss A", LT: "h:mm A", L: "MM/DD/YYYY", LL: "MMMM D, YYYY", LLL: "MMMM D, YYYY h:mm A", LLLL: "dddd, MMMM D, YYYY h:mm A" }, + se = "Invalid date", + te = "%d", + ue = /\d{1,2}/, + ve = { future: "in %s", past: "%s ago", s: "a few seconds", m: "a minute", mm: "%d minutes", h: "an hour", hh: "%d hours", d: "a day", dd: "%d days", M: "a month", MM: "%d months", y: "a year", yy: "%d years" }, + we = A.prototype;we._calendar = qe, we.calendar = sc, we._longDateFormat = re, we.longDateFormat = tc, we._invalidDate = se, we.invalidDate = uc, we._ordinal = te, we.ordinal = vc, we._ordinalParse = ue, we.preparse = wc, we.postformat = wc, we._relativeTime = ve, we.relativeTime = xc, we.pastFuture = yc, we.set = y, we.months = ca, we._months = Ld, we.monthsShort = da, we._monthsShort = Md, we.monthsParse = ea, we._monthsRegex = Od, we.monthsRegex = ja, we._monthsShortRegex = Nd, we.monthsShortRegex = ia, we.week = Wb, we._week = de, we.firstDayOfYear = Yb, we.firstDayOfWeek = Xb, we.weekdays = ac, we._weekdays = fe, we.weekdaysMin = cc, we._weekdaysMin = he, we.weekdaysShort = bc, we._weekdaysShort = ge, we.weekdaysParse = dc, we.isPM = lc, we._meridiemParse = ie, we.meridiem = mc, E("en", { ordinalParse: /\d{1,2}(th|st|nd|rd)/, ordinal: function ordinal(a) { + var b = a % 10, + c = 1 === r(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th";return a + c; + } }), a.lang = u("moment.lang is deprecated. Use moment.locale instead.", E), a.langData = u("moment.langData is deprecated. Use moment.localeData instead.", H);var xe = Math.abs, + ye = Qc("ms"), + ze = Qc("s"), + Ae = Qc("m"), + Be = Qc("h"), + Ce = Qc("d"), + De = Qc("w"), + Ee = Qc("M"), + Fe = Qc("y"), + Ge = Sc("milliseconds"), + He = Sc("seconds"), + Ie = Sc("minutes"), + Je = Sc("hours"), + Ke = Sc("days"), + Le = Sc("months"), + Me = Sc("years"), + Ne = Math.round, + Oe = { s: 45, m: 45, h: 22, d: 26, M: 11 }, + Pe = Math.abs, + Qe = Na.prototype;Qe.abs = Gc, Qe.add = Ic, Qe.subtract = Jc, Qe.as = Oc, Qe.asMilliseconds = ye, Qe.asSeconds = ze, Qe.asMinutes = Ae, Qe.asHours = Be, Qe.asDays = Ce, Qe.asWeeks = De, Qe.asMonths = Ee, Qe.asYears = Fe, Qe.valueOf = Pc, Qe._bubble = Lc, Qe.get = Rc, Qe.milliseconds = Ge, Qe.seconds = He, Qe.minutes = Ie, Qe.hours = Je, Qe.days = Ke, Qe.weeks = Tc, Qe.months = Le, Qe.years = Me, Qe.humanize = Xc, Qe.toISOString = Yc, Qe.toString = Yc, Qe.toJSON = Yc, Qe.locale = Ab, Qe.localeData = Bb, Qe.toIsoString = u("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)", Yc), Qe.lang = ce, R("X", 0, 0, "unix"), R("x", 0, 0, "valueOf"), W("x", ud), W("X", xd), $("X", function (a, b, c) { + c._d = new Date(1e3 * parseFloat(a, 10)); + }), $("x", function (a, b, c) { + c._d = new Date(r(a)); + }), a.version = "2.12.0", b(Ja), a.fn = pe, a.min = La, a.max = Ma, a.now = Yd, a.utc = h, a.unix = qc, a.months = Bc, a.isDate = d, a.locale = E, a.invalid = l, a.duration = cb, a.isMoment = p, a.weekdays = Dc, a.parseZone = rc, a.localeData = H, a.isDuration = Oa, a.monthsShort = Cc, a.weekdaysMin = Fc, a.defineLocale = F, a.updateLocale = G, a.locales = I, a.weekdaysShort = Ec, a.normalizeUnits = K, a.relativeTimeThreshold = Wc, a.prototype = pe;var Re = a;return Re; +}); +//! momentjs.com + diff --git a/PlexRequests.UI/Content/moment.min.es5.min.js b/PlexRequests.UI/Content/moment.min.es5.min.js new file mode 100644 index 000000000..cd6b1a6e8 --- /dev/null +++ b/PlexRequests.UI/Content/moment.min.es5.min.js @@ -0,0 +1,6 @@ +//! moment.js +//! version : 2.12.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):n.moment=t()}(undefined,function(){"use strict";function i(){return he.apply(null,arguments)}function so(n){he=n}function at(n){return n instanceof Array||"[object Array]"===Object.prototype.toString.call(n)}function li(n){return n instanceof Date||"[object Date]"===Object.prototype.toString.call(n)}function yu(n,t){for(var r=[],i=0;i0)for(u in iu)i=iu[u],r=t[i],y(r)||(n[i]=r);return n}function ui(n){sr(this,n);this._d=new Date(null!=n._d?n._d.getTime():NaN);ru===!1&&(ru=!0,i.updateOffset(this),ru=!1)}function et(n){return n instanceof ui||null!=n&&null!=n._isAMomentObject}function p(n){return 0>n?Math.ceil(n):Math.floor(n)}function f(n){var t=+n,i=0;return 0!==t&&isFinite(t)&&(i=p(t)),i}function pu(n,t,i){for(var e=Math.min(n.length,t.length),o=Math.abs(n.length-t.length),u=0,r=0;e>r;r++)(i&&n[r]!==t[r]||!i&&f(n[r])!==f(t[r]))&&u++;return u+o}function wu(n){i.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+n)}function b(n,t){var i=!0;return vt(function(){return i&&(wu(n+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),i=!1),t.apply(this,arguments)},t)}function hr(n,t){ce[n]||(wu(t),ce[n]=!0)}function ot(n){return n instanceof Function||"[object Function]"===Object.prototype.toString.call(n)}function bu(n){return"[object Object]"===Object.prototype.toString.call(n)}function co(n){var t;for(var i in n)t=n[i],ot(t)?this[i]=t:this["_"+i]=t;this._config=n;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function cr(n,t){var i,r=vt({},n);for(i in t)nt(t,i)&&(bu(n[i])&&bu(t[i])?(r[i]={},vt(r[i],n[i]),vt(r[i],t[i])):null!=t[i]?r[i]=t[i]:delete r[i]);return r}function lr(n){null!=n&&this.set(n)}function ku(n){return n?n.toLowerCase().replace("_","-"):n}function lo(n){for(var i,t,f,r,u=0;u0;){if(f=du(r.slice(0,i).join("-")))return f;if(t&&t.length>=i&&pu(r,t,!0)>=i-1)break;i--}u++}return null}function du(n){var t=null;if(!l[n]&&"undefined"!=typeof module&&module&&module.exports)try{t=gi._abbr;require("./locale/"+n);gt(t)}catch(i){}return l[n]}function gt(n,t){var i;return n&&(i=y(t)?yt(n):gu(n,t),i&&(gi=i)),gi._abbr}function gu(n,t){return null!==t?(t.abbr=n,null!=l[n]?(hr("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"),t=cr(l[n]._config,t)):null!=t.parentLocale&&(null!=l[t.parentLocale]?t=cr(l[t.parentLocale]._config,t):hr("parentLocaleUndefined","specified parentLocale is not defined yet")),l[n]=new lr(t),gt(n),l[n]):(delete l[n],null)}function ao(n,t){if(null!=t){var i;null!=l[n]&&(t=cr(l[n]._config,t));i=new lr(t);i.parentLocale=l[n];l[n]=i;gt(n)}else null!=l[n]&&(null!=l[n].parentLocale?l[n]=l[n].parentLocale:null!=l[n]&&delete l[n]);return l[n]}function yt(n){var t;if(n&&n._locale&&n._locale._abbr&&(n=n._locale._abbr),!n)return gi;if(!at(n)){if(t=du(n))return t;n=[n]}return lo(n)}function vo(){return Object.keys(l)}function v(n,t){var i=n.toLowerCase();hi[i]=hi[i+"s"]=hi[t]=n}function k(n){if("string"==typeof n)return hi[n]||hi[n.toLowerCase()]}function nf(n){var i,t,r={};for(t in n)nt(n,t)&&(i=k(t),i&&(r[i]=n[t]));return r}function ni(n,t){return function(r){return null!=r?(tf(this,n,r),i.updateOffset(this,t),this):vi(this,n)}}function vi(n,t){return n.isValid()?n._d["get"+(n._isUTC?"UTC":"")+t]():NaN}function tf(n,t,i){n.isValid()&&n._d["set"+(n._isUTC?"UTC":"")+t](i)}function rf(n,t){var i;if("object"==typeof n)for(i in n)this.set(i,n[i]);else if(n=k(n),ot(this[n]))return this[n](t);return this}function it(n,t,i){var r=""+Math.abs(n),u=t-r.length,f=n>=0;return(f?i?"+":"":"-")+Math.pow(10,Math.max(0,u)).toString().substr(1)+r}function r(n,t,i,r){var u=r;"string"==typeof r&&(u=function(){return this[r]()});n&&(ii[n]=u);t&&(ii[t[0]]=function(){return it(u.apply(this,arguments),t[1],t[2])});i&&(ii[i]=function(){return this.localeData().ordinal(u.apply(this,arguments),n)})}function yo(n){return n.match(/\[[\s\S]/)?n.replace(/^\[|\]$/g,""):n.replace(/\\/g,"")}function po(n){for(var i=n.match(le),t=0,r=i.length;r>t;t++)i[t]=ii[i[t]]?ii[i[t]]:yo(i[t]);return function(u){var f="";for(t=0;r>t;t++)f+=i[t]instanceof Function?i[t].call(u,n):i[t];return f}}function ar(n,t){return n.isValid()?(t=uf(t,n.localeData()),uu[t]=uu[t]||po(t),uu[t](n)):n.localeData().invalidDate()}function uf(n,t){function r(n){return t.longDateFormat(n)||n}var i=5;for(nr.lastIndex=0;i>=0&&nr.test(n);)n=n.replace(nr,r),nr.lastIndex=0,i-=1;return n}function t(n,t,i){ou[n]=ot(t)?t:function(n){return n&&i?i:t}}function wo(n,t){return nt(ou,n)?ou[n](t._strict,t._locale):new RegExp(bo(n))}function bo(n){return yi(n.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(n,t,i,r,u){return t||i||r||u}))}function yi(n){return n.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function s(n,t){var i,r=t;for("string"==typeof n&&(n=[n]),"number"==typeof t&&(r=function(n,i){i[t]=f(n)}),i=0;ir;r++)if((u=dt([2e3,r]),i&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp("^"+this.months(u,"").replace(".","")+"$","i"),this._shortMonthsParse[r]=new RegExp("^"+this.monthsShort(u,"").replace(".","")+"$","i")),i||this._monthsParse[r]||(f="^"+this.months(u,"")+"|^"+this.monthsShort(u,""),this._monthsParse[r]=new RegExp(f.replace(".",""),"i")),i&&"MMMM"===t&&this._longMonthsParse[r].test(n))||i&&"MMM"===t&&this._shortMonthsParse[r].test(n)||!i&&this._monthsParse[r].test(n))return r}function ff(n,t){var i;if(!n.isValid())return n;if("string"==typeof t)if(/^\d+$/.test(t))t=f(t);else if(t=n.localeData().monthsParse(t),"number"!=typeof t)return n;return i=Math.min(n.date(),vr(n.year(),t)),n._d["set"+(n._isUTC?"UTC":"")+"Month"](t,i),n}function ef(n){return null!=n?(ff(this,n),i.updateOffset(this,!0),this):vi(this,"Month")}function is(){return vr(this.year(),this.month())}function rs(n){return this._monthsParseExact?(nt(this,"_monthsRegex")||of.call(this),n?this._monthsShortStrictRegex:this._monthsShortRegex):this._monthsShortStrictRegex&&n?this._monthsShortStrictRegex:this._monthsShortRegex}function us(n){return this._monthsParseExact?(nt(this,"_monthsRegex")||of.call(this),n?this._monthsStrictRegex:this._monthsRegex):this._monthsStrictRegex&&n?this._monthsStrictRegex:this._monthsRegex}function of(){function f(n,t){return t.length-n.length}for(var i,r=[],u=[],t=[],n=0;12>n;n++)i=dt([2e3,n]),r.push(this.monthsShort(i,"")),u.push(this.months(i,"")),t.push(this.months(i,"")),t.push(this.monthsShort(i,""));for(r.sort(f),u.sort(f),t.sort(f),n=0;12>n;n++)r[n]=yi(r[n]),u[n]=yi(u[n]),t[n]=yi(t[n]);this._monthsRegex=new RegExp("^("+t.join("|")+")","i");this._monthsShortRegex=this._monthsRegex;this._monthsStrictRegex=new RegExp("^("+u.join("|")+")$","i");this._monthsShortStrictRegex=new RegExp("^("+r.join("|")+")$","i")}function yr(n){var i,t=n._a;return t&&-2===e(n).overflow&&(i=t[rt]<0||t[rt]>11?rt:t[tt]<1||t[tt]>vr(t[d],t[rt])?tt:t[a]<0||t[a]>24||24===t[a]&&(0!==t[g]||0!==t[ut]||0!==t[kt])?a:t[g]<0||t[g]>59?g:t[ut]<0||t[ut]>59?ut:t[kt]<0||t[kt]>999?kt:-1,e(n)._overflowDayOfYear&&(d>i||i>tt)&&(i=tt),e(n)._overflowWeeks&&-1===i&&(i=rv),e(n)._overflowWeekday&&-1===i&&(i=uv),e(n).overflow=i),n}function sf(n){var t,r,o,f,u,s,h=n._i,i=hv.exec(h)||cv.exec(h);if(i){for(e(n).iso=!0,t=0,r=er.length;r>t;t++)if(er[t][1].exec(i[1])){f=er[t][0];o=er[t][2]!==!1;break}if(null==f)return void(n._isValid=!1);if(i[3]){for(t=0,r=hu.length;r>t;t++)if(hu[t][1].exec(i[3])){u=(i[2]||" ")+hu[t][0];break}if(null==u)return void(n._isValid=!1)}if(!o&&null!=u)return void(n._isValid=!1);if(i[4]){if(!lv.exec(i[4]))return void(n._isValid=!1);s="Z"}n._f=f+(u||"")+(s||"");wr(n)}else n._isValid=!1}function fs(n){var t=av.exec(n._i);return null!==t?void(n._d=new Date(+t[1])):(sf(n),void(n._isValid===!1&&(delete n._isValid,i.createFromInputFallback(n))))}function es(n,t,i,r,u,f,e){var o=new Date(n,t,i,r,u,f,e);return 100>n&&n>=0&&isFinite(o.getFullYear())&&o.setFullYear(n),o}function pi(n){var t=new Date(Date.UTC.apply(null,arguments));return 100>n&&n>=0&&isFinite(t.getUTCFullYear())&&t.setUTCFullYear(n),t}function ei(n){return hf(n)?366:365}function hf(n){return n%4==0&&n%100!=0||n%400==0}function os(){return hf(this.year())}function wi(n,t,i){var r=7+t-i,u=(7+pi(n,0,r).getUTCDay()-t)%7;return-u+r-1}function cf(n,t,i,r,u){var f,o,s=(7+i-r)%7,h=wi(n,r,u),e=1+7*(t-1)+s+h;return 0>=e?(f=n-1,o=ei(f)+e):e>ei(n)?(f=n+1,o=e-ei(n)):(f=n,o=e),{year:f,dayOfYear:o}}function oi(n,t,i){var f,r,e=wi(n.year(),t,i),u=Math.floor((n.dayOfYear()-e-1)/7)+1;return 1>u?(r=n.year()-1,f=u+pt(r,t,i)):u>pt(n.year(),t,i)?(f=u-pt(n.year(),t,i),r=n.year()+1):(r=n.year(),f=u),{week:f,year:r}}function pt(n,t,i){var r=wi(n,t,i),u=wi(n+1,t,i);return(ei(n)-r+u)/7}function ti(n,t,i){return null!=n?n:null!=t?t:i}function ss(n){var t=new Date(i.now());return n._useUTC?[t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()]:[t.getFullYear(),t.getMonth(),t.getDate()]}function pr(n){var t,i,r,u,f=[];if(!n._d){for(r=ss(n),n._w&&null==n._a[tt]&&null==n._a[rt]&&hs(n),n._dayOfYear&&(u=ti(n._a[d],r[d]),n._dayOfYear>ei(u)&&(e(n)._overflowDayOfYear=!0),i=pi(u,0,n._dayOfYear),n._a[rt]=i.getUTCMonth(),n._a[tt]=i.getUTCDate()),t=0;3>t&&null==n._a[t];++t)n._a[t]=f[t]=r[t];for(;7>t;t++)n._a[t]=f[t]=null==n._a[t]?2===t?1:0:n._a[t];24===n._a[a]&&0===n._a[g]&&0===n._a[ut]&&0===n._a[kt]&&(n._nextDay=!0,n._a[a]=0);n._d=(n._useUTC?pi:es).apply(null,f);null!=n._tzm&&n._d.setUTCMinutes(n._d.getUTCMinutes()-n._tzm);n._nextDay&&(n._a[a]=24)}}function hs(n){var t,o,u,i,r,f,c,s;t=n._w;null!=t.GG||null!=t.W||null!=t.E?(r=1,f=4,o=ti(t.GG,n._a[d],oi(h(),1,4).year),u=ti(t.W,1),i=ti(t.E,1),(1>i||i>7)&&(s=!0)):(r=n._locale._week.dow,f=n._locale._week.doy,o=ti(t.gg,n._a[d],oi(h(),r,f).year),u=ti(t.w,1),null!=t.d?(i=t.d,(0>i||i>6)&&(s=!0)):null!=t.e?(i=t.e+r,(t.e<0||t.e>6)&&(s=!0)):i=r);1>u||u>pt(o,r,f)?e(n)._overflowWeeks=!0:null!=s?e(n)._overflowWeekday=!0:(c=cf(o,u,i,r,f),n._a[d]=c.year,n._dayOfYear=c.dayOfYear)}function wr(n){if(n._f===i.ISO_8601)return void sf(n);n._a=[];e(n).empty=!0;for(var t,u,s,r=""+n._i,c=r.length,h=0,o=uf(n._f,n._locale).match(le)||[],f=0;f0&&e(n).unusedInput.push(s),r=r.slice(r.indexOf(t)+t.length),h+=t.length),ii[u]?(t?e(n).empty=!1:e(n).unusedTokens.push(u),ko(u,t,n)):n._strict&&!t&&e(n).unusedTokens.push(u);e(n).charsLeftOver=c-h;r.length>0&&e(n).unusedInput.push(r);e(n).bigHour===!0&&n._a[a]<=12&&n._a[a]>0&&(e(n).bigHour=void 0);n._a[a]=cs(n._locale,n._a[a],n._meridiem);pr(n);yr(n)}function cs(n,t,i){var r;return null==i?t:null!=n.meridiemHour?n.meridiemHour(t,i):null!=n.isPM?(r=n.isPM(i),r&&12>t&&(t+=12),r||12!==t||(t=0),t):t}function ls(n){var t,f,u,r,i;if(0===n._f.length)return e(n).invalidFormat=!0,void(n._d=new Date(NaN));for(r=0;ri)&&(u=i,f=t));vt(n,f||t)}function as(n){if(!n._d){var t=nf(n._i);n._a=yu([t.year,t.month,t.day||t.date,t.hour,t.minute,t.second,t.millisecond],function(n){return n&&parseInt(n,10)});pr(n)}}function vs(n){var t=new ui(yr(lf(n)));return t._nextDay&&(t.add(1,"d"),t._nextDay=void 0),t}function lf(n){var t=n._i,i=n._f;return n._locale=n._locale||yt(n._l),null===t||void 0===i&&""===t?ai({nullInput:!0}):("string"==typeof t&&(n._i=t=n._locale.preparse(t)),et(t)?new ui(yr(t)):(at(i)?ls(n):i?wr(n):li(t)?n._d=t:ys(n),or(n)||(n._d=null),n))}function ys(n){var t=n._i;void 0===t?n._d=new Date(i.now()):li(t)?n._d=new Date(+t):"string"==typeof t?fs(n):at(t)?(n._a=yu(t.slice(0),function(n){return parseInt(n,10)}),pr(n)):"object"==typeof t?as(n):"number"==typeof t?n._d=new Date(t):i.createFromInputFallback(n)}function af(n,t,i,r,u){var f={};return"boolean"==typeof i&&(r=i,i=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=u,f._l=i,f._i=n,f._f=t,f._strict=r,vs(f)}function h(n,t,i,r){return af(n,t,i,r,!1)}function vf(n,t){var r,i;if(1===t.length&&at(t[0])&&(t=t[0]),!t.length)return h();for(r=t[0],i=1;in&&(n=-n,i="-"),i+it(~~(n/60),2)+t+it(~~n%60,2)})}function kr(n,t){var r=(t||"").match(n)||[],e=r[r.length-1]||[],i=(e+"").match(be)||["-",0,0],u=+(60*i[1])+f(i[2]);return"+"===i[0]?u:-u}function dr(n,t){var r,u;return t._isUTC?(r=t.clone(),u=(et(n)||li(n)?+n:+h(n))-+r,r._d.setTime(+r._d+u),i.updateOffset(r,!1),r):h(n).local()}function gr(n){return 15*-Math.round(n._d.getTimezoneOffset()/15)}function bs(n,t){var r,u=this._offset||0;return this.isValid()?null!=n?("string"==typeof n?n=kr(fr,n):Math.abs(n)<16&&(n=60*n),!this._isUTC&&t&&(r=gr(this)),this._offset=n,this._isUTC=!0,null!=r&&this.add(r,"m"),u!==n&&(!t||this._changeInProgress?df(this,st(n-u,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,i.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?u:gr(this):null!=n?this:NaN}function ks(n,t){return null!=n?("string"!=typeof n&&(n=-n),this.utcOffset(n,t),this):-this.utcOffset()}function ds(n){return this.utcOffset(0,n)}function gs(n){return this._isUTC&&(this.utcOffset(0,n),this._isUTC=!1,n&&this.subtract(gr(this),"m")),this}function nh(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(kr(iv,this._i)),this}function th(n){return this.isValid()?(n=n?h(n).utcOffset():0,(this.utcOffset()-n)%60==0):!1}function ih(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function rh(){var n,t;return y(this._isDSTShifted)?(n={},(sr(n,this),n=lf(n),n._a)?(t=n._isUTC?dt(n._a):h(n._a),this._isDSTShifted=this.isValid()&&pu(n._a,t.toArray())>0):this._isDSTShifted=!1,this._isDSTShifted):this._isDSTShifted}function uh(){return this.isValid()?!this._isUTC:!1}function fh(){return this.isValid()?this._isUTC:!1}function pf(){return this.isValid()?this._isUTC&&0===this._offset:!1}function st(n,t){var u,e,o,i=n,r=null;return br(n)?i={ms:n._milliseconds,d:n._days,M:n._months}:"number"==typeof n?(i={},t?i[t]=n:i.milliseconds=n):(r=ke.exec(n))?(u="-"===r[1]?-1:1,i={y:0,d:f(r[tt])*u,h:f(r[a])*u,m:f(r[g])*u,s:f(r[ut])*u,ms:f(r[kt])*u}):(r=de.exec(n))?(u="-"===r[1]?-1:1,i={y:wt(r[2],u),M:wt(r[3],u),w:wt(r[4],u),d:wt(r[5],u),h:wt(r[6],u),m:wt(r[7],u),s:wt(r[8],u)}):null==i?i={}:"object"==typeof i&&("from"in i||"to"in i)&&(o=eh(h(i.from),h(i.to)),i={},i.ms=o.milliseconds,i.M=o.months),e=new bi(i),br(n)&&nt(n,"_locale")&&(e._locale=n._locale),e}function wt(n,t){var i=n&&parseFloat(n.replace(",","."));return(isNaN(i)?0:i)*t}function wf(n,t){var i={milliseconds:0,months:0};return i.months=t.month()-n.month()+12*(t.year()-n.year()),n.clone().add(i.months,"M").isAfter(t)&&--i.months,i.milliseconds=+t-+n.clone().add(i.months,"M"),i}function eh(n,t){var i;return n.isValid()&&t.isValid()?(t=dr(t,n),n.isBefore(t)?i=wf(n,t):(i=wf(t,n),i.milliseconds=-i.milliseconds,i.months=-i.months),i):{milliseconds:0,months:0}}function bf(n){return 0>n?-1*Math.round(-1*n):Math.round(n)}function kf(n,t){return function(i,r){var u,f;return null===r||isNaN(+r)||(hr(t,"moment()."+t+"(period, number) is deprecated. Please use moment()."+t+"(number, period)."),f=i,i=r,r=f),i="string"==typeof i?+i:i,u=st(i,r),df(this,u,n),this}}function df(n,t,r,u){var o=t._milliseconds,f=bf(t._days),e=bf(t._months);n.isValid()&&(u=null==u?!0:u,o&&n._d.setTime(+n._d+o*r),f&&tf(n,"Date",vi(n,"Date")+f*r),e&&ff(n,vi(n,"Month")+e*r),u&&i.updateOffset(n,f||e))}function oh(n,t){var u=n||h(),f=dr(u,this).startOf("day"),i=this.diff(f,"days",!0),r=-6>i?"sameElse":-1>i?"lastWeek":0>i?"lastDay":1>i?"sameDay":2>i?"nextDay":7>i?"nextWeek":"sameElse",e=t&&(ot(t[r])?t[r]():t[r]);return this.format(e||this.localeData().calendar(r,this,h(u)))}function sh(){return new ui(this)}function hh(n,t){var i=et(n)?n:h(n);return this.isValid()&&i.isValid()?(t=k(y(t)?"millisecond":t),"millisecond"===t?+this>+i:+i<+this.clone().startOf(t)):!1}function ch(n,t){var i=et(n)?n:h(n);return this.isValid()&&i.isValid()?(t=k(y(t)?"millisecond":t),"millisecond"===t?+i>+this:+this.clone().endOf(t)<+i):!1}function lh(n,t,i){return this.isAfter(n,i)&&this.isBefore(t,i)}function ah(n,t){var i,r=et(n)?n:h(n);return this.isValid()&&r.isValid()?(t=k(t||"millisecond"),"millisecond"===t?+this==+r:(i=+r,+this.clone().startOf(t)<=i&&i<=+this.clone().endOf(t))):!1}function vh(n,t){return this.isSame(n,t)||this.isAfter(n,t)}function yh(n,t){return this.isSame(n,t)||this.isBefore(n,t)}function ph(n,t,i){var f,e,r,u;return this.isValid()?(f=dr(n,this),f.isValid()?(e=6e4*(f.utcOffset()-this.utcOffset()),t=k(t),"year"===t||"month"===t||"quarter"===t?(u=wh(this,f),"quarter"===t?u/=3:"year"===t&&(u/=12)):(r=this-f,u="second"===t?r/1e3:"minute"===t?r/6e4:"hour"===t?r/36e5:"day"===t?(r-e)/864e5:"week"===t?(r-e)/6048e5:r),i?u:p(u)):NaN):NaN}function wh(n,t){var r,f,u=12*(t.year()-n.year())+(t.month()-n.month()),i=n.clone().add(u,"months");return 0>t-i?(r=n.clone().add(u-1,"months"),f=(t-i)/(i-r)):(r=n.clone().add(u+1,"months"),f=(t-i)/(r-i)),-(u+f)}function bh(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function kh(){var n=this.clone().utc();return 0f&&(t=f),dc.call(this,n,t,i,r,u))}function dc(n,t,i,r,u){var e=cf(n,t,i,r,u),f=pi(e.year,0,e.dayOfYear);return this.year(f.getUTCFullYear()),this.month(f.getUTCMonth()),this.date(f.getUTCDate()),this}function gc(n){return null==n?Math.ceil((this.month()+1)/3):this.month(3*(n-1)+this.month()%3)}function nl(n){return oi(n,this._week.dow,this._week.doy).week}function tl(){return this._week.dow}function il(){return this._week.doy}function rl(n){var t=this.localeData().week(this);return null==n?t:this.add(7*(n-t),"d")}function ul(n){var t=oi(this,1,4).week;return null==n?t:this.add(7*(n-t),"d")}function fl(n,t){return"string"!=typeof n?n:isNaN(n)?(n=t.weekdaysParse(n),"number"==typeof n?n:null):parseInt(n,10)}function el(n,t){return at(this._weekdays)?this._weekdays[n.day()]:this._weekdays[this._weekdays.isFormat.test(t)?"format":"standalone"][n.day()]}function ol(n){return this._weekdaysShort[n.day()]}function sl(n){return this._weekdaysMin[n.day()]}function hl(n,t,i){var r,u,f;for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),r=0;7>r;r++)if((u=h([2e3,1]).day(r),i&&!this._fullWeekdaysParse[r]&&(this._fullWeekdaysParse[r]=new RegExp("^"+this.weekdays(u,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[r]=new RegExp("^"+this.weekdaysShort(u,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[r]=new RegExp("^"+this.weekdaysMin(u,"").replace(".",".?")+"$","i")),this._weekdaysParse[r]||(f="^"+this.weekdays(u,"")+"|^"+this.weekdaysShort(u,"")+"|^"+this.weekdaysMin(u,""),this._weekdaysParse[r]=new RegExp(f.replace(".",""),"i")),i&&"dddd"===t&&this._fullWeekdaysParse[r].test(n))||i&&"ddd"===t&&this._shortWeekdaysParse[r].test(n)||i&&"dd"===t&&this._minWeekdaysParse[r].test(n)||!i&&this._weekdaysParse[r].test(n))return r}function cl(n){if(!this.isValid())return null!=n?this:NaN;var t=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=n?(n=fl(n,this.localeData()),this.add(n-t,"d")):t}function ll(n){if(!this.isValid())return null!=n?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==n?t:this.add(n-t,"d")}function al(n){return this.isValid()?null==n?this.day()||7:this.day(this.day()%7?n:n-7):null!=n?this:NaN}function vl(n){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==n?t:this.add(n-t,"d")}function nu(){return this.hours()%12||12}function ie(n,t){r(n,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function re(n,t){return t._meridiemParse}function yl(n){return"p"===(n+"").toLowerCase().charAt(0)}function pl(n,t,i){return n>11?i?"pm":"PM":i?"am":"AM"}function wl(n,t){t[kt]=f(1e3*("0."+n))}function bl(){return this._isUTC?"UTC":""}function kl(){return this._isUTC?"Coordinated Universal Time":""}function dl(n){return h(1e3*n)}function gl(){return h.apply(null,arguments).parseZone()}function na(n,t,i){var r=this._calendar[n];return ot(r)?r.call(t,i):r}function ta(n){var t=this._longDateFormat[n],i=this._longDateFormat[n.toUpperCase()];return t||!i?t:(this._longDateFormat[n]=i.replace(/MMMM|MM|DD|dddd/g,function(n){return n.slice(1)}),this._longDateFormat[n])}function ia(){return this._invalidDate}function ra(n){return this._ordinal.replace("%d",n)}function ue(n){return n}function ua(n,t,i,r){var u=this._relativeTime[i];return ot(u)?u(n,t,i,r):u.replace(/%d/i,n)}function fa(n,t){var i=this._relativeTime[n>0?"future":"past"];return ot(i)?i(t):i.replace(/%s/i,t)}function fe(n,t,i,r){var u=yt(),f=dt().set(r,t);return u[i](f,n)}function si(n,t,i,r,u){if("number"==typeof n&&(t=n,n=void 0),n=n||"",null!=t)return fe(n,t,i,u);for(var e=[],f=0;r>f;f++)e[f]=fe(n,f,i,u);return e}function ea(n,t){return si(n,t,"months",12,"month")}function oa(n,t){return si(n,t,"monthsShort",12,"month")}function sa(n,t){return si(n,t,"weekdays",7,"day")}function ha(n,t){return si(n,t,"weekdaysShort",7,"day")}function ca(n,t){return si(n,t,"weekdaysMin",7,"day")}function la(){var n=this._data;return this._milliseconds=ft(this._milliseconds),this._days=ft(this._days),this._months=ft(this._months),n.milliseconds=ft(n.milliseconds),n.seconds=ft(n.seconds),n.minutes=ft(n.minutes),n.hours=ft(n.hours),n.months=ft(n.months),n.years=ft(n.years),this}function ee(n,t,i,r){var u=st(t,i);return n._milliseconds+=r*u._milliseconds,n._days+=r*u._days,n._months+=r*u._months,n._bubble()}function aa(n,t){return ee(this,n,t,1)}function va(n,t){return ee(this,n,t,-1)}function oe(n){return 0>n?Math.floor(n):Math.ceil(n)}function ya(){var u,f,e,s,o,r=this._milliseconds,n=this._days,t=this._months,i=this._data;return r>=0&&n>=0&&t>=0||0>=r&&0>=n&&0>=t||(r+=864e5*oe(tu(t)+n),n=0,t=0),i.milliseconds=r%1e3,u=p(r/1e3),i.seconds=u%60,f=p(u/60),i.minutes=f%60,e=p(f/60),i.hours=e%24,n+=p(e/24),o=p(se(n)),t+=o,n-=oe(tu(o)),s=p(t/12),t%=12,i.days=n,i.months=t,i.years=s,this}function se(n){return 4800*n/146097}function tu(n){return 146097*n/4800}function pa(n){var t,r,i=this._milliseconds;if(n=k(n),"month"===n||"year"===n)return t=this._days+i/864e5,r=this._months+se(t),"month"===n?r:r/12;switch(t=this._days+Math.round(tu(this._months)),n){case"week":return t/7+i/6048e5;case"day":return t+i/864e5;case"hour":return 24*t+i/36e5;case"minute":return 1440*t+i/6e4;case"second":return 86400*t+i/1e3;case"millisecond":return Math.floor(864e5*t)+i;default:throw new Error("Unknown unit "+n);}}function wa(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*f(this._months/12)}function ht(n){return function(){return this.as(n)}}function ba(n){return n=k(n),this[n+"s"]()}function bt(n){return function(){return this._data[n]}}function ka(){return p(this.days()/7)}function da(n,t,i,r,u){return u.relativeTime(t||1,!!i,n,r)}function ga(n,t,i){var r=st(n).abs(),h=ri(r.as("s")),f=ri(r.as("m")),e=ri(r.as("h")),o=ri(r.as("d")),s=ri(r.as("M")),c=ri(r.as("y")),u=h=f&&["m"]||f=e&&["h"]||e=o&&["d"]||o=s&&["M"]||s=c&&["y"]||["yy",c];return u[2]=t,u[3]=+n>0,u[4]=i,da.apply(null,u)}function nv(n,t){return void 0===lt[n]?!1:void 0===t?lt[n]:(lt[n]=t,!0)}function tv(n){var t=this.localeData(),i=ga(this,!n,t);return n&&(i=t.pastFuture(+this,i)),t.postformat(i)}function di(){var n,e,o,t=vu(this._milliseconds)/1e3,a=vu(this._days),i=vu(this._months);n=p(t/60);e=p(n/60);t%=60;n%=60;o=p(i/12);i%=12;var s=o,h=i,c=a,r=e,u=n,f=t,l=this.asSeconds();return l?(0>l?"-":"")+"P"+(s?s+"Y":"")+(h?h+"M":"")+(c?c+"D":"")+(r||u||f?"T":"")+(r?r+"H":"")+(u?u+"M":"")+(f?f+"S":""):"P0D"}var he,iu=i.momentProperties=[],ru=!1,ce={},cu,be,ke,de,ge,no,lu,to,au,io,ro,uo,fo,ct,eo,n;i.suppressDeprecationWarnings=!1;var gi,l={},hi={},le=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,nr=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,uu={},ii={},ae=/\d/,w=/\d\d/,ve=/\d{3}/,fu=/\d{4}/,tr=/[+-]?\d{6}/,c=/\d\d?/,ye=/\d\d\d\d?/,pe=/\d\d\d\d\d\d?/,ir=/\d{1,3}/,eu=/\d{1,4}/,rr=/[+-]?\d{1,6}/,ur=/[+-]?\d+/,iv=/Z|[+-]\d\d:?\d\d/gi,fr=/Z|[+-]\d\d(?::?\d\d)?/gi,ci=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,ou={},su={},d=0,rt=1,tt=2,a=3,g=4,ut=5,kt=6,rv=7,uv=8;r("M",["MM",2],"Mo",function(){return this.month()+1});r("MMM",0,0,function(n){return this.localeData().monthsShort(this,n)});r("MMMM",0,0,function(n){return this.localeData().months(this,n)});v("month","M");t("M",c);t("MM",c,w);t("MMM",function(n,t){return t.monthsShortRegex(n)});t("MMMM",function(n,t){return t.monthsRegex(n)});s(["M","MM"],function(n,t){t[rt]=f(n)-1});s(["MMM","MMMM"],function(n,t,i,r){var u=i._locale.monthsParse(n,r,i._strict);null!=u?t[rt]=u:e(i).invalidMonth=n});var we=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/,fv="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ev="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),ov=ci,sv=ci,hv=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,cv=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,lv=/Z|[+-]\d\d(?::?\d\d)?/,er=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],hu=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],av=/^\/?Date\((\-?\d+)/i;i.createFromInputFallback=b("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(n){n._d=new Date(n._i+(n._useUTC?" UTC":""))});r("Y",0,0,function(){var n=this.year();return 9999>=n?""+n:"+"+n});r(0,["YY",2],0,function(){return this.year()%100});r(0,["YYYY",4],0,"year");r(0,["YYYYY",5],0,"year");r(0,["YYYYYY",6,!0],0,"year");v("year","y");t("Y",ur);t("YY",c,w);t("YYYY",eu,fu);t("YYYYY",rr,tr);t("YYYYYY",rr,tr);s(["YYYYY","YYYYYY"],d);s("YYYY",function(n,t){t[d]=2===n.length?i.parseTwoDigitYear(n):f(n)});s("YY",function(n,t){t[d]=i.parseTwoDigitYear(n)});s("Y",function(n,t){t[d]=parseInt(n,10)});i.parseTwoDigitYear=function(n){return f(n)+(f(n)>68?1900:2e3)};cu=ni("FullYear",!1);i.ISO_8601=function(){};var vv=b("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var n=h.apply(null,arguments);return this.isValid()&&n.isValid()?this>n?this:n:ai()}),yv=b("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var n=h.apply(null,arguments);return this.isValid()&&n.isValid()?n>this?this:n:ai()}),pv=function(){return Date.now?Date.now():+new Date};yf("Z",":");yf("ZZ","");t("Z",fr);t("ZZ",fr);s(["Z","ZZ"],function(n,t,i){i._useUTC=!0;i._tzm=kr(fr,n)});be=/([\+\-]|\d\d)/gi;i.updateOffset=function(){};ke=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/;de=/^(-)?P(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)W)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?$/;st.fn=bi.prototype;ge=kf(1,"add");no=kf(-1,"subtract");i.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";lu=b("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(n){return void 0===n?this.localeData():this.locale(n)});r(0,["gg",2],0,function(){return this.weekYear()%100});r(0,["GG",2],0,function(){return this.isoWeekYear()%100});ki("gggg","weekYear");ki("ggggg","weekYear");ki("GGGG","isoWeekYear");ki("GGGGG","isoWeekYear");v("weekYear","gg");v("isoWeekYear","GG");t("G",ur);t("g",ur);t("GG",c,w);t("gg",c,w);t("GGGG",eu,fu);t("gggg",eu,fu);t("GGGGG",rr,tr);t("ggggg",rr,tr);fi(["gggg","ggggg","GGGG","GGGGG"],function(n,t,i,r){t[r.substr(0,2)]=f(n)});fi(["gg","GG"],function(n,t,r,u){t[u]=i.parseTwoDigitYear(n)});r("Q",0,"Qo","quarter");v("quarter","Q");t("Q",ae);s("Q",function(n,t){t[rt]=3*(f(n)-1)});r("w",["ww",2],"wo","week");r("W",["WW",2],"Wo","isoWeek");v("week","w");v("isoWeek","W");t("w",c);t("ww",c,w);t("W",c);t("WW",c,w);fi(["w","ww","W","WW"],function(n,t,i,r){t[r.substr(0,1)]=f(n)});to={dow:0,doy:6};r("D",["DD",2],"Do","date");v("date","D");t("D",c);t("DD",c,w);t("Do",function(n,t){return n?t._ordinalParse:t._ordinalParseLenient});s(["D","DD"],tt);s("Do",function(n,t){t[tt]=f(n.match(c)[0],10)});au=ni("Date",!0);r("d",0,"do","day");r("dd",0,0,function(n){return this.localeData().weekdaysMin(this,n)});r("ddd",0,0,function(n){return this.localeData().weekdaysShort(this,n)});r("dddd",0,0,function(n){return this.localeData().weekdays(this,n)});r("e",0,0,"weekday");r("E",0,0,"isoWeekday");v("day","d");v("weekday","e");v("isoWeekday","E");t("d",c);t("e",c);t("E",c);t("dd",ci);t("ddd",ci);t("dddd",ci);fi(["dd","ddd","dddd"],function(n,t,i,r){var u=i._locale.weekdaysParse(n,r,i._strict);null!=u?t.d=u:e(i).invalidWeekday=n});fi(["d","e","E"],function(n,t,i,r){t[r]=f(n)});var wv="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),bv="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),kv="Su_Mo_Tu_We_Th_Fr_Sa".split("_");for(r("DDD",["DDDD",3],"DDDo","dayOfYear"),v("dayOfYear","DDD"),t("DDD",ir),t("DDDD",ve),s(["DDD","DDDD"],function(n,t,i){i._dayOfYear=f(n)}),r("H",["HH",2],0,"hour"),r("h",["hh",2],0,nu),r("hmm",0,0,function(){return""+nu.apply(this)+it(this.minutes(),2)}),r("hmmss",0,0,function(){return""+nu.apply(this)+it(this.minutes(),2)+it(this.seconds(),2)}),r("Hmm",0,0,function(){return""+this.hours()+it(this.minutes(),2)}),r("Hmmss",0,0,function(){return""+this.hours()+it(this.minutes(),2)+it(this.seconds(),2)}),ie("a",!0),ie("A",!1),v("hour","h"),t("a",re),t("A",re),t("H",c),t("h",c),t("HH",c,w),t("hh",c,w),t("hmm",ye),t("hmmss",pe),t("Hmm",ye),t("Hmmss",pe),s(["H","HH"],a),s(["a","A"],function(n,t,i){i._isPm=i._locale.isPM(n);i._meridiem=n}),s(["h","hh"],function(n,t,i){t[a]=f(n);e(i).bigHour=!0}),s("hmm",function(n,t,i){var r=n.length-2;t[a]=f(n.substr(0,r));t[g]=f(n.substr(r));e(i).bigHour=!0}),s("hmmss",function(n,t,i){var r=n.length-4,u=n.length-2;t[a]=f(n.substr(0,r));t[g]=f(n.substr(r,2));t[ut]=f(n.substr(u));e(i).bigHour=!0}),s("Hmm",function(n,t){var i=n.length-2;t[a]=f(n.substr(0,i));t[g]=f(n.substr(i))}),s("Hmmss",function(n,t){var i=n.length-4,r=n.length-2;t[a]=f(n.substr(0,i));t[g]=f(n.substr(i,2));t[ut]=f(n.substr(r))}),io=/[ap]\.?m?\.?/i,ro=ni("Hours",!0),r("m",["mm",2],0,"minute"),v("minute","m"),t("m",c),t("mm",c,w),s(["m","mm"],g),uo=ni("Minutes",!1),r("s",["ss",2],0,"second"),v("second","s"),t("s",c),t("ss",c,w),s(["s","ss"],ut),fo=ni("Seconds",!1),r("S",0,0,function(){return~~(this.millisecond()/100)}),r(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),r(0,["SSS",3],0,"millisecond"),r(0,["SSSS",4],0,function(){return 10*this.millisecond()}),r(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),r(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),r(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),r(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),r(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),v("millisecond","ms"),t("S",ir,ae),t("SS",ir,w),t("SSS",ir,ve),ct="SSSS";ct.length<=9;ct+="S")t(ct,/\d+/);for(ct="S";ct.length<=9;ct+="S")s(ct,wl);eo=ni("Milliseconds",!1);r("z",0,0,"zoneAbbr");r("zz",0,0,"zoneName");n=ui.prototype;n.add=ge;n.calendar=oh;n.clone=sh;n.diff=ph;n.endOf=uc;n.format=dh;n.from=gh;n.fromNow=nc;n.to=tc;n.toNow=ic;n.get=rf;n.invalidAt=vc;n.isAfter=hh;n.isBefore=ch;n.isBetween=lh;n.isSame=ah;n.isSameOrAfter=vh;n.isSameOrBefore=yh;n.isValid=lc;n.lang=lu;n.locale=gf;n.localeData=ne;n.max=yv;n.min=vv;n.parsingFlags=ac;n.set=rf;n.startOf=rc;n.subtract=no;n.toArray=sc;n.toObject=hc;n.toDate=oc;n.toISOString=kh;n.toJSON=cc;n.toString=bh;n.unix=ec;n.valueOf=fc;n.creationData=yc;n.year=cu;n.isLeapYear=os;n.weekYear=pc;n.isoWeekYear=wc;n.quarter=n.quarters=gc;n.month=ef;n.daysInMonth=is;n.week=n.weeks=rl;n.isoWeek=n.isoWeeks=ul;n.weeksInYear=kc;n.isoWeeksInYear=bc;n.date=au;n.day=n.days=cl;n.weekday=ll;n.isoWeekday=al;n.dayOfYear=vl;n.hour=n.hours=ro;n.minute=n.minutes=uo;n.second=n.seconds=fo;n.millisecond=n.milliseconds=eo;n.utcOffset=bs;n.utc=ds;n.local=gs;n.parseZone=nh;n.hasAlignedHourOffset=th;n.isDST=ih;n.isDSTShifted=rh;n.isLocal=uh;n.isUtcOffset=fh;n.isUtc=pf;n.isUTC=pf;n.zoneAbbr=bl;n.zoneName=kl;n.dates=b("dates accessor is deprecated. Use date instead.",au);n.months=b("months accessor is deprecated. Use month instead",ef);n.years=b("years accessor is deprecated. Use year instead",cu);n.zone=b("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",ks);var oo=n,u=lr.prototype;u._calendar={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"};u.calendar=na;u._longDateFormat={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"};u.longDateFormat=ta;u._invalidDate="Invalid date";u.invalidDate=ia;u._ordinal="%d";u.ordinal=ra;u._ordinalParse=/\d{1,2}/;u.preparse=ue;u.postformat=ue;u._relativeTime={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};u.relativeTime=ua;u.pastFuture=fa;u.set=co;u.months=go;u._months=fv;u.monthsShort=ns;u._monthsShort=ev;u.monthsParse=ts;u._monthsRegex=sv;u.monthsRegex=us;u._monthsShortRegex=ov;u.monthsShortRegex=rs;u.week=nl;u._week=to;u.firstDayOfYear=il;u.firstDayOfWeek=tl;u.weekdays=el;u._weekdays=wv;u.weekdaysMin=sl;u._weekdaysMin=kv;u.weekdaysShort=ol;u._weekdaysShort=bv;u.weekdaysParse=hl;u.isPM=yl;u._meridiemParse=io;u.meridiem=pl;gt("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(n){var t=n%10,i=1===f(n%100/10)?"th":1===t?"st":2===t?"nd":3===t?"rd":"th";return n+i}});i.lang=b("moment.lang is deprecated. Use moment.locale instead.",gt);i.langData=b("moment.langData is deprecated. Use moment.localeData instead.",yt);var ft=Math.abs,dv=ht("ms"),gv=ht("s"),ny=ht("m"),ty=ht("h"),iy=ht("d"),ry=ht("w"),uy=ht("M"),fy=ht("y"),ey=bt("milliseconds"),oy=bt("seconds"),sy=bt("minutes"),hy=bt("hours"),cy=bt("days"),ly=bt("months"),ay=bt("years"),ri=Math.round,lt={s:45,m:45,h:22,d:26,M:11},vu=Math.abs,o=bi.prototype;return o.abs=la,o.add=aa,o.subtract=va,o.as=pa,o.asMilliseconds=dv,o.asSeconds=gv,o.asMinutes=ny,o.asHours=ty,o.asDays=iy,o.asWeeks=ry,o.asMonths=uy,o.asYears=fy,o.valueOf=wa,o._bubble=ya,o.get=ba,o.milliseconds=ey,o.seconds=oy,o.minutes=sy,o.hours=hy,o.days=cy,o.weeks=ka,o.months=ly,o.years=ay,o.humanize=tv,o.toISOString=di,o.toString=di,o.toJSON=di,o.locale=gf,o.localeData=ne,o.toIsoString=b("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",di),o.lang=lu,r("X",0,0,"unix"),r("x",0,0,"valueOf"),t("x",ur),t("X",/[+-]?\d+(\.\d{1,3})?/),s("X",function(n,t,i){i._d=new Date(1e3*parseFloat(n,10))}),s("x",function(n,t,i){i._d=new Date(f(n))}),i.version="2.12.0",so(h),i.fn=oo,i.min=ps,i.max=ws,i.now=pv,i.utc=dt,i.unix=dl,i.months=ea,i.isDate=li,i.locale=gt,i.invalid=ai,i.duration=st,i.isMoment=et,i.weekdays=sa,i.parseZone=gl,i.localeData=yt,i.isDuration=br,i.monthsShort=oa,i.weekdaysMin=ca,i.defineLocale=gu,i.updateLocale=ao,i.locales=vo,i.weekdaysShort=ha,i.normalizeUnits=k,i.relativeTimeThreshold=nv,i.prototype=oo,i}); +//! momentjs.com diff --git a/PlexRequests.UI/Content/moment.min.js b/PlexRequests.UI/Content/moment.min.js new file mode 100644 index 000000000..28e8f2b50 --- /dev/null +++ b/PlexRequests.UI/Content/moment.min.js @@ -0,0 +1,10 @@ +//! moment.js +//! version : 2.12.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function (a, b) { "object" == typeof exports && "undefined" != typeof module ? module.exports = b() : "function" == typeof define && define.amd ? define(b) : a.moment = b() }(this, function () { + "use strict"; function a() { return Zc.apply(null, arguments) } function b(a) { Zc = a } function c(a) { return a instanceof Array || "[object Array]" === Object.prototype.toString.call(a) } function d(a) { return a instanceof Date || "[object Date]" === Object.prototype.toString.call(a) } function e(a, b) { var c, d = []; for (c = 0; c < a.length; ++c) d.push(b(a[c], c)); return d } function f(a, b) { return Object.prototype.hasOwnProperty.call(a, b) } function g(a, b) { for (var c in b) f(b, c) && (a[c] = b[c]); return f(b, "toString") && (a.toString = b.toString), f(b, "valueOf") && (a.valueOf = b.valueOf), a } function h(a, b, c, d) { return Ia(a, b, c, d, !0).utc() } function i() { return { empty: !1, unusedTokens: [], unusedInput: [], overflow: -2, charsLeftOver: 0, nullInput: !1, invalidMonth: null, invalidFormat: !1, userInvalidated: !1, iso: !1 } } function j(a) { return null == a._pf && (a._pf = i()), a._pf } function k(a) { if (null == a._isValid) { var b = j(a); a._isValid = !(isNaN(a._d.getTime()) || !(b.overflow < 0) || b.empty || b.invalidMonth || b.invalidWeekday || b.nullInput || b.invalidFormat || b.userInvalidated), a._strict && (a._isValid = a._isValid && 0 === b.charsLeftOver && 0 === b.unusedTokens.length && void 0 === b.bigHour) } return a._isValid } function l(a) { var b = h(NaN); return null != a ? g(j(b), a) : j(b).userInvalidated = !0, b } function m(a) { return void 0 === a } function n(a, b) { var c, d, e; if (m(b._isAMomentObject) || (a._isAMomentObject = b._isAMomentObject), m(b._i) || (a._i = b._i), m(b._f) || (a._f = b._f), m(b._l) || (a._l = b._l), m(b._strict) || (a._strict = b._strict), m(b._tzm) || (a._tzm = b._tzm), m(b._isUTC) || (a._isUTC = b._isUTC), m(b._offset) || (a._offset = b._offset), m(b._pf) || (a._pf = j(b)), m(b._locale) || (a._locale = b._locale), $c.length > 0) for (c in $c) d = $c[c], e = b[d], m(e) || (a[d] = e); return a } function o(b) { n(this, b), this._d = new Date(null != b._d ? b._d.getTime() : NaN), _c === !1 && (_c = !0, a.updateOffset(this), _c = !1) } function p(a) { return a instanceof o || null != a && null != a._isAMomentObject } function q(a) { return 0 > a ? Math.ceil(a) : Math.floor(a) } function r(a) { var b = +a, c = 0; return 0 !== b && isFinite(b) && (c = q(b)), c } function s(a, b, c) { var d, e = Math.min(a.length, b.length), f = Math.abs(a.length - b.length), g = 0; for (d = 0; e > d; d++) (c && a[d] !== b[d] || !c && r(a[d]) !== r(b[d])) && g++; return g + f } function t(b) { a.suppressDeprecationWarnings === !1 && "undefined" != typeof console && console.warn && console.warn("Deprecation warning: " + b) } function u(a, b) { var c = !0; return g(function () { return c && (t(a + "\nArguments: " + Array.prototype.slice.call(arguments).join(", ") + "\n" + (new Error).stack), c = !1), b.apply(this, arguments) }, b) } function v(a, b) { ad[a] || (t(b), ad[a] = !0) } function w(a) { return a instanceof Function || "[object Function]" === Object.prototype.toString.call(a) } function x(a) { return "[object Object]" === Object.prototype.toString.call(a) } function y(a) { var b, c; for (c in a) b = a[c], w(b) ? this[c] = b : this["_" + c] = b; this._config = a, this._ordinalParseLenient = new RegExp(this._ordinalParse.source + "|" + /\d{1,2}/.source) } function z(a, b) { var c, d = g({}, a); for (c in b) f(b, c) && (x(a[c]) && x(b[c]) ? (d[c] = {}, g(d[c], a[c]), g(d[c], b[c])) : null != b[c] ? d[c] = b[c] : delete d[c]); return d } function A(a) { null != a && this.set(a) } function B(a) { return a ? a.toLowerCase().replace("_", "-") : a } function C(a) { for (var b, c, d, e, f = 0; f < a.length;) { for (e = B(a[f]).split("-"), b = e.length, c = B(a[f + 1]), c = c ? c.split("-") : null; b > 0;) { if (d = D(e.slice(0, b).join("-"))) return d; if (c && c.length >= b && s(e, c, !0) >= b - 1) break; b-- } f++ } return null } function D(a) { var b = null; if (!cd[a] && "undefined" != typeof module && module && module.exports) try { b = bd._abbr, require("./locale/" + a), E(b) } catch (c) { } return cd[a] } function E(a, b) { var c; return a && (c = m(b) ? H(a) : F(a, b), c && (bd = c)), bd._abbr } function F(a, b) { return null !== b ? (b.abbr = a, null != cd[a] ? (v("defineLocaleOverride", "use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"), b = z(cd[a]._config, b)) : null != b.parentLocale && (null != cd[b.parentLocale] ? b = z(cd[b.parentLocale]._config, b) : v("parentLocaleUndefined", "specified parentLocale is not defined yet")), cd[a] = new A(b), E(a), cd[a]) : (delete cd[a], null) } function G(a, b) { if (null != b) { var c; null != cd[a] && (b = z(cd[a]._config, b)), c = new A(b), c.parentLocale = cd[a], cd[a] = c, E(a) } else null != cd[a] && (null != cd[a].parentLocale ? cd[a] = cd[a].parentLocale : null != cd[a] && delete cd[a]); return cd[a] } function H(a) { var b; if (a && a._locale && a._locale._abbr && (a = a._locale._abbr), !a) return bd; if (!c(a)) { if (b = D(a)) return b; a = [a] } return C(a) } function I() { return Object.keys(cd) } function J(a, b) { var c = a.toLowerCase(); dd[c] = dd[c + "s"] = dd[b] = a } function K(a) { return "string" == typeof a ? dd[a] || dd[a.toLowerCase()] : void 0 } function L(a) { var b, c, d = {}; for (c in a) f(a, c) && (b = K(c), b && (d[b] = a[c])); return d } function M(b, c) { return function (d) { return null != d ? (O(this, b, d), a.updateOffset(this, c), this) : N(this, b) } } function N(a, b) { return a.isValid() ? a._d["get" + (a._isUTC ? "UTC" : "") + b]() : NaN } function O(a, b, c) { a.isValid() && a._d["set" + (a._isUTC ? "UTC" : "") + b](c) } function P(a, b) { var c; if ("object" == typeof a) for (c in a) this.set(c, a[c]); else if (a = K(a), w(this[a])) return this[a](b); return this } function Q(a, b, c) { var d = "" + Math.abs(a), e = b - d.length, f = a >= 0; return (f ? c ? "+" : "" : "-") + Math.pow(10, Math.max(0, e)).toString().substr(1) + d } function R(a, b, c, d) { var e = d; "string" == typeof d && (e = function () { return this[d]() }), a && (hd[a] = e), b && (hd[b[0]] = function () { return Q(e.apply(this, arguments), b[1], b[2]) }), c && (hd[c] = function () { return this.localeData().ordinal(e.apply(this, arguments), a) }) } function S(a) { return a.match(/\[[\s\S]/) ? a.replace(/^\[|\]$/g, "") : a.replace(/\\/g, "") } function T(a) { var b, c, d = a.match(ed); for (b = 0, c = d.length; c > b; b++) hd[d[b]] ? d[b] = hd[d[b]] : d[b] = S(d[b]); return function (e) { var f = ""; for (b = 0; c > b; b++) f += d[b] instanceof Function ? d[b].call(e, a) : d[b]; return f } } function U(a, b) { return a.isValid() ? (b = V(b, a.localeData()), gd[b] = gd[b] || T(b), gd[b](a)) : a.localeData().invalidDate() } function V(a, b) { function c(a) { return b.longDateFormat(a) || a } var d = 5; for (fd.lastIndex = 0; d >= 0 && fd.test(a) ;) a = a.replace(fd, c), fd.lastIndex = 0, d -= 1; return a } function W(a, b, c) { zd[a] = w(b) ? b : function (a, d) { return a && c ? c : b } } function X(a, b) { return f(zd, a) ? zd[a](b._strict, b._locale) : new RegExp(Y(a)) } function Y(a) { return Z(a.replace("\\", "").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (a, b, c, d, e) { return b || c || d || e })) } function Z(a) { return a.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") } function $(a, b) { var c, d = b; for ("string" == typeof a && (a = [a]), "number" == typeof b && (d = function (a, c) { c[b] = r(a) }), c = 0; c < a.length; c++) Ad[a[c]] = d } function _(a, b) { $(a, function (a, c, d, e) { d._w = d._w || {}, b(a, d._w, d, e) }) } function aa(a, b, c) { null != b && f(Ad, a) && Ad[a](b, c._a, c, a) } function ba(a, b) { return new Date(Date.UTC(a, b + 1, 0)).getUTCDate() } function ca(a, b) { return c(this._months) ? this._months[a.month()] : this._months[Kd.test(b) ? "format" : "standalone"][a.month()] } function da(a, b) { return c(this._monthsShort) ? this._monthsShort[a.month()] : this._monthsShort[Kd.test(b) ? "format" : "standalone"][a.month()] } function ea(a, b, c) { var d, e, f; for (this._monthsParse || (this._monthsParse = [], this._longMonthsParse = [], this._shortMonthsParse = []), d = 0; 12 > d; d++) { if (e = h([2e3, d]), c && !this._longMonthsParse[d] && (this._longMonthsParse[d] = new RegExp("^" + this.months(e, "").replace(".", "") + "$", "i"), this._shortMonthsParse[d] = new RegExp("^" + this.monthsShort(e, "").replace(".", "") + "$", "i")), c || this._monthsParse[d] || (f = "^" + this.months(e, "") + "|^" + this.monthsShort(e, ""), this._monthsParse[d] = new RegExp(f.replace(".", ""), "i")), c && "MMMM" === b && this._longMonthsParse[d].test(a)) return d; if (c && "MMM" === b && this._shortMonthsParse[d].test(a)) return d; if (!c && this._monthsParse[d].test(a)) return d } } function fa(a, b) { var c; if (!a.isValid()) return a; if ("string" == typeof b) if (/^\d+$/.test(b)) b = r(b); else if (b = a.localeData().monthsParse(b), "number" != typeof b) return a; return c = Math.min(a.date(), ba(a.year(), b)), a._d["set" + (a._isUTC ? "UTC" : "") + "Month"](b, c), a } function ga(b) { return null != b ? (fa(this, b), a.updateOffset(this, !0), this) : N(this, "Month") } function ha() { return ba(this.year(), this.month()) } function ia(a) { return this._monthsParseExact ? (f(this, "_monthsRegex") || ka.call(this), a ? this._monthsShortStrictRegex : this._monthsShortRegex) : this._monthsShortStrictRegex && a ? this._monthsShortStrictRegex : this._monthsShortRegex } function ja(a) { return this._monthsParseExact ? (f(this, "_monthsRegex") || ka.call(this), a ? this._monthsStrictRegex : this._monthsRegex) : this._monthsStrictRegex && a ? this._monthsStrictRegex : this._monthsRegex } function ka() { function a(a, b) { return b.length - a.length } var b, c, d = [], e = [], f = []; for (b = 0; 12 > b; b++) c = h([2e3, b]), d.push(this.monthsShort(c, "")), e.push(this.months(c, "")), f.push(this.months(c, "")), f.push(this.monthsShort(c, "")); for (d.sort(a), e.sort(a), f.sort(a), b = 0; 12 > b; b++) d[b] = Z(d[b]), e[b] = Z(e[b]), f[b] = Z(f[b]); this._monthsRegex = new RegExp("^(" + f.join("|") + ")", "i"), this._monthsShortRegex = this._monthsRegex, this._monthsStrictRegex = new RegExp("^(" + e.join("|") + ")$", "i"), this._monthsShortStrictRegex = new RegExp("^(" + d.join("|") + ")$", "i") } function la(a) { var b, c = a._a; return c && -2 === j(a).overflow && (b = c[Cd] < 0 || c[Cd] > 11 ? Cd : c[Dd] < 1 || c[Dd] > ba(c[Bd], c[Cd]) ? Dd : c[Ed] < 0 || c[Ed] > 24 || 24 === c[Ed] && (0 !== c[Fd] || 0 !== c[Gd] || 0 !== c[Hd]) ? Ed : c[Fd] < 0 || c[Fd] > 59 ? Fd : c[Gd] < 0 || c[Gd] > 59 ? Gd : c[Hd] < 0 || c[Hd] > 999 ? Hd : -1, j(a)._overflowDayOfYear && (Bd > b || b > Dd) && (b = Dd), j(a)._overflowWeeks && -1 === b && (b = Id), j(a)._overflowWeekday && -1 === b && (b = Jd), j(a).overflow = b), a } function ma(a) { var b, c, d, e, f, g, h = a._i, i = Pd.exec(h) || Qd.exec(h); if (i) { for (j(a).iso = !0, b = 0, c = Sd.length; c > b; b++) if (Sd[b][1].exec(i[1])) { e = Sd[b][0], d = Sd[b][2] !== !1; break } if (null == e) return void (a._isValid = !1); if (i[3]) { for (b = 0, c = Td.length; c > b; b++) if (Td[b][1].exec(i[3])) { f = (i[2] || " ") + Td[b][0]; break } if (null == f) return void (a._isValid = !1) } if (!d && null != f) return void (a._isValid = !1); if (i[4]) { if (!Rd.exec(i[4])) return void (a._isValid = !1); g = "Z" } a._f = e + (f || "") + (g || ""), Ba(a) } else a._isValid = !1 } function na(b) { var c = Ud.exec(b._i); return null !== c ? void (b._d = new Date(+c[1])) : (ma(b), void (b._isValid === !1 && (delete b._isValid, a.createFromInputFallback(b)))) } function oa(a, b, c, d, e, f, g) { var h = new Date(a, b, c, d, e, f, g); return 100 > a && a >= 0 && isFinite(h.getFullYear()) && h.setFullYear(a), h } function pa(a) { var b = new Date(Date.UTC.apply(null, arguments)); return 100 > a && a >= 0 && isFinite(b.getUTCFullYear()) && b.setUTCFullYear(a), b } function qa(a) { return ra(a) ? 366 : 365 } function ra(a) { return a % 4 === 0 && a % 100 !== 0 || a % 400 === 0 } function sa() { return ra(this.year()) } function ta(a, b, c) { var d = 7 + b - c, e = (7 + pa(a, 0, d).getUTCDay() - b) % 7; return -e + d - 1 } function ua(a, b, c, d, e) { var f, g, h = (7 + c - d) % 7, i = ta(a, d, e), j = 1 + 7 * (b - 1) + h + i; return 0 >= j ? (f = a - 1, g = qa(f) + j) : j > qa(a) ? (f = a + 1, g = j - qa(a)) : (f = a, g = j), { year: f, dayOfYear: g } } function va(a, b, c) { var d, e, f = ta(a.year(), b, c), g = Math.floor((a.dayOfYear() - f - 1) / 7) + 1; return 1 > g ? (e = a.year() - 1, d = g + wa(e, b, c)) : g > wa(a.year(), b, c) ? (d = g - wa(a.year(), b, c), e = a.year() + 1) : (e = a.year(), d = g), { week: d, year: e } } function wa(a, b, c) { var d = ta(a, b, c), e = ta(a + 1, b, c); return (qa(a) - d + e) / 7 } function xa(a, b, c) { return null != a ? a : null != b ? b : c } function ya(b) { var c = new Date(a.now()); return b._useUTC ? [c.getUTCFullYear(), c.getUTCMonth(), c.getUTCDate()] : [c.getFullYear(), c.getMonth(), c.getDate()] } function za(a) { var b, c, d, e, f = []; if (!a._d) { for (d = ya(a), a._w && null == a._a[Dd] && null == a._a[Cd] && Aa(a), a._dayOfYear && (e = xa(a._a[Bd], d[Bd]), a._dayOfYear > qa(e) && (j(a)._overflowDayOfYear = !0), c = pa(e, 0, a._dayOfYear), a._a[Cd] = c.getUTCMonth(), a._a[Dd] = c.getUTCDate()), b = 0; 3 > b && null == a._a[b]; ++b) a._a[b] = f[b] = d[b]; for (; 7 > b; b++) a._a[b] = f[b] = null == a._a[b] ? 2 === b ? 1 : 0 : a._a[b]; 24 === a._a[Ed] && 0 === a._a[Fd] && 0 === a._a[Gd] && 0 === a._a[Hd] && (a._nextDay = !0, a._a[Ed] = 0), a._d = (a._useUTC ? pa : oa).apply(null, f), null != a._tzm && a._d.setUTCMinutes(a._d.getUTCMinutes() - a._tzm), a._nextDay && (a._a[Ed] = 24) } } function Aa(a) { var b, c, d, e, f, g, h, i; b = a._w, null != b.GG || null != b.W || null != b.E ? (f = 1, g = 4, c = xa(b.GG, a._a[Bd], va(Ja(), 1, 4).year), d = xa(b.W, 1), e = xa(b.E, 1), (1 > e || e > 7) && (i = !0)) : (f = a._locale._week.dow, g = a._locale._week.doy, c = xa(b.gg, a._a[Bd], va(Ja(), f, g).year), d = xa(b.w, 1), null != b.d ? (e = b.d, (0 > e || e > 6) && (i = !0)) : null != b.e ? (e = b.e + f, (b.e < 0 || b.e > 6) && (i = !0)) : e = f), 1 > d || d > wa(c, f, g) ? j(a)._overflowWeeks = !0 : null != i ? j(a)._overflowWeekday = !0 : (h = ua(c, d, e, f, g), a._a[Bd] = h.year, a._dayOfYear = h.dayOfYear) } function Ba(b) { if (b._f === a.ISO_8601) return void ma(b); b._a = [], j(b).empty = !0; var c, d, e, f, g, h = "" + b._i, i = h.length, k = 0; for (e = V(b._f, b._locale).match(ed) || [], c = 0; c < e.length; c++) f = e[c], d = (h.match(X(f, b)) || [])[0], d && (g = h.substr(0, h.indexOf(d)), g.length > 0 && j(b).unusedInput.push(g), h = h.slice(h.indexOf(d) + d.length), k += d.length), hd[f] ? (d ? j(b).empty = !1 : j(b).unusedTokens.push(f), aa(f, d, b)) : b._strict && !d && j(b).unusedTokens.push(f); j(b).charsLeftOver = i - k, h.length > 0 && j(b).unusedInput.push(h), j(b).bigHour === !0 && b._a[Ed] <= 12 && b._a[Ed] > 0 && (j(b).bigHour = void 0), b._a[Ed] = Ca(b._locale, b._a[Ed], b._meridiem), za(b), la(b) } function Ca(a, b, c) { var d; return null == c ? b : null != a.meridiemHour ? a.meridiemHour(b, c) : null != a.isPM ? (d = a.isPM(c), d && 12 > b && (b += 12), d || 12 !== b || (b = 0), b) : b } function Da(a) { var b, c, d, e, f; if (0 === a._f.length) return j(a).invalidFormat = !0, void (a._d = new Date(NaN)); for (e = 0; e < a._f.length; e++) f = 0, b = n({}, a), null != a._useUTC && (b._useUTC = a._useUTC), b._f = a._f[e], Ba(b), k(b) && (f += j(b).charsLeftOver, f += 10 * j(b).unusedTokens.length, j(b).score = f, (null == d || d > f) && (d = f, c = b)); g(a, c || b) } function Ea(a) { if (!a._d) { var b = L(a._i); a._a = e([b.year, b.month, b.day || b.date, b.hour, b.minute, b.second, b.millisecond], function (a) { return a && parseInt(a, 10) }), za(a) } } function Fa(a) { var b = new o(la(Ga(a))); return b._nextDay && (b.add(1, "d"), b._nextDay = void 0), b } function Ga(a) { var b = a._i, e = a._f; return a._locale = a._locale || H(a._l), null === b || void 0 === e && "" === b ? l({ nullInput: !0 }) : ("string" == typeof b && (a._i = b = a._locale.preparse(b)), p(b) ? new o(la(b)) : (c(e) ? Da(a) : e ? Ba(a) : d(b) ? a._d = b : Ha(a), k(a) || (a._d = null), a)) } function Ha(b) { var f = b._i; void 0 === f ? b._d = new Date(a.now()) : d(f) ? b._d = new Date(+f) : "string" == typeof f ? na(b) : c(f) ? (b._a = e(f.slice(0), function (a) { return parseInt(a, 10) }), za(b)) : "object" == typeof f ? Ea(b) : "number" == typeof f ? b._d = new Date(f) : a.createFromInputFallback(b) } function Ia(a, b, c, d, e) { var f = {}; return "boolean" == typeof c && (d = c, c = void 0), f._isAMomentObject = !0, f._useUTC = f._isUTC = e, f._l = c, f._i = a, f._f = b, f._strict = d, Fa(f) } function Ja(a, b, c, d) { return Ia(a, b, c, d, !1) } function Ka(a, b) { var d, e; if (1 === b.length && c(b[0]) && (b = b[0]), !b.length) return Ja(); for (d = b[0], e = 1; e < b.length; ++e) (!b[e].isValid() || b[e][a](d)) && (d = b[e]); return d } function La() { var a = [].slice.call(arguments, 0); return Ka("isBefore", a) } function Ma() { var a = [].slice.call(arguments, 0); return Ka("isAfter", a) } function Na(a) { var b = L(a), c = b.year || 0, d = b.quarter || 0, e = b.month || 0, f = b.week || 0, g = b.day || 0, h = b.hour || 0, i = b.minute || 0, j = b.second || 0, k = b.millisecond || 0; this._milliseconds = +k + 1e3 * j + 6e4 * i + 36e5 * h, this._days = +g + 7 * f, this._months = +e + 3 * d + 12 * c, this._data = {}, this._locale = H(), this._bubble() } function Oa(a) { return a instanceof Na } function Pa(a, b) { R(a, 0, 0, function () { var a = this.utcOffset(), c = "+"; return 0 > a && (a = -a, c = "-"), c + Q(~~(a / 60), 2) + b + Q(~~a % 60, 2) }) } function Qa(a, b) { var c = (b || "").match(a) || [], d = c[c.length - 1] || [], e = (d + "").match(Zd) || ["-", 0, 0], f = +(60 * e[1]) + r(e[2]); return "+" === e[0] ? f : -f } function Ra(b, c) { var e, f; return c._isUTC ? (e = c.clone(), f = (p(b) || d(b) ? +b : +Ja(b)) - +e, e._d.setTime(+e._d + f), a.updateOffset(e, !1), e) : Ja(b).local() } function Sa(a) { return 15 * -Math.round(a._d.getTimezoneOffset() / 15) } function Ta(b, c) { var d, e = this._offset || 0; return this.isValid() ? null != b ? ("string" == typeof b ? b = Qa(wd, b) : Math.abs(b) < 16 && (b = 60 * b), !this._isUTC && c && (d = Sa(this)), this._offset = b, this._isUTC = !0, null != d && this.add(d, "m"), e !== b && (!c || this._changeInProgress ? ib(this, cb(b - e, "m"), 1, !1) : this._changeInProgress || (this._changeInProgress = !0, a.updateOffset(this, !0), this._changeInProgress = null)), this) : this._isUTC ? e : Sa(this) : null != b ? this : NaN } function Ua(a, b) { return null != a ? ("string" != typeof a && (a = -a), this.utcOffset(a, b), this) : -this.utcOffset() } function Va(a) { return this.utcOffset(0, a) } function Wa(a) { return this._isUTC && (this.utcOffset(0, a), this._isUTC = !1, a && this.subtract(Sa(this), "m")), this } function Xa() { return this._tzm ? this.utcOffset(this._tzm) : "string" == typeof this._i && this.utcOffset(Qa(vd, this._i)), this } function Ya(a) { return this.isValid() ? (a = a ? Ja(a).utcOffset() : 0, (this.utcOffset() - a) % 60 === 0) : !1 } function Za() { return this.utcOffset() > this.clone().month(0).utcOffset() || this.utcOffset() > this.clone().month(5).utcOffset() } function $a() { if (!m(this._isDSTShifted)) return this._isDSTShifted; var a = {}; if (n(a, this), a = Ga(a), a._a) { var b = a._isUTC ? h(a._a) : Ja(a._a); this._isDSTShifted = this.isValid() && s(a._a, b.toArray()) > 0 } else this._isDSTShifted = !1; return this._isDSTShifted } function _a() { return this.isValid() ? !this._isUTC : !1 } function ab() { return this.isValid() ? this._isUTC : !1 } function bb() { return this.isValid() ? this._isUTC && 0 === this._offset : !1 } function cb(a, b) { var c, d, e, g = a, h = null; return Oa(a) ? g = { ms: a._milliseconds, d: a._days, M: a._months } : "number" == typeof a ? (g = {}, b ? g[b] = a : g.milliseconds = a) : (h = $d.exec(a)) ? (c = "-" === h[1] ? -1 : 1, g = { y: 0, d: r(h[Dd]) * c, h: r(h[Ed]) * c, m: r(h[Fd]) * c, s: r(h[Gd]) * c, ms: r(h[Hd]) * c }) : (h = _d.exec(a)) ? (c = "-" === h[1] ? -1 : 1, g = { y: db(h[2], c), M: db(h[3], c), w: db(h[4], c), d: db(h[5], c), h: db(h[6], c), m: db(h[7], c), s: db(h[8], c) }) : null == g ? g = {} : "object" == typeof g && ("from" in g || "to" in g) && (e = fb(Ja(g.from), Ja(g.to)), g = {}, g.ms = e.milliseconds, g.M = e.months), d = new Na(g), Oa(a) && f(a, "_locale") && (d._locale = a._locale), d } function db(a, b) { var c = a && parseFloat(a.replace(",", ".")); return (isNaN(c) ? 0 : c) * b } function eb(a, b) { var c = { milliseconds: 0, months: 0 }; return c.months = b.month() - a.month() + 12 * (b.year() - a.year()), a.clone().add(c.months, "M").isAfter(b) && --c.months, c.milliseconds = +b - +a.clone().add(c.months, "M"), c } function fb(a, b) { var c; return a.isValid() && b.isValid() ? (b = Ra(b, a), a.isBefore(b) ? c = eb(a, b) : (c = eb(b, a), c.milliseconds = -c.milliseconds, c.months = -c.months), c) : { milliseconds: 0, months: 0 } } function gb(a) { return 0 > a ? -1 * Math.round(-1 * a) : Math.round(a) } function hb(a, b) { return function (c, d) { var e, f; return null === d || isNaN(+d) || (v(b, "moment()." + b + "(period, number) is deprecated. Please use moment()." + b + "(number, period)."), f = c, c = d, d = f), c = "string" == typeof c ? +c : c, e = cb(c, d), ib(this, e, a), this } } function ib(b, c, d, e) { var f = c._milliseconds, g = gb(c._days), h = gb(c._months); b.isValid() && (e = null == e ? !0 : e, f && b._d.setTime(+b._d + f * d), g && O(b, "Date", N(b, "Date") + g * d), h && fa(b, N(b, "Month") + h * d), e && a.updateOffset(b, g || h)) } function jb(a, b) { var c = a || Ja(), d = Ra(c, this).startOf("day"), e = this.diff(d, "days", !0), f = -6 > e ? "sameElse" : -1 > e ? "lastWeek" : 0 > e ? "lastDay" : 1 > e ? "sameDay" : 2 > e ? "nextDay" : 7 > e ? "nextWeek" : "sameElse", g = b && (w(b[f]) ? b[f]() : b[f]); return this.format(g || this.localeData().calendar(f, this, Ja(c))) } function kb() { return new o(this) } function lb(a, b) { var c = p(a) ? a : Ja(a); return this.isValid() && c.isValid() ? (b = K(m(b) ? "millisecond" : b), "millisecond" === b ? +this > +c : +c < +this.clone().startOf(b)) : !1 } function mb(a, b) { var c = p(a) ? a : Ja(a); return this.isValid() && c.isValid() ? (b = K(m(b) ? "millisecond" : b), "millisecond" === b ? +c > +this : +this.clone().endOf(b) < +c) : !1 } function nb(a, b, c) { return this.isAfter(a, c) && this.isBefore(b, c) } function ob(a, b) { var c, d = p(a) ? a : Ja(a); return this.isValid() && d.isValid() ? (b = K(b || "millisecond"), "millisecond" === b ? +this === +d : (c = +d, +this.clone().startOf(b) <= c && c <= +this.clone().endOf(b))) : !1 } function pb(a, b) { return this.isSame(a, b) || this.isAfter(a, b) } function qb(a, b) { return this.isSame(a, b) || this.isBefore(a, b) } function rb(a, b, c) { var d, e, f, g; return this.isValid() ? (d = Ra(a, this), d.isValid() ? (e = 6e4 * (d.utcOffset() - this.utcOffset()), b = K(b), "year" === b || "month" === b || "quarter" === b ? (g = sb(this, d), "quarter" === b ? g /= 3 : "year" === b && (g /= 12)) : (f = this - d, g = "second" === b ? f / 1e3 : "minute" === b ? f / 6e4 : "hour" === b ? f / 36e5 : "day" === b ? (f - e) / 864e5 : "week" === b ? (f - e) / 6048e5 : f), c ? g : q(g)) : NaN) : NaN } function sb(a, b) { var c, d, e = 12 * (b.year() - a.year()) + (b.month() - a.month()), f = a.clone().add(e, "months"); return 0 > b - f ? (c = a.clone().add(e - 1, "months"), d = (b - f) / (f - c)) : (c = a.clone().add(e + 1, "months"), d = (b - f) / (c - f)), -(e + d) } function tb() { return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ") } function ub() { var a = this.clone().utc(); return 0 < a.year() && a.year() <= 9999 ? w(Date.prototype.toISOString) ? this.toDate().toISOString() : U(a, "YYYY-MM-DD[T]HH:mm:ss.SSS[Z]") : U(a, "YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]") } function vb(b) { var c = U(this, b || a.defaultFormat); return this.localeData().postformat(c) } function wb(a, b) { return this.isValid() && (p(a) && a.isValid() || Ja(a).isValid()) ? cb({ to: this, from: a }).locale(this.locale()).humanize(!b) : this.localeData().invalidDate() } function xb(a) { return this.from(Ja(), a) } function yb(a, b) { return this.isValid() && (p(a) && a.isValid() || Ja(a).isValid()) ? cb({ from: this, to: a }).locale(this.locale()).humanize(!b) : this.localeData().invalidDate() } function zb(a) { return this.to(Ja(), a) } function Ab(a) { var b; return void 0 === a ? this._locale._abbr : (b = H(a), null != b && (this._locale = b), this) } function Bb() { return this._locale } function Cb(a) { switch (a = K(a)) { case "year": this.month(0); case "quarter": case "month": this.date(1); case "week": case "isoWeek": case "day": this.hours(0); case "hour": this.minutes(0); case "minute": this.seconds(0); case "second": this.milliseconds(0) } return "week" === a && this.weekday(0), "isoWeek" === a && this.isoWeekday(1), "quarter" === a && this.month(3 * Math.floor(this.month() / 3)), this } function Db(a) { return a = K(a), void 0 === a || "millisecond" === a ? this : this.startOf(a).add(1, "isoWeek" === a ? "week" : a).subtract(1, "ms") } function Eb() { return +this._d - 6e4 * (this._offset || 0) } function Fb() { return Math.floor(+this / 1e3) } function Gb() { return this._offset ? new Date(+this) : this._d } function Hb() { var a = this; return [a.year(), a.month(), a.date(), a.hour(), a.minute(), a.second(), a.millisecond()] } function Ib() { var a = this; return { years: a.year(), months: a.month(), date: a.date(), hours: a.hours(), minutes: a.minutes(), seconds: a.seconds(), milliseconds: a.milliseconds() } } function Jb() { return this.isValid() ? this.toISOString() : null } function Kb() { return k(this) } function Lb() { return g({}, j(this)) } function Mb() { return j(this).overflow } function Nb() { return { input: this._i, format: this._f, locale: this._locale, isUTC: this._isUTC, strict: this._strict } } function Ob(a, b) { R(0, [a, a.length], 0, b) } function Pb(a) { return Tb.call(this, a, this.week(), this.weekday(), this.localeData()._week.dow, this.localeData()._week.doy) } function Qb(a) { return Tb.call(this, a, this.isoWeek(), this.isoWeekday(), 1, 4) } function Rb() { return wa(this.year(), 1, 4) } function Sb() { var a = this.localeData()._week; return wa(this.year(), a.dow, a.doy) } function Tb(a, b, c, d, e) { var f; return null == a ? va(this, d, e).year : (f = wa(a, d, e), b > f && (b = f), Ub.call(this, a, b, c, d, e)) } function Ub(a, b, c, d, e) { var f = ua(a, b, c, d, e), g = pa(f.year, 0, f.dayOfYear); return this.year(g.getUTCFullYear()), this.month(g.getUTCMonth()), this.date(g.getUTCDate()), this } function Vb(a) { return null == a ? Math.ceil((this.month() + 1) / 3) : this.month(3 * (a - 1) + this.month() % 3) } function Wb(a) { return va(a, this._week.dow, this._week.doy).week } function Xb() { return this._week.dow } function Yb() { return this._week.doy } function Zb(a) { var b = this.localeData().week(this); return null == a ? b : this.add(7 * (a - b), "d") } function $b(a) { var b = va(this, 1, 4).week; return null == a ? b : this.add(7 * (a - b), "d") } function _b(a, b) { return "string" != typeof a ? a : isNaN(a) ? (a = b.weekdaysParse(a), "number" == typeof a ? a : null) : parseInt(a, 10) } function ac(a, b) { return c(this._weekdays) ? this._weekdays[a.day()] : this._weekdays[this._weekdays.isFormat.test(b) ? "format" : "standalone"][a.day()] } function bc(a) { return this._weekdaysShort[a.day()] } function cc(a) { return this._weekdaysMin[a.day()] } function dc(a, b, c) { var d, e, f; for (this._weekdaysParse || (this._weekdaysParse = [], this._minWeekdaysParse = [], this._shortWeekdaysParse = [], this._fullWeekdaysParse = []), d = 0; 7 > d; d++) { if (e = Ja([2e3, 1]).day(d), c && !this._fullWeekdaysParse[d] && (this._fullWeekdaysParse[d] = new RegExp("^" + this.weekdays(e, "").replace(".", ".?") + "$", "i"), this._shortWeekdaysParse[d] = new RegExp("^" + this.weekdaysShort(e, "").replace(".", ".?") + "$", "i"), this._minWeekdaysParse[d] = new RegExp("^" + this.weekdaysMin(e, "").replace(".", ".?") + "$", "i")), this._weekdaysParse[d] || (f = "^" + this.weekdays(e, "") + "|^" + this.weekdaysShort(e, "") + "|^" + this.weekdaysMin(e, ""), this._weekdaysParse[d] = new RegExp(f.replace(".", ""), "i")), c && "dddd" === b && this._fullWeekdaysParse[d].test(a)) return d; if (c && "ddd" === b && this._shortWeekdaysParse[d].test(a)) return d; if (c && "dd" === b && this._minWeekdaysParse[d].test(a)) return d; if (!c && this._weekdaysParse[d].test(a)) return d } } function ec(a) { if (!this.isValid()) return null != a ? this : NaN; var b = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); return null != a ? (a = _b(a, this.localeData()), this.add(a - b, "d")) : b } function fc(a) { if (!this.isValid()) return null != a ? this : NaN; var b = (this.day() + 7 - this.localeData()._week.dow) % 7; return null == a ? b : this.add(a - b, "d") } function gc(a) { return this.isValid() ? null == a ? this.day() || 7 : this.day(this.day() % 7 ? a : a - 7) : null != a ? this : NaN } function hc(a) { var b = Math.round((this.clone().startOf("day") - this.clone().startOf("year")) / 864e5) + 1; return null == a ? b : this.add(a - b, "d") } function ic() { return this.hours() % 12 || 12 } function jc(a, b) { R(a, 0, 0, function () { return this.localeData().meridiem(this.hours(), this.minutes(), b) }) } function kc(a, b) { return b._meridiemParse } function lc(a) { return "p" === (a + "").toLowerCase().charAt(0) } function mc(a, b, c) { return a > 11 ? c ? "pm" : "PM" : c ? "am" : "AM" } function nc(a, b) { b[Hd] = r(1e3 * ("0." + a)) } function oc() { return this._isUTC ? "UTC" : "" } function pc() { return this._isUTC ? "Coordinated Universal Time" : "" } function qc(a) { return Ja(1e3 * a) } function rc() { return Ja.apply(null, arguments).parseZone() } function sc(a, b, c) { var d = this._calendar[a]; return w(d) ? d.call(b, c) : d } function tc(a) { var b = this._longDateFormat[a], c = this._longDateFormat[a.toUpperCase()]; return b || !c ? b : (this._longDateFormat[a] = c.replace(/MMMM|MM|DD|dddd/g, function (a) { return a.slice(1) }), this._longDateFormat[a]) } function uc() { return this._invalidDate } function vc(a) { return this._ordinal.replace("%d", a) } function wc(a) { return a } function xc(a, b, c, d) { var e = this._relativeTime[c]; return w(e) ? e(a, b, c, d) : e.replace(/%d/i, a) } function yc(a, b) { var c = this._relativeTime[a > 0 ? "future" : "past"]; return w(c) ? c(b) : c.replace(/%s/i, b) } function zc(a, b, c, d) { var e = H(), f = h().set(d, b); return e[c](f, a) } function Ac(a, b, c, d, e) { if ("number" == typeof a && (b = a, a = void 0), a = a || "", null != b) return zc(a, b, c, e); var f, g = []; for (f = 0; d > f; f++) g[f] = zc(a, f, c, e); return g } function Bc(a, b) { return Ac(a, b, "months", 12, "month") } function Cc(a, b) { return Ac(a, b, "monthsShort", 12, "month") } function Dc(a, b) { return Ac(a, b, "weekdays", 7, "day") } function Ec(a, b) { return Ac(a, b, "weekdaysShort", 7, "day") } function Fc(a, b) { return Ac(a, b, "weekdaysMin", 7, "day") } function Gc() { var a = this._data; return this._milliseconds = xe(this._milliseconds), this._days = xe(this._days), this._months = xe(this._months), a.milliseconds = xe(a.milliseconds), a.seconds = xe(a.seconds), a.minutes = xe(a.minutes), a.hours = xe(a.hours), a.months = xe(a.months), a.years = xe(a.years), this } function Hc(a, b, c, d) { var e = cb(b, c); return a._milliseconds += d * e._milliseconds, a._days += d * e._days, a._months += d * e._months, a._bubble() } function Ic(a, b) { return Hc(this, a, b, 1) } function Jc(a, b) { return Hc(this, a, b, -1) } function Kc(a) { return 0 > a ? Math.floor(a) : Math.ceil(a) } function Lc() { var a, b, c, d, e, f = this._milliseconds, g = this._days, h = this._months, i = this._data; return f >= 0 && g >= 0 && h >= 0 || 0 >= f && 0 >= g && 0 >= h || (f += 864e5 * Kc(Nc(h) + g), g = 0, h = 0), i.milliseconds = f % 1e3, a = q(f / 1e3), i.seconds = a % 60, b = q(a / 60), i.minutes = b % 60, c = q(b / 60), i.hours = c % 24, g += q(c / 24), e = q(Mc(g)), h += e, g -= Kc(Nc(e)), d = q(h / 12), h %= 12, i.days = g, i.months = h, i.years = d, this } function Mc(a) { return 4800 * a / 146097 } function Nc(a) { return 146097 * a / 4800 } function Oc(a) { var b, c, d = this._milliseconds; if (a = K(a), "month" === a || "year" === a) return b = this._days + d / 864e5, c = this._months + Mc(b), "month" === a ? c : c / 12; switch (b = this._days + Math.round(Nc(this._months)), a) { case "week": return b / 7 + d / 6048e5; case "day": return b + d / 864e5; case "hour": return 24 * b + d / 36e5; case "minute": return 1440 * b + d / 6e4; case "second": return 86400 * b + d / 1e3; case "millisecond": return Math.floor(864e5 * b) + d; default: throw new Error("Unknown unit " + a) } } function Pc() { return this._milliseconds + 864e5 * this._days + this._months % 12 * 2592e6 + 31536e6 * r(this._months / 12) } function Qc(a) { return function () { return this.as(a) } } function Rc(a) { return a = K(a), this[a + "s"]() } function Sc(a) { return function () { return this._data[a] } } function Tc() { return q(this.days() / 7) } function Uc(a, b, c, d, e) { return e.relativeTime(b || 1, !!c, a, d) } function Vc(a, b, c) { var d = cb(a).abs(), e = Ne(d.as("s")), f = Ne(d.as("m")), g = Ne(d.as("h")), h = Ne(d.as("d")), i = Ne(d.as("M")), j = Ne(d.as("y")), k = e < Oe.s && ["s", e] || 1 >= f && ["m"] || f < Oe.m && ["mm", f] || 1 >= g && ["h"] || g < Oe.h && ["hh", g] || 1 >= h && ["d"] || h < Oe.d && ["dd", h] || 1 >= i && ["M"] || i < Oe.M && ["MM", i] || 1 >= j && ["y"] || ["yy", j]; return k[2] = b, k[3] = +a > 0, k[4] = c, Uc.apply(null, k) } function Wc(a, b) { return void 0 === Oe[a] ? !1 : void 0 === b ? Oe[a] : (Oe[a] = b, !0) } function Xc(a) { var b = this.localeData(), c = Vc(this, !a, b); return a && (c = b.pastFuture(+this, c)), b.postformat(c) } function Yc() { var a, b, c, d = Pe(this._milliseconds) / 1e3, e = Pe(this._days), f = Pe(this._months); a = q(d / 60), b = q(a / 60), d %= 60, a %= 60, c = q(f / 12), f %= 12; var g = c, h = f, i = e, j = b, k = a, l = d, m = this.asSeconds(); return m ? (0 > m ? "-" : "") + "P" + (g ? g + "Y" : "") + (h ? h + "M" : "") + (i ? i + "D" : "") + (j || k || l ? "T" : "") + (j ? j + "H" : "") + (k ? k + "M" : "") + (l ? l + "S" : "") : "P0D" } var Zc, $c = a.momentProperties = [], _c = !1, ad = {}; a.suppressDeprecationWarnings = !1; var bd, cd = {}, dd = {}, ed = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, fd = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, gd = {}, hd = {}, id = /\d/, jd = /\d\d/, kd = /\d{3}/, ld = /\d{4}/, md = /[+-]?\d{6}/, nd = /\d\d?/, od = /\d\d\d\d?/, pd = /\d\d\d\d\d\d?/, qd = /\d{1,3}/, rd = /\d{1,4}/, sd = /[+-]?\d{1,6}/, td = /\d+/, ud = /[+-]?\d+/, vd = /Z|[+-]\d\d:?\d\d/gi, wd = /Z|[+-]\d\d(?::?\d\d)?/gi, xd = /[+-]?\d+(\.\d{1,3})?/, yd = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, zd = {}, Ad = {}, Bd = 0, Cd = 1, Dd = 2, Ed = 3, Fd = 4, Gd = 5, Hd = 6, Id = 7, Jd = 8; R("M", ["MM", 2], "Mo", function () { return this.month() + 1 }), R("MMM", 0, 0, function (a) { return this.localeData().monthsShort(this, a) }), R("MMMM", 0, 0, function (a) { return this.localeData().months(this, a) }), J("month", "M"), W("M", nd), W("MM", nd, jd), W("MMM", function (a, b) { return b.monthsShortRegex(a) }), W("MMMM", function (a, b) { return b.monthsRegex(a) }), $(["M", "MM"], function (a, b) { b[Cd] = r(a) - 1 }), $(["MMM", "MMMM"], function (a, b, c, d) { var e = c._locale.monthsParse(a, d, c._strict); null != e ? b[Cd] = e : j(c).invalidMonth = a }); var Kd = /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/, Ld = "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), Md = "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), Nd = yd, Od = yd, Pd = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/, Qd = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/, Rd = /Z|[+-]\d\d(?::?\d\d)?/, Sd = [["YYYYYY-MM-DD", /[+-]\d{6}-\d\d-\d\d/], ["YYYY-MM-DD", /\d{4}-\d\d-\d\d/], ["GGGG-[W]WW-E", /\d{4}-W\d\d-\d/], ["GGGG-[W]WW", /\d{4}-W\d\d/, !1], ["YYYY-DDD", /\d{4}-\d{3}/], ["YYYY-MM", /\d{4}-\d\d/, !1], ["YYYYYYMMDD", /[+-]\d{10}/], ["YYYYMMDD", /\d{8}/], ["GGGG[W]WWE", /\d{4}W\d{3}/], ["GGGG[W]WW", /\d{4}W\d{2}/, !1], ["YYYYDDD", /\d{7}/]], Td = [["HH:mm:ss.SSSS", /\d\d:\d\d:\d\d\.\d+/], ["HH:mm:ss,SSSS", /\d\d:\d\d:\d\d,\d+/], ["HH:mm:ss", /\d\d:\d\d:\d\d/], ["HH:mm", /\d\d:\d\d/], ["HHmmss.SSSS", /\d\d\d\d\d\d\.\d+/], ["HHmmss,SSSS", /\d\d\d\d\d\d,\d+/], ["HHmmss", /\d\d\d\d\d\d/], ["HHmm", /\d\d\d\d/], ["HH", /\d\d/]], Ud = /^\/?Date\((\-?\d+)/i; a.createFromInputFallback = u("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.", function (a) { a._d = new Date(a._i + (a._useUTC ? " UTC" : "")) }), R("Y", 0, 0, function () { var a = this.year(); return 9999 >= a ? "" + a : "+" + a }), R(0, ["YY", 2], 0, function () { return this.year() % 100 }), R(0, ["YYYY", 4], 0, "year"), R(0, ["YYYYY", 5], 0, "year"), R(0, ["YYYYYY", 6, !0], 0, "year"), J("year", "y"), W("Y", ud), W("YY", nd, jd), W("YYYY", rd, ld), W("YYYYY", sd, md), W("YYYYYY", sd, md), $(["YYYYY", "YYYYYY"], Bd), $("YYYY", function (b, c) { + c[Bd] = 2 === b.length ? a.parseTwoDigitYear(b) : r(b); + }), $("YY", function (b, c) { c[Bd] = a.parseTwoDigitYear(b) }), $("Y", function (a, b) { b[Bd] = parseInt(a, 10) }), a.parseTwoDigitYear = function (a) { return r(a) + (r(a) > 68 ? 1900 : 2e3) }; var Vd = M("FullYear", !1); a.ISO_8601 = function () { }; var Wd = u("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548", function () { var a = Ja.apply(null, arguments); return this.isValid() && a.isValid() ? this > a ? this : a : l() }), Xd = u("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548", function () { var a = Ja.apply(null, arguments); return this.isValid() && a.isValid() ? a > this ? this : a : l() }), Yd = function () { return Date.now ? Date.now() : +new Date }; Pa("Z", ":"), Pa("ZZ", ""), W("Z", wd), W("ZZ", wd), $(["Z", "ZZ"], function (a, b, c) { c._useUTC = !0, c._tzm = Qa(wd, a) }); var Zd = /([\+\-]|\d\d)/gi; a.updateOffset = function () { }; var $d = /^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/, _d = /^(-)?P(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)W)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?$/; cb.fn = Na.prototype; var ae = hb(1, "add"), be = hb(-1, "subtract"); a.defaultFormat = "YYYY-MM-DDTHH:mm:ssZ"; var ce = u("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.", function (a) { return void 0 === a ? this.localeData() : this.locale(a) }); R(0, ["gg", 2], 0, function () { return this.weekYear() % 100 }), R(0, ["GG", 2], 0, function () { return this.isoWeekYear() % 100 }), Ob("gggg", "weekYear"), Ob("ggggg", "weekYear"), Ob("GGGG", "isoWeekYear"), Ob("GGGGG", "isoWeekYear"), J("weekYear", "gg"), J("isoWeekYear", "GG"), W("G", ud), W("g", ud), W("GG", nd, jd), W("gg", nd, jd), W("GGGG", rd, ld), W("gggg", rd, ld), W("GGGGG", sd, md), W("ggggg", sd, md), _(["gggg", "ggggg", "GGGG", "GGGGG"], function (a, b, c, d) { b[d.substr(0, 2)] = r(a) }), _(["gg", "GG"], function (b, c, d, e) { c[e] = a.parseTwoDigitYear(b) }), R("Q", 0, "Qo", "quarter"), J("quarter", "Q"), W("Q", id), $("Q", function (a, b) { b[Cd] = 3 * (r(a) - 1) }), R("w", ["ww", 2], "wo", "week"), R("W", ["WW", 2], "Wo", "isoWeek"), J("week", "w"), J("isoWeek", "W"), W("w", nd), W("ww", nd, jd), W("W", nd), W("WW", nd, jd), _(["w", "ww", "W", "WW"], function (a, b, c, d) { b[d.substr(0, 1)] = r(a) }); var de = { dow: 0, doy: 6 }; R("D", ["DD", 2], "Do", "date"), J("date", "D"), W("D", nd), W("DD", nd, jd), W("Do", function (a, b) { return a ? b._ordinalParse : b._ordinalParseLenient }), $(["D", "DD"], Dd), $("Do", function (a, b) { b[Dd] = r(a.match(nd)[0], 10) }); var ee = M("Date", !0); R("d", 0, "do", "day"), R("dd", 0, 0, function (a) { return this.localeData().weekdaysMin(this, a) }), R("ddd", 0, 0, function (a) { return this.localeData().weekdaysShort(this, a) }), R("dddd", 0, 0, function (a) { return this.localeData().weekdays(this, a) }), R("e", 0, 0, "weekday"), R("E", 0, 0, "isoWeekday"), J("day", "d"), J("weekday", "e"), J("isoWeekday", "E"), W("d", nd), W("e", nd), W("E", nd), W("dd", yd), W("ddd", yd), W("dddd", yd), _(["dd", "ddd", "dddd"], function (a, b, c, d) { var e = c._locale.weekdaysParse(a, d, c._strict); null != e ? b.d = e : j(c).invalidWeekday = a }), _(["d", "e", "E"], function (a, b, c, d) { b[d] = r(a) }); var fe = "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), ge = "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), he = "Su_Mo_Tu_We_Th_Fr_Sa".split("_"); R("DDD", ["DDDD", 3], "DDDo", "dayOfYear"), J("dayOfYear", "DDD"), W("DDD", qd), W("DDDD", kd), $(["DDD", "DDDD"], function (a, b, c) { c._dayOfYear = r(a) }), R("H", ["HH", 2], 0, "hour"), R("h", ["hh", 2], 0, ic), R("hmm", 0, 0, function () { return "" + ic.apply(this) + Q(this.minutes(), 2) }), R("hmmss", 0, 0, function () { return "" + ic.apply(this) + Q(this.minutes(), 2) + Q(this.seconds(), 2) }), R("Hmm", 0, 0, function () { return "" + this.hours() + Q(this.minutes(), 2) }), R("Hmmss", 0, 0, function () { return "" + this.hours() + Q(this.minutes(), 2) + Q(this.seconds(), 2) }), jc("a", !0), jc("A", !1), J("hour", "h"), W("a", kc), W("A", kc), W("H", nd), W("h", nd), W("HH", nd, jd), W("hh", nd, jd), W("hmm", od), W("hmmss", pd), W("Hmm", od), W("Hmmss", pd), $(["H", "HH"], Ed), $(["a", "A"], function (a, b, c) { c._isPm = c._locale.isPM(a), c._meridiem = a }), $(["h", "hh"], function (a, b, c) { b[Ed] = r(a), j(c).bigHour = !0 }), $("hmm", function (a, b, c) { var d = a.length - 2; b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d)), j(c).bigHour = !0 }), $("hmmss", function (a, b, c) { var d = a.length - 4, e = a.length - 2; b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d, 2)), b[Gd] = r(a.substr(e)), j(c).bigHour = !0 }), $("Hmm", function (a, b, c) { var d = a.length - 2; b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d)) }), $("Hmmss", function (a, b, c) { var d = a.length - 4, e = a.length - 2; b[Ed] = r(a.substr(0, d)), b[Fd] = r(a.substr(d, 2)), b[Gd] = r(a.substr(e)) }); var ie = /[ap]\.?m?\.?/i, je = M("Hours", !0); R("m", ["mm", 2], 0, "minute"), J("minute", "m"), W("m", nd), W("mm", nd, jd), $(["m", "mm"], Fd); var ke = M("Minutes", !1); R("s", ["ss", 2], 0, "second"), J("second", "s"), W("s", nd), W("ss", nd, jd), $(["s", "ss"], Gd); var le = M("Seconds", !1); R("S", 0, 0, function () { return ~~(this.millisecond() / 100) }), R(0, ["SS", 2], 0, function () { return ~~(this.millisecond() / 10) }), R(0, ["SSS", 3], 0, "millisecond"), R(0, ["SSSS", 4], 0, function () { return 10 * this.millisecond() }), R(0, ["SSSSS", 5], 0, function () { return 100 * this.millisecond() }), R(0, ["SSSSSS", 6], 0, function () { return 1e3 * this.millisecond() }), R(0, ["SSSSSSS", 7], 0, function () { return 1e4 * this.millisecond() }), R(0, ["SSSSSSSS", 8], 0, function () { return 1e5 * this.millisecond() }), R(0, ["SSSSSSSSS", 9], 0, function () { return 1e6 * this.millisecond() }), J("millisecond", "ms"), W("S", qd, id), W("SS", qd, jd), W("SSS", qd, kd); var me; for (me = "SSSS"; me.length <= 9; me += "S") W(me, td); for (me = "S"; me.length <= 9; me += "S") $(me, nc); var ne = M("Milliseconds", !1); R("z", 0, 0, "zoneAbbr"), R("zz", 0, 0, "zoneName"); var oe = o.prototype; oe.add = ae, oe.calendar = jb, oe.clone = kb, oe.diff = rb, oe.endOf = Db, oe.format = vb, oe.from = wb, oe.fromNow = xb, oe.to = yb, oe.toNow = zb, oe.get = P, oe.invalidAt = Mb, oe.isAfter = lb, oe.isBefore = mb, oe.isBetween = nb, oe.isSame = ob, oe.isSameOrAfter = pb, oe.isSameOrBefore = qb, oe.isValid = Kb, oe.lang = ce, oe.locale = Ab, oe.localeData = Bb, oe.max = Xd, oe.min = Wd, oe.parsingFlags = Lb, oe.set = P, oe.startOf = Cb, oe.subtract = be, oe.toArray = Hb, oe.toObject = Ib, oe.toDate = Gb, oe.toISOString = ub, oe.toJSON = Jb, oe.toString = tb, oe.unix = Fb, oe.valueOf = Eb, oe.creationData = Nb, oe.year = Vd, oe.isLeapYear = sa, oe.weekYear = Pb, oe.isoWeekYear = Qb, oe.quarter = oe.quarters = Vb, oe.month = ga, oe.daysInMonth = ha, oe.week = oe.weeks = Zb, oe.isoWeek = oe.isoWeeks = $b, oe.weeksInYear = Sb, oe.isoWeeksInYear = Rb, oe.date = ee, oe.day = oe.days = ec, oe.weekday = fc, oe.isoWeekday = gc, oe.dayOfYear = hc, oe.hour = oe.hours = je, oe.minute = oe.minutes = ke, oe.second = oe.seconds = le, oe.millisecond = oe.milliseconds = ne, oe.utcOffset = Ta, oe.utc = Va, oe.local = Wa, oe.parseZone = Xa, oe.hasAlignedHourOffset = Ya, oe.isDST = Za, oe.isDSTShifted = $a, oe.isLocal = _a, oe.isUtcOffset = ab, oe.isUtc = bb, oe.isUTC = bb, oe.zoneAbbr = oc, oe.zoneName = pc, oe.dates = u("dates accessor is deprecated. Use date instead.", ee), oe.months = u("months accessor is deprecated. Use month instead", ga), oe.years = u("years accessor is deprecated. Use year instead", Vd), oe.zone = u("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779", Ua); var pe = oe, qe = { sameDay: "[Today at] LT", nextDay: "[Tomorrow at] LT", nextWeek: "dddd [at] LT", lastDay: "[Yesterday at] LT", lastWeek: "[Last] dddd [at] LT", sameElse: "L" }, re = { LTS: "h:mm:ss A", LT: "h:mm A", L: "MM/DD/YYYY", LL: "MMMM D, YYYY", LLL: "MMMM D, YYYY h:mm A", LLLL: "dddd, MMMM D, YYYY h:mm A" }, se = "Invalid date", te = "%d", ue = /\d{1,2}/, ve = { future: "in %s", past: "%s ago", s: "a few seconds", m: "a minute", mm: "%d minutes", h: "an hour", hh: "%d hours", d: "a day", dd: "%d days", M: "a month", MM: "%d months", y: "a year", yy: "%d years" }, we = A.prototype; we._calendar = qe, we.calendar = sc, we._longDateFormat = re, we.longDateFormat = tc, we._invalidDate = se, we.invalidDate = uc, we._ordinal = te, we.ordinal = vc, we._ordinalParse = ue, we.preparse = wc, we.postformat = wc, we._relativeTime = ve, we.relativeTime = xc, we.pastFuture = yc, we.set = y, we.months = ca, we._months = Ld, we.monthsShort = da, we._monthsShort = Md, we.monthsParse = ea, we._monthsRegex = Od, we.monthsRegex = ja, we._monthsShortRegex = Nd, we.monthsShortRegex = ia, we.week = Wb, we._week = de, we.firstDayOfYear = Yb, we.firstDayOfWeek = Xb, we.weekdays = ac, we._weekdays = fe, we.weekdaysMin = cc, we._weekdaysMin = he, we.weekdaysShort = bc, we._weekdaysShort = ge, we.weekdaysParse = dc, we.isPM = lc, we._meridiemParse = ie, we.meridiem = mc, E("en", { ordinalParse: /\d{1,2}(th|st|nd|rd)/, ordinal: function (a) { var b = a % 10, c = 1 === r(a % 100 / 10) ? "th" : 1 === b ? "st" : 2 === b ? "nd" : 3 === b ? "rd" : "th"; return a + c } }), a.lang = u("moment.lang is deprecated. Use moment.locale instead.", E), a.langData = u("moment.langData is deprecated. Use moment.localeData instead.", H); var xe = Math.abs, ye = Qc("ms"), ze = Qc("s"), Ae = Qc("m"), Be = Qc("h"), Ce = Qc("d"), De = Qc("w"), Ee = Qc("M"), Fe = Qc("y"), Ge = Sc("milliseconds"), He = Sc("seconds"), Ie = Sc("minutes"), Je = Sc("hours"), Ke = Sc("days"), Le = Sc("months"), Me = Sc("years"), Ne = Math.round, Oe = { s: 45, m: 45, h: 22, d: 26, M: 11 }, Pe = Math.abs, Qe = Na.prototype; Qe.abs = Gc, Qe.add = Ic, Qe.subtract = Jc, Qe.as = Oc, Qe.asMilliseconds = ye, Qe.asSeconds = ze, Qe.asMinutes = Ae, Qe.asHours = Be, Qe.asDays = Ce, Qe.asWeeks = De, Qe.asMonths = Ee, Qe.asYears = Fe, Qe.valueOf = Pc, Qe._bubble = Lc, Qe.get = Rc, Qe.milliseconds = Ge, Qe.seconds = He, Qe.minutes = Ie, Qe.hours = Je, Qe.days = Ke, Qe.weeks = Tc, Qe.months = Le, Qe.years = Me, Qe.humanize = Xc, Qe.toISOString = Yc, Qe.toString = Yc, Qe.toJSON = Yc, Qe.locale = Ab, Qe.localeData = Bb, Qe.toIsoString = u("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)", Yc), Qe.lang = ce, R("X", 0, 0, "unix"), R("x", 0, 0, "valueOf"), W("x", ud), W("X", xd), $("X", function (a, b, c) { c._d = new Date(1e3 * parseFloat(a, 10)) }), $("x", function (a, b, c) { c._d = new Date(r(a)) }), a.version = "2.12.0", b(Ja), a.fn = pe, a.min = La, a.max = Ma, a.now = Yd, a.utc = h, a.unix = qc, a.months = Bc, a.isDate = d, a.locale = E, a.invalid = l, a.duration = cb, a.isMoment = p, a.weekdays = Dc, a.parseZone = rc, a.localeData = H, a.isDuration = Oa, a.monthsShort = Cc, a.weekdaysMin = Fc, a.defineLocale = F, a.updateLocale = G, a.locales = I, a.weekdaysShort = Ec, a.normalizeUnits = K, a.relativeTimeThreshold = Wc, a.prototype = pe; var Re = a; return Re +}); \ No newline at end of file diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 3872da2c2..00763cc48 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -1,77 +1,156 @@ Handlebars.registerHelper('if_eq', function (a, b, opts) { if (a == b) - return opts.fn(this); + return !opts ? null : opts.fn(this); else - return opts.inverse(this); + return !opts ? null : opts.inverse(this); }); 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 = ""; + + var $ml = $('#movieList'); + var $tvl = $('#tvList'); + var $musicL = $('#musicList'); + + $('.approve-category').hide(); if (target === "#TvShowTab") { - if ($('#movieList').mixItUp('isLoaded')) { - activeState = $('#movieList').mixItUp('getState'); - $('#movieList').mixItUp('destroy'); + $('#approveTVShows').show(); + if ($ml.mixItUp('isLoaded')) { + activeState = $ml.mixItUp('getState'); + $ml.mixItUp('destroy'); } - if (!$('#tvList').mixItUp('isLoaded')) { - $('#tvList').mixItUp({ - load: { - filter: activeState.activeFilter || 'all', - sort: activeState.activeSort || 'default:asc' - }, - layout: { - display: 'block' - } - - }); + if ($musicL.mixItUp('isLoaded')) { + activeState = $musicL.mixItUp('getState'); + $musicL.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'); + $('#approveMovies').show(); + if ($tvl.mixItUp('isLoaded')) { + activeState = $tvl.mixItUp('getState'); + $tvl.mixItUp('destroy'); } - if (!$('#movieList').mixItUp('isLoaded')) { - $('#movieList').mixItUp({ - load: { - filter: activeState.activeFilter || 'all', - sort: activeState.activeSort || 'default:asc' - }, - layout: { - display: 'block' - } - }); + if ($musicL.mixItUp('isLoaded')) { + activeState = $musicL.mixItUp('getState'); + $musicL.mixItUp('destroy'); + } + if ($ml.mixItUp('isLoaded')) $ml.mixItUp('destroy'); + $ml.mixItUp(mixItUpConfig(activeState)); // init or reinit + } + + if (target === "#MusicTab") { + $('#approveMusic').show(); + if ($tvl.mixItUp('isLoaded')) { + activeState = $tvl.mixItUp('getState'); + $tvl.mixItUp('destroy'); } + if ($ml.mixItUp('isLoaded')) { + activeState = $ml.mixItUp('getState'); + $ml.mixItUp('destroy'); + } + if ($musicL.mixItUp('isLoaded')) $musicL.mixItUp('destroy'); + $musicL.mixItUp(mixItUpConfig(activeState)); // init or reinit } }); // 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) { @@ -211,37 +290,44 @@ $(document).on("click", ".delete", function (e) { // Approve single request $(document).on("click", ".approve", function (e) { e.preventDefault(); - var buttonId = e.target.id; - var $form = $('#approve' + buttonId); + var $this = $(this); + var $form = $this.parents('form').first(); - if ($('#' + buttonId).text() === " Loading...") { + if ($this.text() === " Loading...") { return; } - loadingButton(buttonId, "success"); + loadingButton($this.attr('id'), "success"); - $.ajax({ - type: $form.prop('method'), - url: $form.prop('action'), - data: $form.serialize(), - dataType: "json", - success: function (response) { + approveRequest($form, null, function () { + $("#" + $this.attr('id') + "notapproved").prop("class", "fa fa-check"); + + var $group = $this.parent('.btn-split'); + if ($group.length > 0) { + $group.remove(); + } + else { + $this.remove(); + } + }); +}); - if (checkJsonResponse(response)) { - if (response.message) { - generateNotify(response.message, "success"); - } else { - generateNotify("Success! Request Approved.", "success"); - } +$(document).on("click", ".approve-with-quality", function (e) { + e.preventDefault(); + var $this = $(this); + var $button = $this.parents('.btn-split').children('.approve').first(); + var qualityId = e.target.id + var $form = $this.parents('form').first(); + + if ($button.text() === " Loading...") { + return; + } - $("button[custom-button='" + buttonId + "']").remove(); - $("#" + buttonId + "notapproved").prop("class", "fa fa-check"); - } - }, - error: function (e) { - console.log(e); - generateNotify("Something went wrong!", "danger"); - } + loadingButton($button.attr('id'), "success"); + + approveRequest($form, qualityId, function () { + $("#" + $button.attr('id') + "notapproved").prop("class", "fa fa-check"); + $this.parents('.btn-split').remove(); }); }); @@ -315,36 +401,119 @@ $(document).on("click", ".change", function (e) { }); +function approveRequest($form, qualityId, successCallback) { + + var formData = $form.serialize(); + if (qualityId) formData += ("&qualityId=" + qualityId); + + $.ajax({ + type: $form.prop('method'), + url: $form.prop('action'), + data: formData, + dataType: "json", + success: function (response) { + + if (checkJsonResponse(response)) { + if (response.message) { + generateNotify(response.message, "success"); + } else { + generateNotify("Success! Request Approved.", "success"); + } + + if (successCallback) { + successCallback(); + } + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); +} + +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(); +} + 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 = albumTemplate(context); + $albumL.append(html); + }); + } + else { + $albumL.html(noResultsMusic.format("albums")); + } + $albumL.mixItUp(mixItUpConfig()); }); }; @@ -358,10 +527,12 @@ function buildRequestContext(result, type) { year: result.releaseYear, type: type, status: result.status, - releaseDate: result.releaseDate, + releaseDate: Humanize(result.releaseDate), + releaseDateTicks: result.releaseDateTicks, approved: result.approved, - requestedBy: result.requestedBy, - requestedDate: result.requestedDate, + requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '', + requestedDate: Humanize(result.requestedDate), + requestedDateTicks: result.requestedDateTicks, available: result.available, admin: result.admin, issues: result.issues, @@ -369,20 +540,13 @@ function buildRequestContext(result, type) { requestId: result.id, adminNote: result.adminNotes, imdb: result.imdbId, - seriesRequested: result.tvSeriesRequestType + seriesRequested: result.tvSeriesRequestType, + coverArtUrl: result.coverArtUrl, + qualities: result.qualities, + hasQualities: result.qualities && result.qualities.length > 0, + artist: result.artistName }; 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 62ed6843b..1096290ed 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -5,175 +5,312 @@ return opts.inverse(this); }); -var searchSource = $("#search-template").html(); -var searchTemplate = Handlebars.compile(searchSource); -var movieTimer = 0; -var tvimer = 0; - -// Type in movie search -$("#movieSearchContent").on("input", function () { - if (movieTimer) { - clearTimeout(movieTimer); - } - $('#movieSearchButton').attr("class","fa fa-spinner fa-spin"); - movieTimer = setTimeout(movieSearch, 400); +$(function () { -}); + var searchSource = $("#search-template").html(); + var musicSource = $("#music-template").html(); + var searchTemplate = Handlebars.compile(searchSource); + var musicTemplate = Handlebars.compile(musicSource); -// Type in TV search -$("#tvSearchContent").on("input", function () { - if (tvimer) { - clearTimeout(tvimer); - } - $('#tvSearchButton').attr("class", "fa fa-spinner fa-spin"); - tvimer = setTimeout(tvSearch, 400); -}); + var searchTimer = 0; -// Click TV dropdown option -$(document).on("click", ".dropdownTv", function (e) { - e.preventDefault(); - var buttonId = e.target.id; - if ($("#" + buttonId).attr('disabled')) { - return; + // fix for selecting a default tab + var $tabs = $('#nav-tabs').children('li'); + if ($tabs.filter(function (li) { return $(li).hasClass('active') }).length <= 0) { + $tabs.first().children('a:first-child').tab('show'); } - $("#" + buttonId).prop("disabled", true); - loadingButton(buttonId, "primary"); + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + focusSearch($($(e.target).attr('href'))) + }); + focusSearch($('li.active a', '#nav-tabs').first().attr('href')); + // Type in movie search + $("#movieSearchContent").on("input", function () { + if (searchTimer) { + clearTimeout(searchTimer); + } + $('#movieSearchButton').attr("class", "fa fa-spinner fa-spin"); + searchTimer = setTimeout(movieSearch, 400); - var $form = $('#form' + buttonId); - var data = $form.serialize(); - var seasons = $(this).attr("season-select"); - if (seasons === "2") { - // Send over the latest - data = data + "&seasons=latest"; - } - if (seasons === "1") { - // Send over the first season - data = data + "&seasons=first"; + }); - } + $('#moviesComingSoon').on('click', function (e) { + e.preventDefault(); + moviesComingSoon(); + }); - var type = $form.prop('method'); - var url = $form.prop('action'); + $('#moviesInTheaters').on('click', function (e) { + e.preventDefault(); + moviesInTheaters(); + }); - sendRequestAjax(data, type, url, buttonId); -}); + // Type in TV search + $("#tvSearchContent").on("input", function () { + if (searchTimer) { + clearTimeout(searchTimer); + } + $('#tvSearchButton').attr("class", "fa fa-spinner fa-spin"); + searchTimer = setTimeout(tvSearch, 400); + }); -// Click Request for movie -$(document).on("click", ".requestMovie", function (e) { - e.preventDefault(); - var buttonId = e.target.id; - if ($("#" + buttonId).attr('disabled')) { - return; - } + // Click TV dropdown option + $(document).on("click", ".dropdownTv", function (e) { + e.preventDefault(); + var buttonId = e.target.id; + if ($("#" + buttonId).attr('disabled')) { + return; + } - $("#" + buttonId).prop("disabled", true); - loadingButton(buttonId, "primary"); + $("#" + buttonId).prop("disabled", true); + loadingButton(buttonId, "primary"); - var $form = $('#form' + buttonId); + var $form = $('#form' + buttonId); + var data = $form.serialize(); + var seasons = $(this).attr("season-select"); + if (seasons === "2") { + // Send over the latest + data = data + "&seasons=latest"; + } + if (seasons === "1") { + // Send over the first season + data = data + "&seasons=first"; - var type = $form.prop('method'); - var url = $form.prop('action'); - var data = $form.serialize(); + } - sendRequestAjax(data, type, url, buttonId); - -}); + var type = $form.prop('method'); + var url = $form.prop('action'); -function sendRequestAjax(data, type, url, buttonId) { - $.ajax({ - type: type, - url: url, - data: data, - dataType: "json", - success: function (response) { - console.log(response); - if (response.result === true) { - generateNotify("Success!", "success"); - - $('#' + buttonId).html(" Requested"); - $('#' + buttonId).removeClass("btn-primary-outline"); - $('#' + buttonId).removeAttr("data-toggle"); - $('#' + buttonId).addClass("btn-success-outline"); - } else { - generateNotify(response.message, "warning"); - $('#' + buttonId).html(" Request"); - $('#' + buttonId).attr("data-toggle", "dropdown"); - $("#" + buttonId).removeAttr("disabled"); - } - }, - error: function (e) { - console.log(e); - generateNotify("Something went wrong!", "danger"); - } + sendRequestAjax(data, type, url, buttonId); }); -} -function movieSearch() { - $("#movieList").html(""); - var query = $("#movieSearchContent").val(); + // Search Music + $("#musicSearchContent").on("input", function () { + if (searchTimer) { + clearTimeout(searchTimer); + } + $('#musicSearchButton').attr("class", "fa fa-spinner fa-spin"); + searchTimer = setTimeout(musicSearch, 400); - $.ajax("/search/movie/" + query).success(function (results) { - if (results.length > 0) { - results.forEach(function(result) { - var context = buildMovieContext(result); + }); - var html = searchTemplate(context); - $("#movieList").append(html); - }); + // Click Request for movie + $(document).on("click", ".requestMovie", function (e) { + e.preventDefault(); + var buttonId = e.target.id; + if ($("#" + buttonId).attr('disabled')) { + return; } - $('#movieSearchButton').attr("class","fa fa-search"); + + $("#" + 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 tvSearch() { - $("#tvList").html(""); - var query = $("#tvSearchContent").val(); - - $.ajax("/search/tv/" + query).success(function (results) { - if (results.length > 0) { - results.forEach(function(result) { - var context = buildTvShowContext(result); - var html = searchTemplate(context); - $("#tvList").append(html); - }); + + // Click Request for album + $(document).on("click", ".requestAlbum", function (e) { + e.preventDefault(); + var buttonId = e.target.id; + if ($("#" + buttonId).attr('disabled')) { + return; } - $('#tvSearchButton').attr("class", "fa fa-search"); + + $("#" + 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 buildMovieContext(result) { - var date = new Date(result.releaseDate); - var year = date.getFullYear(); - var context = { - posterPath: result.posterPath, - id: result.id, - title: result.title, - overview: result.overview, - voteCount: result.voteCount, - voteAverage: result.voteAverage, - year: year, - type: "movie", - imdb: result.imdbId + + function focusSearch($content) { + if ($content.length > 0) { + $('input[type=text].form-control', $content).first().focus(); + } + } + + function sendRequestAjax(data, type, url, buttonId) { + $.ajax({ + type: type, + url: url, + data: data, + dataType: "json", + success: function (response) { + console.log(response); + if (response.result === true) { + generateNotify(response.message || "Success!", "success"); + + $('#' + buttonId).html(" Requested"); + $('#' + buttonId).removeClass("btn-primary-outline"); + $('#' + buttonId).removeAttr("data-toggle"); + $('#' + buttonId).addClass("btn-success-outline"); + } else { + generateNotify(response.message, "warning"); + $('#' + buttonId).html(" Request"); + $('#' + buttonId).attr("data-toggle", "dropdown"); + $("#" + buttonId).removeAttr("disabled"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + } + + function movieSearch() { + var query = $("#movieSearchContent").val(); + getMovies("/search/movie/" + query); + } + + 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); + + var html = searchTemplate(context); + $("#movieList").append(html); + }); + } + else { + $("#movieList").html(noResultsHtml); + } + $('#movieSearchButton').attr("class", "fa fa-search"); + }); + }; + + function tvSearch() { + var query = $("#tvSearchContent").val(); + getTvShows("/search/tv/" + query); + } + + function getTvShows(url) { + $("#tvList").html(""); + + $.ajax(url).success(function (results) { + if (results.length > 0) { + results.forEach(function (result) { + var context = buildTvShowContext(result); + var html = searchTemplate(context); + $("#tvList").append(html); + }); + } + else { + $("#tvList").html(noResultsHtml); + } + $('#tvSearchButton').attr("class", "fa fa-search"); + }); }; - return context; -} - -function buildTvShowContext(result) { - var date = new Date(result.firstAired); - var year = date.getFullYear(); - var context = { - posterPath: result.banner, - id: result.id, - title: result.seriesName, - overview: result.overview, - year: year, - type: "tv", - imdb: result.imdbId + 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); + getCoverArt(context.id); + }); + } + else { + $("#musicList").html(noResultsMusic); + } + $('#musicSearchButton').attr("class", "fa fa-search"); + }); }; - return context; -} + + function getCoverArt(artistId) { + $.ajax("/search/music/coverart/" + artistId).success(function (result) { + if (result) { + $('#' + artistId + "imageDiv").html(" poster"); + } + }); + }; + + function buildMovieContext(result) { + var date = new Date(result.releaseDate); + var year = date.getFullYear(); + var context = { + posterPath: result.posterPath, + id: result.id, + title: result.title, + overview: result.overview, + voteCount: result.voteCount, + voteAverage: result.voteAverage, + year: year, + type: "movie", + imdb: result.imdbId + }; + + return context; + } + + function buildTvShowContext(result) { + var date = new Date(result.firstAired); + var year = date.getFullYear(); + var context = { + posterPath: result.banner, + id: result.id, + title: result.seriesName, + overview: result.overview, + year: year, + type: "tv", + imdb: result.imdbId + }; + 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..e2784b315 100644 --- a/PlexRequests.UI/Content/site.js +++ b/PlexRequests.UI/Content/site.js @@ -1,4 +1,20 @@ -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 Humanize(date) { + var mNow = moment(); + var mDate = moment(date).local(); + return moment.duration(mNow - mDate).humanize() + (mNow.isBefore(mDate) ? ' from now' : ' ago'); +} + +function generateNotify(message, type) { // type = danger, warning, info, successs $.notify({ // options @@ -25,13 +41,28 @@ function checkJsonResponse(response) { } function loadingButton(elementId, originalCss) { - $('#' + elementId).removeClass("btn-" + originalCss + "-outline"); - $('#' + elementId).addClass("btn-primary-outline"); - $('#' + elementId).html(" Loading..."); + var $element = $('#' + elementId); + $element.removeClass("btn-" + originalCss + "-outline").addClass("btn-primary-outline").addClass('disabled').html(" Loading..."); + + // handle split-buttons + var $dropdown = $element.next('.dropdown-toggle') + if ($dropdown.length > 0) { + $dropdown.removeClass("btn-" + originalCss + "-outline").addClass("btn-primary-outline").addClass('disabled'); + } } 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 $element = $('#' + elementId); + $element.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline").html(html); + + // handle split-buttons + var $dropdown = $element.next('.dropdown-toggle') + if ($dropdown.length > 0) { + $dropdown.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline"); + } +} + +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/HeadphonesSender.cs b/PlexRequests.UI/Helpers/HeadphonesSender.cs new file mode 100644 index 000000000..17674e891 --- /dev/null +++ b/PlexRequests.UI/Helpers/HeadphonesSender.cs @@ -0,0 +1,176 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: HeadphonesSender.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.Linq; +using System.Threading; +using System.Threading.Tasks; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Store; + +namespace PlexRequests.UI.Helpers +{ + public class HeadphonesSender + { + public HeadphonesSender(IHeadphonesApi api, HeadphonesSettings settings, IRequestService request) + { + Api = api; + Settings = settings; + RequestService = request; + } + + private int WaitTime => 2000; + private int CounterMax => 60; + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private IHeadphonesApi Api { get; } + private IRequestService RequestService { get; } + private HeadphonesSettings Settings { get; } + + public async Task AddAlbum(RequestedModel request) + { + var addArtistResult = await AddArtist(request); + if (!addArtistResult) + { + return false; + } + + // Artist is now active + // Add album + var albumResult = await Api.AddAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + if (!albumResult) + { + Log.Error("Couldn't add the album to headphones"); + } + + // Set the status to wanted and search + var status = await SetAlbumStatus(request); + if (!status) + { + return false; + } + + // Approve it + request.Approved = true; + + // Update the record + var updated = RequestService.UpdateRequest(request); + + return updated; + } + + private async Task AddArtist(RequestedModel request) + { + var index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + var artistExists = index.Any(x => x.ArtistID == request.ArtistId); + if (!artistExists) + { + var artistAdd = Api.AddArtist(Settings.ApiKey, Settings.FullUri, request.ArtistId); + Log.Info("Artist add result : {0}", artistAdd); + } + + var counter = 0; + while (index.All(x => x.ArtistID != request.ArtistId)) + { + Thread.Sleep(WaitTime); + counter++; + Log.Trace("Artist is still not present in the index. Counter = {0}", counter); + index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + + if (counter > CounterMax) + { + Log.Trace("Artist is still not present in the index. Counter = {0}. Returning false", counter); + Log.Warn("We have tried adding the artist but it seems they are still not in headphones."); + return false; + } + } + + counter = 0; + var artistStatus = index.Where(x => x.ArtistID == request.ArtistId).Select(x => x.Status).FirstOrDefault(); + while (artistStatus != "Active") + { + Thread.Sleep(WaitTime); + counter++; + Log.Trace("Artist status {1}. Counter = {0}", counter, artistStatus); + index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + artistStatus = index.Where(x => x.ArtistID == request.ArtistId).Select(x => x.Status).FirstOrDefault(); + if (counter > CounterMax) + { + Log.Trace("Artist status is still not active. Counter = {0}. Returning false", counter); + Log.Warn("The artist status is still not Active. We have waited long enough, seems to be a big delay in headphones."); + return false; + } + } + + var addedArtist = index.FirstOrDefault(x => x.ArtistID == request.ArtistId); + var artistName = addedArtist?.ArtistName ?? string.Empty; + counter = 0; + while (artistName.Contains("Fetch failed")) + { + Thread.Sleep(WaitTime); + await Api.RefreshArtist(Settings.ApiKey, Settings.FullUri, request.ArtistId); + + index = await Api.GetIndex(Settings.ApiKey, Settings.FullUri); + + artistName = index?.FirstOrDefault(x => x.ArtistID == request.ArtistId)?.ArtistName ?? string.Empty; + counter++; + if (counter > CounterMax) + { + Log.Trace("Artist fetch has failed. Counter = {0}. Returning false", counter); + Log.Warn("Artist in headphones fetch has failed, we have tried refreshing the artist but no luck."); + return false; + } + } + + return true; + } + + private async Task SetAlbumStatus(RequestedModel request) + { + var counter = 0; + var setStatus = await Api.QueueAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + + while (!setStatus) + { + Thread.Sleep(WaitTime); + counter++; + Log.Trace("Setting Album status. Counter = {0}", counter); + setStatus = await Api.QueueAlbum(Settings.ApiKey, Settings.FullUri, request.MusicBrainzId); + if (counter > CounterMax) + { + Log.Trace("Album status is still not active. Counter = {0}. Returning false", counter); + Log.Warn("We tried to se the status for the album but headphones didn't want to snatch it."); + return false; + } + } + return true; + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/StringHelper.cs b/PlexRequests.UI/Helpers/StringHelper.cs new file mode 100644 index 000000000..e1368bb64 --- /dev/null +++ b/PlexRequests.UI/Helpers/StringHelper.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace PlexRequests.UI.Helpers +{ + public static class StringHelper + { + public static string FirstCharToUpper(this string input) + { + if (String.IsNullOrEmpty(input)) + return input; + + return input.First().ToString().ToUpper() + String.Join("", input.Skip(1)); + } + + public static string CamelCaseToWords(this string input) + { + return Regex.Replace(input.FirstCharToUpper(), "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 "); + } + } +} diff --git a/PlexRequests.UI/Helpers/TvSender.cs b/PlexRequests.UI/Helpers/TvSender.cs index d26611321..c4edac50f 100644 --- a/PlexRequests.UI/Helpers/TvSender.cs +++ b/PlexRequests.UI/Helpers/TvSender.cs @@ -24,17 +24,14 @@ // 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; +using System.Linq; namespace PlexRequests.UI.Helpers { @@ -50,9 +47,19 @@ public TvSender(ISonarrApi sonarrApi, ISickRageApi srApi) private static Logger Log = LogManager.GetCurrentClassLogger(); public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model) + { + return SendToSonarr(sonarrSettings, model, string.Empty); + } + + public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, string qualityId) { int qualityProfile; - int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); + + if (!string.IsNullOrEmpty(qualityId) || !int.TryParse(qualityId, out qualityProfile)) // try to parse the passed in quality, otherwise use the settings default quality + { + int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); + } + var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey, sonarrSettings.FullUri); @@ -65,13 +72,24 @@ public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedMode public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) { - var result = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, sickRageSettings.QualityProfile, + return SendToSickRage(sickRageSettings, model, sickRageSettings.QualityProfile); + } + + public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model, string qualityId) + { + if (!sickRageSettings.Qualities.Any(x => x.Key == qualityId)) + { + qualityId = sickRageSettings.QualityProfile; + } + + var apiResult = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, qualityId, sickRageSettings.ApiKey, sickRageSettings.FullUri); + var result = apiResult.Result; Log.Trace("SickRage Add Result: "); Log.Trace(result.DumpJson()); - return result.Result; + return result; } } } \ No newline at end of file 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 @@ public ITask GetTaskInstance() where T : ITask //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/QualityModel.cs b/PlexRequests.UI/Models/QualityModel.cs new file mode 100644 index 000000000..80d7996b6 --- /dev/null +++ b/PlexRequests.UI/Models/QualityModel.cs @@ -0,0 +1,8 @@ +namespace PlexRequests.UI.Models +{ + public class QualityModel + { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/PlexRequests.UI/Models/RequestViewModel.cs b/PlexRequests.UI/Models/RequestViewModel.cs index 011b9977d..e5dc09746 100644 --- a/PlexRequests.UI/Models/RequestViewModel.cs +++ b/PlexRequests.UI/Models/RequestViewModel.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion using PlexRequests.Store; +using System; namespace PlexRequests.UI.Models { @@ -36,12 +37,14 @@ public class RequestViewModel public string Overview { get; set; } public string Title { get; set; } public string PosterPath { get; set; } - public string ReleaseDate { get; set; } + public DateTime 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 RequestedDate { get; set; } + public string[] RequestedUsers { get; set; } + public DateTime 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 +52,8 @@ public class RequestViewModel public string OtherMessage { get; set; } public string AdminNotes { get; set; } public string TvSeriesRequestType { get; set; } + public string MusicBrainzId { get; set; } + public QualityModel[] Qualities { get; set; } + public string ArtistName { 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..631cc7fe9 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -28,14 +28,12 @@ using System.Collections.Generic; using System.Dynamic; using System.Linq; -using Humanizer; using MarkdownSharp; using Nancy; using Nancy.Extensions; using Nancy.ModelBinding; using Nancy.Responses.Negotiation; -using Nancy.Security; using Nancy.Validation; using NLog; @@ -51,12 +49,16 @@ using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; +using System; + +using Nancy.Json; +using Nancy.Security; 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 +67,7 @@ public class AdminModule : NancyModule 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; } @@ -72,9 +75,10 @@ public class AdminModule : NancyModule private ICouchPotatoApi CpApi { get; } private IRepository LogsRepo { get; } private INotificationService NotificationService { get; } + private ICacheProvider Cache { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); - public AdminModule(ISettingsService rpService, + public AdminModule(ISettingsService prService, ISettingsService cpService, ISettingsService auth, ISettingsService plex, @@ -89,9 +93,11 @@ public AdminModule(ISettingsService rpService, ISettingsService pushoverSettings, IPushoverApi pushoverApi, IRepository logsRepo, - INotificationService notify) : base("admin") + INotificationService notify, + ISettingsService headphones, + ICacheProvider cache) : base("admin") { - RpService = rpService; + PrService = prService; CpService = cpService; AuthService = auth; PlexService = plex; @@ -107,6 +113,8 @@ public AdminModule(ISettingsService rpService, PushoverService = pushoverSettings; PushoverApi = pushoverApi; NotificationService = notify; + HeadphonesService = headphones; + Cache = cache; #if !DEBUG this.RequiresAuthentication(); @@ -139,18 +147,24 @@ public AdminModule(ISettingsService rpService, 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 +188,7 @@ private Response SaveAuthentication() private Negotiator Admin() { - var settings = RpService.GetSettings(); + var settings = PrService.GetSettings(); Log.Trace("Getting Settings:"); Log.Trace(settings.DumpJson()); @@ -185,7 +199,7 @@ private Response SaveAdmin() { var model = this.Bind(); - RpService.SaveSettings(model); + PrService.SaveSettings(model); return Context.GetRedirect("~/admin"); @@ -362,6 +376,12 @@ private Response GetSonarrQualityProfiles() var settings = this.Bind(); var profiles = SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri); + // set the cache + if (profiles != null) + { + Cache.Set(CacheKeys.SonarrQualityProfiles, profiles); + } + return Response.AsJson(profiles); } @@ -372,6 +392,37 @@ private Negotiator EmailNotifications() 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 +491,37 @@ private Response SavePushbulletNotifications() : 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,11 +554,48 @@ private Response SavePushoverNotifications() : 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(); var profiles = CpApi.GetProfiles(settings.FullUri, settings.ApiKey); + // set the cache + if (profiles != null) + { + Cache.Set(CacheKeys.CouchPotatoQualityProfiles, profiles); + } + return Response.AsJson(profiles); } @@ -487,7 +606,8 @@ private Negotiator Logs() private Response LoadLogs() { - var allLogs = LogsRepo.GetAll(); + JsonSettings.MaxJsonLength = int.MaxValue; + var allLogs = LogsRepo.GetAll().OrderByDescending(x => x.Id).Take(200); var model = new DatatablesModel {Data = new List()}; foreach (var l in allLogs) { @@ -509,5 +629,32 @@ private Response UpdateLogLevels(int level) 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..929949bd2 100644 --- a/PlexRequests.UI/Modules/ApplicationTesterModule.cs +++ b/PlexRequests.UI/Modules/ApplicationTesterModule.cs @@ -43,7 +43,7 @@ public class ApplicationTesterModule : BaseModule { public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, - ISettingsService authSettings, ISickRageApi srApi) : base("test") + ISettingsService authSettings, ISickRageApi srApi, IHeadphonesApi hpApi) : base("test") { this.RequiresAuthentication(); @@ -52,11 +52,13 @@ public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPle PlexApi = plexApi; AuthSettings = authSettings; SickRageApi = srApi; + HeadphonesApi = hpApi; Post["/cp"] = _ => CouchPotatoTest(); Post["/sonarr"] = _ => SonarrTest(); Post["/plex"] = _ => PlexTest(); Post["/sickrage"] = _ => SickRageTest(); + Post["/headphones"] = _ => HeadphonesTest(); } @@ -65,6 +67,7 @@ public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPle private ICouchPotatoApi CpApi { get; } private IPlexApi PlexApi { get; } private ISickRageApi SickRageApi { get; } + private IHeadphonesApi HeadphonesApi { get; } private ISettingsService AuthSettings { get; } private Response CouchPotatoTest() @@ -168,5 +171,35 @@ private Response SickRageTest() return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); } } + + private Response HeadphonesTest() + { + var settings = this.Bind(); + try + { + var result = HeadphonesApi.GetVersion(settings.ApiKey, settings.FullUri); + if (!string.IsNullOrEmpty(result.latest_version)) + { + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = "Connected to Headphones successfully!" + }); + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Headphones, please check your settings." }); + } + catch (ApplicationException e) + { + Log.Warn("Exception thrown when attempting to get Headphones's status: "); + Log.Warn(e); + var message = $"Could not connect to Headphones, please check your settings. Exception Message: {e.Message}"; + if (e.InnerException != null) + { + message = $"Could not connect to Headphones, please check your settings. Exception Message: {e.InnerException.Message}"; + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); ; + } + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index b2c6217bb..d5871481f 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -47,7 +47,8 @@ public class ApprovalModule : BaseModule { public ApprovalModule(IRequestService service, ISettingsService cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, - ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings) : base("approval") + ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, + ISettingsService hpSettings, IHeadphonesApi hpApi) : base("approval") { this.RequiresAuthentication(); @@ -58,9 +59,13 @@ public ApprovalModule(IRequestService service, ISettingsService Approve((int)Request.Form.requestid); + Post["/approve"] = parameters => Approve((int)Request.Form.requestid, (string)Request.Form.qualityId); Post["/approveall"] = x => ApproveAll(); + Post["/approveallmovies"] = x => ApproveAllMovies(); + Post["/approvealltvshows"] = x => ApproveAllTVShows(); } private IRequestService Service { get; } @@ -69,16 +74,18 @@ public ApprovalModule(IRequestService service, ISettingsService SonarrSettings { get; } private ISettingsService SickRageSettings { get; } private ISettingsService CpService { get; } + private ISettingsService HeadphonesSettings { get; } private ISonarrApi SonarrApi { get; } private ISickRageApi SickRageApi { get; } private ICouchPotatoApi CpApi { get; } + private IHeadphonesApi HeadphoneApi { get; } /// /// Approves the specified request identifier. /// /// The request identifier. /// - private Response Approve(int requestId) + private Response Approve(int requestId, string qualityId) { Log.Info("approving request {0}", requestId); if (!Context.CurrentUser.IsAuthenticated()) @@ -97,15 +104,17 @@ private Response Approve(int requestId) switch (request.Type) { case RequestType.Movie: - return RequestMovieAndUpdateStatus(request); + return RequestMovieAndUpdateStatus(request, qualityId); case RequestType.TvShow: - return RequestTvAndUpdateStatus(request); + return RequestTvAndUpdateStatus(request, qualityId); + case RequestType.Album: + return RequestAlbumAndUpdateStatus(request); default: throw new ArgumentOutOfRangeException(nameof(request)); } } - private Response RequestTvAndUpdateStatus(RequestedModel request) + private Response RequestTvAndUpdateStatus(RequestedModel request, string qualityId) { var sender = new TvSender(SonarrApi, SickRageApi); @@ -113,7 +122,7 @@ private Response RequestTvAndUpdateStatus(RequestedModel request) if (sonarrSettings.Enabled) { Log.Trace("Sending to Sonarr"); - var result = sender.SendToSonarr(sonarrSettings, request); + var result = sender.SendToSonarr(sonarrSettings, request, qualityId); Log.Trace("Sonarr Result: "); Log.Trace(result.DumpJson()); if (!string.IsNullOrEmpty(result.title)) @@ -131,7 +140,7 @@ private Response RequestTvAndUpdateStatus(RequestedModel request) 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" }); } @@ -139,7 +148,7 @@ private Response RequestTvAndUpdateStatus(RequestedModel request) if (srSettings.Enabled) { Log.Trace("Sending to SickRage"); - var result = sender.SendToSickRage(srSettings, request); + var result = sender.SendToSickRage(srSettings, request, qualityId); Log.Trace("SickRage Result: "); Log.Trace(result.DumpJson()); if (result?.result == "success") @@ -167,7 +176,7 @@ private Response RequestTvAndUpdateStatus(RequestedModel request) }); } - private Response RequestMovieAndUpdateStatus(RequestedModel request) + private Response RequestMovieAndUpdateStatus(RequestedModel request, string qualityId) { var cpSettings = CpService.GetSettings(); var cp = new CouchPotatoApi(); @@ -188,7 +197,8 @@ private Response RequestMovieAndUpdateStatus(RequestedModel request) Message = "We could not approve this request. Please try again or check the logs." }); } - var result = cp.AddMovie(request.ImdbId, cpSettings.ApiKey, request.Title, cpSettings.FullUri, cpSettings.ProfileId); + + var result = cp.AddMovie(request.ImdbId, cpSettings.ApiKey, request.Title, cpSettings.FullUri, string.IsNullOrEmpty(qualityId) ? cpSettings.ProfileId : qualityId); Log.Trace("Adding movie to CP result {0}", result); if (result) { @@ -216,6 +226,84 @@ private Response RequestMovieAndUpdateStatus(RequestedModel request) }); } + private Response RequestAlbumAndUpdateStatus(RequestedModel request) + { + var hpSettings = HeadphonesSettings.GetSettings(); + Log.Info("Adding album to Headphones : {0}", request.Title); + if (!hpSettings.Enabled) + { + // Approve it + request.Approved = true; + Log.Warn("We approved Album: {0} but could not add it to Headphones because it has not been setup", request.Title); + + // Update the record + var inserted = Service.UpdateRequest(request); + return Response.AsJson(inserted + ? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to Headphones because it has not been configured." } + : new JsonResponseModel + { + Result = false, + Message = "We could not approve this request. Please try again or check the logs." + }); + } + + var sender = new HeadphonesSender(HeadphoneApi, hpSettings, Service); + var result = sender.AddAlbum(request); + + + return Response.AsJson( new JsonResponseModel { Result = true, Message = "We have sent the approval to Headphones for processing, This can take a few minutes."} ); + } + + 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 +315,35 @@ private Response ApproveAll() 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 +360,8 @@ private Response ApproveAll() 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 +369,14 @@ private Response ApproveAll() 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 +384,7 @@ private Response ApproveAll() else { Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title); + Log.Error("Error message: {0}", res?.ErrorMessage); } } } @@ -291,17 +392,16 @@ private Response ApproveAll() try { - 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." }); - + 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 @@ public LoginModule() { 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 @@ public LoginModule() 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..92a326a56 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -28,8 +28,6 @@ using System; using System.Linq; -using Humanizer; - using Nancy; using Nancy.Responses.Negotiation; using Nancy.Security; @@ -40,22 +38,45 @@ using PlexRequests.Services.Notification; using PlexRequests.Store; using PlexRequests.UI.Models; +using PlexRequests.Helpers; +using PlexRequests.UI.Helpers; +using System.Collections.Generic; +using PlexRequests.Api.Interfaces; +using System.Threading.Tasks; namespace PlexRequests.UI.Modules { public class RequestsModule : BaseModule { - - public RequestsModule(IRequestService service, ISettingsService prSettings, ISettingsService plex, INotificationService notify) : base("requests") + public RequestsModule( + IRequestService service, + ISettingsService prSettings, + ISettingsService plex, + INotificationService notify, + ISettingsService sonarrSettings, + ISettingsService sickRageSettings, + ISettingsService cpSettings, + ICouchPotatoApi cpApi, + ISonarrApi sonarrApi, + ISickRageApi sickRageApi, + ICacheProvider cache) : base("requests") { Service = service; PrSettings = prSettings; PlexSettings = plex; NotificationService = notify; + SonarrSettings = sonarrSettings; + SickRageSettings = sickRageSettings; + CpSettings = cpSettings; + SonarrApi = sonarrApi; + SickRageApi = sickRageApi; + CpApi = cpApi; + Cache = cache; 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); @@ -70,6 +91,13 @@ public RequestsModule(IRequestService service, ISettingsService PrSettings { get; } private ISettingsService PlexSettings { get; } + private ISettingsService SonarrSettings { get; } + private ISettingsService SickRageSettings { get; } + private ISettingsService CpSettings { get; } + private ISonarrApi SonarrApi { get; } + private ISickRageApi SickRageApi { get; } + private ICouchPotatoApi CpApi { get; } + private ICacheProvider Cache { get; } private Negotiator LoadRequests() { @@ -77,60 +105,204 @@ private Negotiator LoadRequests() return View["Index", settings]; } - private Response GetMovies() + private Response GetMovies() // TODO: async await the API calls + { + var settings = PrSettings.GetSettings(); + var isAdmin = Context.CurrentUser.IsAuthenticated(); + + List taskList = new List(); + + List dbMovies = new List(); + taskList.Add(Task.Factory.StartNew(() => + { + return Service.GetAll().Where(x => x.Type == RequestType.Movie); + + }).ContinueWith((t) => + { + dbMovies = t.Result.ToList(); + + if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) + { + dbMovies = dbMovies.Where(x => x.UserHasRequested(Username)).ToList(); + } + })); + + + List qualities = new List(); + + if (isAdmin) + { + var cpSettings = CpSettings.GetSettings(); + if (cpSettings.Enabled) + { + taskList.Add(Task.Factory.StartNew(() => + { + return Cache.GetOrSet(CacheKeys.CouchPotatoQualityProfiles, () => + { + return CpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey); // TODO: cache this! + }); + }).ContinueWith((t) => + { + qualities = t.Result.list.Select(x => new QualityModel() { Id = x._id, Name = x.label }).ToList(); + })); + } + } + + Task.WaitAll(taskList.ToArray()); + + 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, + ReleaseDateTicks = movie.ReleaseDate.Ticks, + RequestedDate = movie.RequestedDate, + 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.ToString().CamelCaseToWords(), + OtherMessage = movie.OtherMessage, + AdminNotes = movie.AdminNote, + Qualities = qualities.ToArray() + }; + }).ToList(); + + return Response.AsJson(viewModel); + } + + private Response GetTvShows() // TODO: async await the API calls { + 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 - { - 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 + + List taskList = new List(); + + List dbTv = new List(); + taskList.Add(Task.Factory.StartNew(() => + { + return Service.GetAll().Where(x => x.Type == RequestType.TvShow); + + }).ContinueWith((t) => + { + dbTv = t.Result.ToList(); + + if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) + { + dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList(); + } + })); + + List qualities = new List(); + if (isAdmin) + { + var sonarrSettings = SonarrSettings.GetSettings(); + if (sonarrSettings.Enabled) + { + taskList.Add(Task.Factory.StartNew(() => + { + return Cache.GetOrSet(CacheKeys.SonarrQualityProfiles, () => + { + return SonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri); // TODO: cache this! + + }); + }).ContinueWith((t) => + { + qualities = t.Result.Select(x => new QualityModel() { Id = x.id.ToString(), Name = x.name }).ToList(); + })); + } + else { + var sickRageSettings = SickRageSettings.GetSettings(); + if (sickRageSettings.Enabled) + { + qualities = sickRageSettings.Qualities.Select(x => new QualityModel() { Id = x.Key, Name = x.Value }).ToList(); + } + } + } + + Task.WaitAll(taskList.ToArray()); + + 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, + ReleaseDateTicks = tv.ReleaseDate.Ticks, + RequestedDate = tv.RequestedDate, + 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.ToString().CamelCaseToWords(), + OtherMessage = tv.OtherMessage, + AdminNotes = tv.AdminNote, + TvSeriesRequestType = tv.SeasonsRequested, + Qualities = qualities.ToArray() + }; }).ToList(); return Response.AsJson(viewModel); } - private Response GetTvShows() + private Response GetAlbumRequests() { + 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 - { - 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 + var dbAlbum = Service.GetAll().Where(x => x.Type == RequestType.Album); + if (settings.UsersCanViewOnlyOwnRequests && !isAdmin) + { + 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, + ReleaseDateTicks = album.ReleaseDate.Ticks, + RequestedDate = album.RequestedDate, + 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.ToString().CamelCaseToWords(), + OtherMessage = album.OtherMessage, + AdminNotes = album.AdminNote, + TvSeriesRequestType = album.SeasonsRequested, + MusicBrainzId = album.MusicBrainzId, + ArtistName = album.ArtistName + + }; }).ToList(); return Response.AsJson(viewModel); @@ -165,7 +337,7 @@ private Response ReportIssue(int requestId, IssueState issue, string comment) } originalRequest.Issues = issue; originalRequest.OtherMessage = !string.IsNullOrEmpty(comment) - ? $"{Session[SessionKeys.UsernameKey]} - {comment}" + ? $"{Username} - {comment}" : string.Empty; @@ -173,11 +345,11 @@ private Response ReportIssue(int requestId, IssueState issue, string comment) var model = new NotificationModel { - User = Session[SessionKeys.UsernameKey].ToString(), + User = Username, NotificationType = NotificationType.Issue, Title = originalRequest.Title, DateTime = DateTime.Now, - Body = issue == IssueState.Other ? comment : issue.Humanize() + Body = issue == IssueState.Other ? comment : issue.ToString().CamelCaseToWords() }; NotificationService.Publish(model); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 1a77c6247..5b541091c 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -31,15 +31,18 @@ using Nancy; using Nancy.Responses.Negotiation; +using Nancy.Security; using NLog; using PlexRequests.Api; using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Music; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Exceptions; +using PlexRequests.Services; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store; @@ -54,7 +57,7 @@ public SearchModule(ICacheProvider cache, ISettingsService 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 +72,24 @@ public SearchModule(ICacheProvider cache, ISettingsService 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["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id); 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 +103,18 @@ public SearchModule(ICacheProvider cache, ISettingsService 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 bool IsAdmin { + get + { + return Context.CurrentUser.IsAuthenticated(); + } + } private Negotiator RequestLoad() { @@ -152,6 +171,28 @@ private Response SearchTvShow(string searchTerm) 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) + { + 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, + ReleaseType = a.status, + Country = a.country + }); + } + return Response.AsJson(model); + } + private Response UpcomingMovies() // TODO : Not used { var movies = MovieApi.GetUpcomingMovies(); @@ -172,30 +213,42 @@ private Response CurrentlyPlayingMovies() // TODO : Not used private Response RequestMovie(int movieId) { + var movieApi = new TheMovieDbApi(); + var movieInfo = movieApi.GetMovieInformation(movieId).Result; + 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 = "Movie 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); - var movieApi = new TheMovieDbApi(); - var movieInfo = movieApi.GetMovieInformation(movieId).Result; - Log.Trace("Getting movie info from TheMovieDb"); - Log.Trace(movieInfo.DumpJson); - //#if !DEBUG try { - if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) + if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString(),null, PlexType.Movie)) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{movieInfo.Title} is already in Plex!" }); + return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} is already in Plex!" }); } } catch (ApplicationSettingsException) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {movieInfo.Title} is in Plex, are you sure it's correctly setup?" }); + return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {fullMovieName} is in Plex, are you sure it's correctly setup?" }); } //#endif @@ -209,16 +262,14 @@ private Response RequestMovie(int movieId) 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 (ShouldAutoApprove(RequestType.Movie, settings)) { var cpSettings = CpService.GetSettings(); @@ -239,13 +290,13 @@ private Response RequestMovie(int movieId) var notificationModel = new NotificationModel { Title = model.Title, - User = model.RequestedBy, + User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; 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 @@ -264,13 +315,13 @@ private Response RequestMovie(int movieId) var notificationModel = new NotificationModel { Title = model.Title, - User = model.RequestedBy, + User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); - return Response.AsJson(new JsonResponseModel { Result = true }); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } } @@ -279,10 +330,10 @@ private Response RequestMovie(int movieId) Log.Debug("Adding movie to database requests"); var id = RequestService.AddRequest(model); - var notificationModel = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; + var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); - return Response.AsJson(new JsonResponseModel { Result = true }); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" }); } catch (Exception e) { @@ -300,30 +351,44 @@ private Response RequestMovie(int movieId) /// private Response RequestTvShow(int showId, string seasons) { - if (RequestService.CheckRequest(showId)) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "TV Show has already been requested!" }); - } - var tvApi = new TvMazeApi(); var showInfo = tvApi.ShowLookupByTheTvDbId(showId); + DateTime firstAir; + DateTime.TryParse(showInfo.premiered, out firstAir); + string fullShowName = $"{showInfo.name} ({firstAir.Year})"; //#if !DEBUG + + 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) + { + // 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 { - if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4))) // Take only the year Format = 2014-01-01 + if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4), null, PlexType.TvShow)) // Take only the year Format = 2014-01-01 { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{showInfo.name} is already in Plex!" }); + return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} is already in Plex!" }); } } catch (ApplicationSettingsException) { - return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {showInfo.name} is in Plex, are you sure it's correctly setup?" }); + return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {fullShowName} is in Plex, are you sure it's correctly setup?" }); } //#endif - DateTime firstAir; - DateTime.TryParse(showInfo.premiered, out firstAir); + var model = new RequestedModel { ProviderId = showInfo.externals?.thetvdb ?? 0, @@ -333,9 +398,9 @@ private Response RequestTvShow(int showId, string seasons) 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 @@ -358,26 +423,26 @@ private Response RequestTvShow(int showId, string seasons) model.SeasonList = seasonsList.ToArray(); - var settings = PrService.GetSettings(); - if (!settings.RequireTvShowApproval) + if (ShouldAutoApprove(RequestType.TvShow, settings)) { 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 = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; + NotificationService.Publish(notify1); - return Response.AsJson(new JsonResponseModel { Result = true }); + 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." }); } @@ -391,10 +456,10 @@ private Response RequestTvShow(int showId, string seasons) Log.Debug("Adding tv to database requests (No approval required & SickRage)"); RequestService.AddRequest(model); - var notify2 = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; + var notify2 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notify2); - return Response.AsJson(new JsonResponseModel { Result = true }); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message != null ? "Message From SickRage: " + result.message : "Something went wrong adding the movie to SickRage! Please check your settings." }); } @@ -405,16 +470,157 @@ private Response RequestTvShow(int showId, string seasons) RequestService.AddRequest(model); - var notificationModel = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; + var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; NotificationService.Publish(notificationModel); - return Response.AsJson(new { Result = true }); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" }); } - private bool CheckIfTitleExistsInPlex(string title, string year) + private bool CheckIfTitleExistsInPlex(string title, string year, string artist, PlexType type) { - var result = Checker.IsAvailable(title, year); + var result = Checker.IsAvailable(title, year, artist, type); 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); + DateTime release; + DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release); + + var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist; + if (artist == null) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "We could not find the artist on MusicBrainz. Please try again later or contact your admin" }); + } + + var alreadyInPlex = CheckIfTitleExistsInPlex(albumInfo.title, release.ToString("yyyy"), artist.name, PlexType.Music); + + if (alreadyInPlex) + { + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"{albumInfo.title} is already in Plex!" + }); + } + + 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, + RequestedDate = DateTime.UtcNow, + ReleaseDate = release, + ArtistName = artist.name, + ArtistId = artist.id + }; + + + if (ShouldAutoApprove(RequestType.Album, settings)) + { + 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 sender = new HeadphonesSender(HeadphonesApi, hpSettings, RequestService); + sender.AddAlbum(model); + model.Approved = true; + RequestService.AddRequest(model); + + return + Response.AsJson(new JsonResponseModel + { + Result = true, + Message = $"{model.Title} was successfully added!" + }); + } + + 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; + } + + private bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings) + { + // if the user is an admin or they are whitelisted, they go ahead and allow auto-approval + if (IsAdmin || prSettings.ApprovalWhiteList.Any(x => x.Equals(Username, StringComparison.OrdinalIgnoreCase))) return true; + + // check by request type if the category requires approval or not + switch (requestType) + { + case RequestType.Movie: + return !prSettings.RequireMovieApproval; + case RequestType.TvShow: + return !prSettings.RequireTvShowApproval; + case RequestType.Album: + return !prSettings.RequireMusicApproval; + default: + return false; + } + } } -} \ 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 @@ public Negotiator Index() 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 @@ private Response LoginUser() 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 @@ private bool CheckIfUserIsInPlexFriends(string username, string authToken) 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..e822b1a70 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -69,10 +69,6 @@ ..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll True
- - ..\packages\Humanizer.Core.2.0.1\lib\dotnet\Humanizer.dll - True - ..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll True @@ -168,9 +164,13 @@ + + + + @@ -248,6 +248,15 @@ Always + + moment.min.js + + + moment.min.es5.js + + + Always + pace.scss @@ -371,6 +380,9 @@ Always + + Always + web.config diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index c8f863f40..6d86572aa 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -64,7 +64,9 @@ static void Main(string[] args) var s = new Setup(); var cn = s.SetupDb(); + s.CacheQualityProfiles(); ConfigureTargets(cn); + if (port == -1) port = GetStartupPort(); 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..69c7d4732 --- /dev/null +++ b/PlexRequests.UI/Views/Admin/Headphones.cshtml @@ -0,0 +1,152 @@ +@Html.Partial("_Sidebar") +@{ + int port; + if (Model.Port == 0) + { + port = 8181; + } + 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/Sickrage.cshtml b/PlexRequests.UI/Views/Admin/Sickrage.cshtml index 31e22d351..94cb51c6e 100644 --- a/PlexRequests.UI/Views/Admin/Sickrage.cshtml +++ b/PlexRequests.UI/Views/Admin/Sickrage.cshtml @@ -75,15 +75,10 @@
diff --git a/PlexRequests.UI/Views/Admin/Sonarr.cshtml b/PlexRequests.UI/Views/Admin/Sonarr.cshtml index 72f0637b2..cad235f58 100644 --- a/PlexRequests.UI/Views/Admin/Sonarr.cshtml +++ b/PlexRequests.UI/Views/Admin/Sonarr.cshtml @@ -131,6 +131,9 @@ { var qualitySelected = @Model.QualityProfile; + if (!qualitySelected) { + return; + } var $form = $("#mainForm"); $.ajax({ type: $form.prop("method"), diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index efb8ce36b..5eb4d757f 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 (Beta) + } + else + { + Headphones (Beta) + } @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..759a16e0b 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..13025d053 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -7,8 +7,8 @@ Plex Requests - + @@ -21,6 +21,7 @@ + @@ -87,8 +88,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.UI/compilerconfig.json b/PlexRequests.UI/compilerconfig.json index 789c03398..f45d98c6d 100644 --- a/PlexRequests.UI/compilerconfig.json +++ b/PlexRequests.UI/compilerconfig.json @@ -6,5 +6,9 @@ { "outputFile": "Content/pace.css", "inputFile": "Content/pace.scss" + }, + { + "outputFile": "Content/moment.min.es5.js", + "inputFile": "Content/moment.min.js" } ] \ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index 6e12f0510..5b88b4438 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -3,7 +3,6 @@ - diff --git a/PlexRequests.UI/web.config b/PlexRequests.UI/web.config index e5b751f70..3c9dd742a 100644 --- a/PlexRequests.UI/web.config +++ b/PlexRequests.UI/web.config @@ -1,49 +1,52 @@ - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PlexRequests.sln b/PlexRequests.sln index 623a5a2e0..e3feb5b1d 100644 --- a/PlexRequests.sln +++ b/PlexRequests.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.UI", "PlexRequests.UI\PlexRequests.UI.csproj", "{68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}" EndProject @@ -17,7 +17,6 @@ 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 @@ -34,8 +33,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Api.Models", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Services.Tests", "PlexRequests.Services.Tests\PlexRequests.Services.Tests.csproj", "{EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Helpers.Tests", "PlexRequests.Helpers.Tests\PlexRequests.Helpers.Tests.csproj", "{0E6395D3-B074-49E8-898D-0EB99E507E0E}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,10 +83,6 @@ Global {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAADB4AC-064F-4D3A-AFF9-64A33131A9A7}.Release|Any CPU.Build.0 = Release|Any CPU - {0E6395D3-B074-49E8-898D-0EB99E507E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E6395D3-B074-49E8-898D-0EB99E507E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E6395D3-B074-49E8-898D-0EB99E507E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E6395D3-B074-49E8-898D-0EB99E507E0E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 23fd72bf2..ac29e41e0 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,63 @@ Do you have an issue or a question? if so check out our [FAQ!](https://github.co 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: +#Debian/Ubuntu + +To configure PlexRequests to run on debian/ubuntu and set it to start up with the system, do the following (via terminal): + +####Create a location to drop the files (up to you, we'll use /opt/PlexRequests as an example) + +```sudo mkdir /opt/PlexRequests``` + +####Download the release zip +``` +sudo wget {release zip file url} +sudo unzip PlexRequests.zip -d /opt/PlexRequests +``` + +####Install Mono (must be on v4.x or above for compatibility) + +```sudo apt-get install mono-devel``` + +####Check your Mono version + +```sudo mono --version``` + +if you don't see v4.x or above, uninstall it, and check here for instructions: +http://www.mono-project.com/docs/getting-started/install/linux/ + +####Verify Mono properly runs PlexRequests + +```sudo /usr/bin/mono /opt/PlexRequests/Release/PlexRequests.exe``` + +####Create an upstart script to auto-start PlexRequests with your system (using port 80 in this example) + +```sudo nano /etc/init/plexrequests.conf``` + +#####Paste in the following: + +``` +start on runlevel [2345] +stop on runlevel [016] + +respawn +expect fork + +pre-start script + # echo "" +end script + +script + exec /usr/bin/mono /opt/PlexRequests/Release/PlexRequests.exe 80 +end script +``` + +####Reboot, then open up your browser to check that it's running! + +``` +sudo shutdown -r 00 +``` + # Contributors We are looking for any contributions to the project! Just pick up a task, if you have any questions ask and i'll get straight on it! @@ -49,4 +106,4 @@ If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet) ## A massive thanks to everyone below for all their help! -[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727) +[heartisall](https://github.com/heartisall), [Stuke00](https://github.com/Stuke00), [shiitake](https://github.com/shiitake), [Drewster727](https://github.com/Drewster727), Majawat, [EddiYo](https://github.com/EddiYo) diff --git a/appveyor.yml b/appveyor.yml index 954fbc48c..adf8268ba 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,11 @@ -version: 1.5.{build} +version: 1.6.{build} configuration: Release assembly_info: patch: true file: '**\AssemblyInfo.*' - assembly_version: '1.5.2' + assembly_version: '1.6.0' assembly_file_version: '{version}' - assembly_informational_version: '1.5.2' + assembly_informational_version: '1.6.0' before_build: - cmd: appveyor-retry nuget restore build: