Merge pull request #140 from tidusjar/dev

Merge dev to master. Release 1.6
This commit is contained in:
Jamie 2016-04-06 07:39:34 +01:00
commit 3277830de4
113 changed files with 5764 additions and 724 deletions

View file

@ -33,6 +33,7 @@ namespace PlexRequests.Api.Interfaces
public interface IApiRequest public interface IApiRequest
{ {
T Execute<T>(IRestRequest request, Uri baseUri) where T : new(); T Execute<T>(IRestRequest request, Uri baseUri) where T : new();
IRestResponse Execute(IRestRequest request, Uri baseUri);
T ExecuteXml<T>(IRestRequest request, Uri baseUri) where T : class; T ExecuteXml<T>(IRestRequest request, Uri baseUri) where T : class;
T ExecuteJson<T>(IRestRequest request, Uri baseUri) where T : new(); T ExecuteJson<T>(IRestRequest request, Uri baseUri) where T : new();
} }

View file

@ -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<bool> AddAlbum(string apiKey, Uri baseUrl, string albumId);
HeadphonesVersion GetVersion(string apiKey, Uri baseUrl);
Task<bool> AddArtist(string apiKey, Uri baseUrl, string artistId);
Task<bool> QueueAlbum(string apiKey, Uri baseUrl, string albumId);
Task<List<HeadphonesGetIndex>> GetIndex(string apiKey, Uri baseUrl);
Task<bool> RefreshArtist(string apiKey, Uri baseUrl, string artistId);
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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; }
}
}

View file

@ -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
}
}

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

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

View file

@ -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<ArtistCredit> 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<ReleaseEvent> ReleaseEvents { get; set; }
public string id { get; set; }
}
}

View file

@ -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<Alias> 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<string> SecondaryTypes { get; set; }
}
public class Area
{
public string id { get; set; }
public string name { get; set; }
[JsonProperty(PropertyName = "sort-name")]
public string SortName { get; set; }
[JsonProperty(PropertyName = "iso-3166-1-codes")]
public List<string> ISO31661Codes { get; set; }
}
public class ReleaseEvent
{
public string date { get; set; }
public Area area { get; set; }
}
public class Label
{
public string id { get; set; }
public string name { get; set; }
}
public class LabelInfo
{
[JsonProperty(PropertyName = "catalog-number")]
public string CatalogNumber { get; set; }
public Label label { get; set; }
}
public class Medium
{
public string format { get; set; }
[JsonProperty(PropertyName = "disc-count")]
public int DiscCount { get; set; }
[JsonProperty(PropertyName = "catalog-number")]
public int CatalogNumber { get; set; }
}
public class Release
{
public string id { get; set; }
public string score { get; set; }
public int count { get; set; }
public string title { get; set; }
public string status { get; set; }
public string disambiguation { get; set; }
public string packaging { get; set; }
[JsonProperty(PropertyName = "text-representation")]
public TextRepresentation TextRepresentation { get; set; }
[JsonProperty(PropertyName = "artist-credit")]
public List<ArtistCredit> ArtistCredit { get; set; }
[JsonProperty(PropertyName = "release-group")]
public ReleaseGroup ReleaseGroup { get; set; }
public string date { get; set; }
public string country { get; set; }
[JsonProperty(PropertyName = "release-events")]
public List<ReleaseEvent> ReleaseEvents { get; set; }
public string barcode { get; set; }
public string asin { get; set; }
[JsonProperty(PropertyName = "label-info")]
public List<LabelInfo> LabelInfo { get; set; }
[JsonProperty(PropertyName = "track-count")]
public int TrackCount { get; set; }
public List<Medium> media { get; set; }
}
public class MusicBrainzSearchResults
{
public string created { get; set; }
public int count { get; set; }
public int offset { get; set; }
public List<Release> releases { get; set; }
}
}

View file

@ -303,6 +303,8 @@ namespace PlexRequests.Api.Models.Plex
public string AddedAt { get; set; } public string AddedAt { get; set; }
[XmlAttribute(AttributeName = "updatedAt")] [XmlAttribute(AttributeName = "updatedAt")]
public string UpdatedAt { get; set; } public string UpdatedAt { get; set; }
[XmlAttribute(AttributeName = "parentTitle")]
public string ParentTitle { get; set; }
} }
@ -310,7 +312,7 @@ namespace PlexRequests.Api.Models.Plex
public class PlexSearch public class PlexSearch
{ {
[XmlElement(ElementName = "Directory")] [XmlElement(ElementName = "Directory")]
public Directory1 Directory { get; set; } public List<Directory1> Directory { get; set; }
[XmlElement(ElementName = "Video")] [XmlElement(ElementName = "Video")]
public List<Video> Video { get; set; } public List<Video> Video { get; set; }
[XmlElement(ElementName = "Provider")] [XmlElement(ElementName = "Provider")]

View file

@ -48,6 +48,13 @@
<Compile Include="Movie\CouchPotatoAdd.cs" /> <Compile Include="Movie\CouchPotatoAdd.cs" />
<Compile Include="Movie\CouchPotatoProfiles.cs" /> <Compile Include="Movie\CouchPotatoProfiles.cs" />
<Compile Include="Movie\CouchPotatoStatus.cs" /> <Compile Include="Movie\CouchPotatoStatus.cs" />
<Compile Include="Music\HeadphonesAlbumSearchResult.cs" />
<Compile Include="Music\HeadphonesArtistSearchResult.cs" />
<Compile Include="Music\HeadphonesGetIndex.cs" />
<Compile Include="Music\HeadphonesVersion.cs" />
<Compile Include="Music\MusicBrainzCoverArt.cs" />
<Compile Include="Music\MusicBrainzReleaseInfo.cs" />
<Compile Include="Music\MusicBrainzSearchResults.cs" />
<Compile Include="Notifications\PushbulletPush.cs" /> <Compile Include="Notifications\PushbulletPush.cs" />
<Compile Include="Notifications\PushbulletResponse.cs" /> <Compile Include="Notifications\PushbulletResponse.cs" />
<Compile Include="Notifications\PushoverResponse.cs" /> <Compile Include="Notifications\PushoverResponse.cs" />
@ -59,11 +66,14 @@
<Compile Include="Plex\PlexStatus.cs" /> <Compile Include="Plex\PlexStatus.cs" />
<Compile Include="Plex\PlexUserRequest.cs" /> <Compile Include="Plex\PlexUserRequest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SickRage\SickRageBase.cs" />
<Compile Include="SickRage\SickRagePing.cs" /> <Compile Include="SickRage\SickRagePing.cs" />
<Compile Include="SickRage\SickRageSeasonList.cs" />
<Compile Include="SickRage\SickRageShowInformation.cs" /> <Compile Include="SickRage\SickRageShowInformation.cs" />
<Compile Include="SickRage\SickRageStatus.cs" /> <Compile Include="SickRage\SickRageStatus.cs" />
<Compile Include="SickRage\SickRageTvAdd.cs" /> <Compile Include="SickRage\SickRageTvAdd.cs" />
<Compile Include="Sonarr\SonarrAddSeries.cs" /> <Compile Include="Sonarr\SonarrAddSeries.cs" />
<Compile Include="Sonarr\SonarrError.cs" />
<Compile Include="Sonarr\SonarrProfile.cs" /> <Compile Include="Sonarr\SonarrProfile.cs" />
<Compile Include="Sonarr\SystemStatus.cs" /> <Compile Include="Sonarr\SystemStatus.cs" />
<Compile Include="Tv\Authentication.cs" /> <Compile Include="Tv\Authentication.cs" />

View file

@ -0,0 +1,9 @@
namespace PlexRequests.Api.Models.SickRage
{
public class SickRageBase<T>
{
public T data { get; set; }
public string message { get; set; }
public string result { get; set; }
}
}

View file

@ -31,10 +31,7 @@ namespace PlexRequests.Api.Models.SickRage
public int pid { get; set; } public int pid { get; set; }
} }
public class SickRagePing public class SickRagePing : SickRageBase<SickRagePingData>
{ {
public SickRagePingData data { get; set; }
public string message { get; set; }
public string result { get; set; }
} }
} }

View file

@ -0,0 +1,6 @@
namespace PlexRequests.Api.Models.SickRage
{
public class SickRageSeasonList : SickRageBase<int[]>
{
}
}

View file

@ -75,11 +75,8 @@ namespace PlexRequests.Api.Models.SickRage
public int tvdbid { get; set; } public int tvdbid { get; set; }
} }
public class SickRageShowInformation public class SickRageShowInformation : SickRageBase<Data>
{ {
public Data data { get; set; }
public string message { get; set; }
public string result { get; set; }
} }
} }

View file

@ -31,11 +31,8 @@ namespace PlexRequests.Api.Models.SickRage
public string name { get; set; } public string name { get; set; }
} }
public class SickRageTvAdd public class SickRageTvAdd : SickRageBase<SickRageTvAddData>
{ {
public SickRageTvAddData data { get; set; }
public string message { get; set; }
public string result { get; set; }
} }
} }

View file

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

View file

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

View file

@ -26,13 +26,9 @@
#endregion #endregion
using System; using System;
using System.IO; using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog; using NLog;
@ -68,6 +64,21 @@ namespace PlexRequests.Api
} }
public IRestResponse Execute(IRestRequest request, Uri baseUri)
{
var client = new RestClient { BaseUrl = baseUri };
var response = client.Execute(request);
if (response.ErrorException != null)
{
var message = "Error retrieving response. Check inner details for more info.";
throw new ApplicationException(message, response.ErrorException);
}
return response;
}
public T ExecuteXml<T>(IRestRequest request, Uri baseUri) where T : class public T ExecuteXml<T>(IRestRequest request, Uri baseUri) where T : class
{ {
var client = new RestClient { BaseUrl = baseUri }; var client = new RestClient { BaseUrl = baseUri };
@ -96,20 +107,13 @@ namespace PlexRequests.Api
throw new ApplicationException(message, response.ErrorException); throw new ApplicationException(message, response.ErrorException);
} }
try
{
var json = JsonConvert.DeserializeObject<T>(response.Content); var json = JsonConvert.DeserializeObject<T>(response.Content);
return json; return json;
} }
catch (Exception e)
{
Log.Fatal(e);
Log.Info(response.Content);
throw;
}
}
public T DeserializeXml<T>(string input) private T DeserializeXml<T>(string input)
where T : class where T : class
{ {
var ser = new XmlSerializer(typeof(T)); var ser = new XmlSerializer(typeof(T));

View file

@ -0,0 +1,204 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: HeadphonesApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Music;
using PlexRequests.Helpers;
using RestSharp;
namespace PlexRequests.Api
{
public class HeadphonesApi : IHeadphonesApi
{
public HeadphonesApi()
{
Api = new ApiRequest();
}
private ApiRequest Api { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public async Task<bool> AddAlbum(string apiKey, Uri baseUrl, string albumId)
{
Log.Trace("Adding album: {0}", albumId);
var request = new RestRequest
{
Resource = "/api?cmd=addAlbum&id={albumId}",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddUrlSegment("albumId", albumId);
try
{
var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false);
Log.Trace("Add Album Result: {0}", result.DumpJson());
var albumResult = result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase);
Log.Info("Album add result {0}", albumResult);
return albumResult;
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
return false; // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public async Task<List<HeadphonesGetIndex>> GetIndex(string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddQueryParameter("cmd", "getIndex");
try
{
var result = await Task.Run(() => Api.ExecuteJson<List<HeadphonesGetIndex>>(request, baseUrl)).ConfigureAwait(false);
return result;
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
return new List<HeadphonesGetIndex>();
}
}
public async Task<bool> AddArtist(string apiKey, Uri baseUrl, string artistId)
{
Log.Trace("Adding Artist: {0}", artistId);
var request = new RestRequest
{
Resource = "/api",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddQueryParameter("cmd", "addArtist");
request.AddQueryParameter("id", artistId);
try
{
var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false);
Log.Info("Add Artist Result: {0}", result.Content);
Log.Trace("Add Artist Result: {0}", result.DumpJson());
return result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase);
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
return false; // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public async Task<bool> QueueAlbum(string apiKey, Uri baseUrl, string albumId)
{
Log.Trace("Queing album: {0}", albumId);
var request = new RestRequest
{
Resource = "/api?cmd=queueAlbum&id={albumId}",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddUrlSegment("albumId", albumId);
try
{
var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false);
Log.Info("Queue Result: {0}", result.Content);
Log.Trace("Queue Result: {0}", result.DumpJson());
return result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase);
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
return false; // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public async Task<bool> RefreshArtist(string apiKey, Uri baseUrl, string artistId)
{
Log.Trace("Refreshing artist: {0}", artistId);
var request = new RestRequest
{
Resource = "/api",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddQueryParameter("cmd", "queueAlbum");
request.AddQueryParameter("id", artistId);
try
{
var result = await Task.Run(() => Api.Execute(request, baseUrl)).ConfigureAwait(false);
Log.Info("Artist refresh Result: {0}", result.Content);
Log.Trace("Artist refresh Result: {0}", result.DumpJson());
return result.Content.Equals("OK", StringComparison.CurrentCultureIgnoreCase);
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
return false; // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public HeadphonesVersion GetVersion(string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api",
Method = Method.GET
};
request.AddQueryParameter("apikey", apiKey);
request.AddQueryParameter("cmd", "getVersion");
try
{
return Api.ExecuteJson<HeadphonesVersion>(request, baseUrl);
}
catch (JsonSerializationException jse)
{
Log.Error(jse);
return new HeadphonesVersion(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
}
}

View file

@ -0,0 +1,116 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: MusicBrainzApi.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System;
using Newtonsoft.Json;
using NLog;
using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Music;
using RestSharp;
namespace PlexRequests.Api
{
public class MusicBrainzApi : IMusicBrainzApi
{
public MusicBrainzApi()
{
Api = new ApiRequest();
}
private ApiRequest Api { get; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private readonly Uri BaseUri = new Uri("http://musicbrainz.org/ws/2/");
public MusicBrainzSearchResults SearchAlbum(string searchTerm)
{
Log.Trace("Searching for album: {0}", searchTerm);
var request = new RestRequest
{
Resource = "release/?query={searchTerm}&fmt=json",
Method = Method.GET
};
request.AddUrlSegment("searchTerm", searchTerm);
try
{
return Api.ExecuteJson<MusicBrainzSearchResults>(request, BaseUri);
}
catch (JsonSerializationException jse)
{
Log.Warn(jse);
return new MusicBrainzSearchResults(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public MusicBrainzReleaseInfo GetAlbum(string releaseId)
{
Log.Trace("Getting album: {0}", releaseId);
var request = new RestRequest
{
Resource = "release/{albumId}",
Method = Method.GET
};
request.AddUrlSegment("albumId", releaseId);
request.AddQueryParameter("fmt", "json");
request.AddQueryParameter("inc", "artists");
try
{
return Api.ExecuteJson<MusicBrainzReleaseInfo>(request, BaseUri);
}
catch (JsonSerializationException jse)
{
Log.Warn(jse);
return new MusicBrainzReleaseInfo(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
public MusicBrainzCoverArt GetCoverArt(string releaseId)
{
Log.Trace("Getting cover art for release: {0}", releaseId);
var request = new RestRequest
{
Resource = "release/{releaseId}",
Method = Method.GET
};
request.AddUrlSegment("releaseId", releaseId);
try
{
return Api.Execute<MusicBrainzCoverArt>(request, new Uri("http://coverartarchive.org/"));
}
catch (Exception e)
{
Log.Warn(e);
return new MusicBrainzCoverArt(); // If there is no matching result we do not get returned a JSON string, it just returns "false".
}
}
}
}

View file

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

View file

@ -74,17 +74,18 @@ namespace PlexRequests.Api
var obj = Api.Execute<SickRageTvAdd>(request, baseUrl); var obj = Api.Execute<SickRageTvAdd>(request, baseUrl);
if (obj.result != "failure") if (obj.result != "failure")
{ {
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
// Check to see if it's been added yet. var seasonIncrement = 0;
var showInfo = new SickRageShowInformation { message = "Show not found" }; var seasonList = new SickRageSeasonList();
while (showInfo.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase)) 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 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."); 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 @@ namespace PlexRequests.Api
} }
sw.Stop(); sw.Stop();
} }
if (seasons.Length > 0) if (seasons.Length > 0)
{ {
//handle the seasons requested //handle the seasons requested
@ -123,6 +122,28 @@ namespace PlexRequests.Api
return obj; return obj;
} }
public SickRageSeasonList VerifyShowHasLoaded(int tvdbId, string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=show.seasonlist",
Method = Method.GET
};
request.AddUrlSegment("apiKey", apiKey);
request.AddQueryParameter("tvdbid", tvdbId.ToString());
try
{
var obj = Api.ExecuteJson<SickRageSeasonList>(request, baseUrl);
return obj;
}
catch (Exception e)
{
Log.Error(e);
return new SickRageSeasonList();
}
}
public async Task<SickRageTvAdd> AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl) public async Task<SickRageTvAdd> AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl)
{ {
var request = new RestRequest var request = new RestRequest
@ -138,21 +159,5 @@ namespace PlexRequests.Api
await Task.Run(() => Thread.Sleep(2000)); await Task.Run(() => Thread.Sleep(2000));
return await Task.Run(() => Api.Execute<SickRageTvAdd>(request, baseUrl)).ConfigureAwait(false); return await Task.Run(() => Api.Execute<SickRageTvAdd>(request, baseUrl)).ConfigureAwait(false);
} }
public SickRageShowInformation CheckShowHasBeenAdded(int tvdbId, string apiKey, Uri baseUrl)
{
var request = new RestRequest
{
Resource = "/api/{apiKey}/?cmd=show",
Method = Method.GET
};
request.AddUrlSegment("apiKey", apiKey);
request.AddQueryParameter("tvdbid", tvdbId.ToString());
var obj = Api.Execute<SickRageShowInformation>(request, baseUrl);
return obj;
}
} }
} }

View file

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

View file

@ -29,5 +29,8 @@ namespace PlexRequests.Core
public class CacheKeys public class CacheKeys
{ {
public const string TvDbToken = "TheTvDbApiToken"; public const string TvDbToken = "TheTvDbApiToken";
public const string SonarrQualityProfiles = "SonarrQualityProfiles";
public const string SickRageQualityProfiles = "SickRageQualityProfiles";
public const string CouchPotatoQualityProfiles = "CouchPotatoQualityProfiles";
} }
} }

View file

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

View file

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

View file

@ -25,11 +25,20 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System.Text.RegularExpressions;
namespace PlexRequests.Core.Models namespace PlexRequests.Core.Models
{ {
public class StatusModel public class StatusModel
{ {
public string Version { get; set; } 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 bool UpdateAvailable { get; set; }
public string UpdateUri { get; set; } public string UpdateUri { get; set; }
public string DownloadUri { get; set; } public string DownloadUri { get; set; }

View file

@ -46,6 +46,10 @@
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> <HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.2.3\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit, Version=0.19.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Octokit, Version=0.19.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath> <HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath>
<Private>True</Private> <Private>True</Private>
@ -74,6 +78,7 @@
<Compile Include="Models\StatusModel.cs" /> <Compile Include="Models\StatusModel.cs" />
<Compile Include="Models\UserProperties.cs" /> <Compile Include="Models\UserProperties.cs" />
<Compile Include="SettingModels\AuthenticationSettings.cs" /> <Compile Include="SettingModels\AuthenticationSettings.cs" />
<Compile Include="SettingModels\HeadphonesSettings.cs" />
<Compile Include="SettingModels\PushoverNotificationSettings.cs" /> <Compile Include="SettingModels\PushoverNotificationSettings.cs" />
<Compile Include="SettingModels\PushBulletNotificationSettings.cs" /> <Compile Include="SettingModels\PushBulletNotificationSettings.cs" />
<Compile Include="SettingModels\EmailNotificationSettings.cs" /> <Compile Include="SettingModels\EmailNotificationSettings.cs" />
@ -95,6 +100,10 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PlexRequests.Api.Interfaces\PlexRequests.Api.Interfaces.csproj">
<Project>{95834072-a675-415d-aa8f-877c91623810}</Project>
<Name>PlexRequests.Api.Interfaces</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Api.Models\PlexRequests.Api.Models.csproj"> <ProjectReference Include="..\PlexRequests.Api.Models\PlexRequests.Api.Models.csproj">
<Project>{CB37A5F8-6DFC-4554-99D3-A42B502E4591}</Project> <Project>{CB37A5F8-6DFC-4554-99D3-A42B502E4591}</Project>
<Name>PlexRequests.Api.Models</Name> <Name>PlexRequests.Api.Models</Name>

View file

@ -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;
}
}
}
}

View file

@ -24,6 +24,10 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace PlexRequests.Core.SettingModels namespace PlexRequests.Core.SettingModels
{ {
public class PlexRequestSettings : Settings public class PlexRequestSettings : Settings
@ -32,8 +36,33 @@ namespace PlexRequests.Core.SettingModels
public bool SearchForMovies { get; set; } public bool SearchForMovies { get; set; }
public bool SearchForTvShows { get; set; } public bool SearchForTvShows { get; set; }
public bool SearchForMusic { get; set; }
public bool RequireMovieApproval { get; set; } public bool RequireMovieApproval { get; set; }
public bool RequireTvShowApproval { get; set; } public bool RequireTvShowApproval { get; set; }
public bool RequireMusicApproval { get; set; }
public bool UsersCanViewOnlyOwnRequests { get; set; }
public int WeeklyRequestLimit { get; set; } public int WeeklyRequestLimit { get; set; }
public string NoApprovalUsers { get; set; }
[JsonIgnore]
public List<string> ApprovalWhiteList
{
get
{
var users = new List<string>();
if (string.IsNullOrEmpty(NoApprovalUsers))
{
return users;
}
var splitUsers = NoApprovalUsers.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var user in splitUsers)
{
if (!string.IsNullOrWhiteSpace(user))
users.Add(user.Trim());
}
return users;
}
}
} }
} }

View file

@ -28,6 +28,7 @@
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using System.Collections.Generic;
namespace PlexRequests.Core.SettingModels namespace PlexRequests.Core.SettingModels
{ {
@ -40,6 +41,23 @@ namespace PlexRequests.Core.SettingModels
public string QualityProfile { get; set; } public string QualityProfile { get; set; }
public bool Ssl { get; set; } public bool Ssl { get; set; }
public string SubDir { get; set; } public string SubDir { get; set; }
public Dictionary<string, string> Qualities
{
get
{
return new Dictionary<string, string>() {
{ "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] [JsonIgnore]
public Uri FullUri public Uri FullUri

View file

@ -30,16 +30,19 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Mono.Data.Sqlite; using Mono.Data.Sqlite;
using NLog;
using PlexRequests.Api; using PlexRequests.Api;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using System.Threading.Tasks;
namespace PlexRequests.Core namespace PlexRequests.Core
{ {
public class Setup public class Setup
{ {
private static Logger Log = LogManager.GetCurrentClassLogger();
private static DbConfiguration Db { get; set; } private static DbConfiguration Db { get; set; }
public string SetupDb() public string SetupDb()
{ {
@ -52,12 +55,40 @@ namespace PlexRequests.Core
CreateDefaultSettingsPage(); CreateDefaultSettingsPage();
} }
MigrateDb(); var version = CheckSchema();
if (version > 0)
{
if (version > 1300 && version <= 1699)
{
MigrateDbFrom1300();
UpdateRequestBlobsTable();
}
}
return Db.DbConnection().ConnectionString; return Db.DbConnection().ConnectionString;
} }
public static string ConnectionString => 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() private void CreateDefaultSettingsPage()
{ {
var defaultSettings = new PlexRequestSettings var defaultSettings = new PlexRequestSettings
@ -72,8 +103,82 @@ namespace PlexRequests.Core
s.SaveSettings(defaultSettings); 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<SonarrSettings>(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<CouchPotatoSettings>(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<long>(); var result = new List<long>();
RequestedModel[] requestedModels; RequestedModel[] requestedModels;
var repo = new GenericRepository<RequestedModel>(Db, new MemoryCacheProvider()); var repo = new GenericRepository<RequestedModel>(Db, new MemoryCacheProvider());
@ -113,7 +218,7 @@ namespace PlexRequests.Core
Issues = r.Issues, Issues = r.Issues,
OtherMessage = r.OtherMessage, OtherMessage = r.OtherMessage,
Overview = show.summary.RemoveHtml(), Overview = show.summary.RemoveHtml(),
RequestedBy = r.RequestedBy, RequestedUsers = r.AllUsers, // should pull in the RequestedBy property and merge with RequestedUsers
RequestedDate = r.ReleaseDate, RequestedDate = r.ReleaseDate,
Status = show.status Status = show.status
}; };

View file

@ -26,8 +26,6 @@
#endregion #endregion
using System; using System;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
using Octokit; using Octokit;
@ -62,7 +60,10 @@ namespace PlexRequests.Core
}; };
var latestRelease = GetLatestRelease(); var latestRelease = GetLatestRelease();
if (latestRelease.Result == null)
{
return new StatusModel { Version = "Unknown" };
}
var latestVersionArray = latestRelease.Result.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries); var latestVersionArray = latestRelease.Result.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries);
var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty; var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty;

View file

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

View file

@ -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);
}
}
}

View file

@ -55,7 +55,7 @@ namespace PlexRequests.Helpers
/// <param name="key">The key.</param> /// <param name="key">The key.</param>
/// <param name="data">The object we want to store.</param> /// <param name="data">The object we want to store.</param>
/// <param name="cacheTime">The amount of time we want to cache the object.</param> /// <param name="cacheTime">The amount of time we want to cache the object.</param>
void Set(string key, object data, int cacheTime); void Set(string key, object data, int cacheTime = 20);
/// <summary> /// <summary>
/// Removes the specified object from the cache. /// Removes the specified object from the cache.

View file

@ -83,7 +83,7 @@ namespace PlexRequests.Helpers
/// <param name="key">The key.</param> /// <param name="key">The key.</param>
/// <param name="data">The object we want to store.</param> /// <param name="data">The object we want to store.</param>
/// <param name="cacheTime">The amount of time we want to cache the object.</param> /// <param name="cacheTime">The amount of time we want to cache the object.</param>
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) }; var policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime) };
Cache.Add(new CacheItem(key, data), policy); Cache.Add(new CacheItem(key, data), policy);

View file

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

View file

@ -32,12 +32,12 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models;
using PlexRequests.Api.Models.Plex; using PlexRequests.Api.Models.Plex;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers.Exceptions; using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Store;
namespace PlexRequests.Services.Tests namespace PlexRequests.Services.Tests
{ {
@ -55,7 +55,7 @@ namespace PlexRequests.Services.Tests
var plexMock = new Mock<IPlexApi>(); var plexMock = new Mock<IPlexApi>();
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object); Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Assert.Throws<ApplicationSettingsException>(() => Checker.IsAvailable("title", "2013"), "We should be throwing an exception since we cannot talk to the services."); Assert.Throws<ApplicationSettingsException>(() => Checker.IsAvailable("title", "2013", null, PlexType.TvShow), "We should be throwing an exception since we cannot talk to the services.");
} }
[Test] [Test]
@ -74,20 +74,20 @@ namespace PlexRequests.Services.Tests
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object); Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2011"); var result = Checker.IsAvailable("title", "2011", null, PlexType.Movie);
Assert.That(result, Is.True); Assert.That(result, Is.True);
} }
[Test] [Test]
public void IsAvailableDirectoryTitleTest() public void IsAvailableMusicDirectoryTitleTest()
{ {
var settingsMock = new Mock<ISettingsService<PlexSettings>>(); var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>(); var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>(); var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>(); var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Directory = new Directory1 {Title = "title", Year = "2013"} }; var searchResult = new PlexSearch { Directory = new List<Directory1> { new Directory1 { Title = "title", Year = "2013", ParentTitle = "dIzZy"} } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" }); settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" }); authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
@ -95,7 +95,49 @@ namespace PlexRequests.Services.Tests
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object); Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2013"); var result = Checker.IsAvailable("title", "2013", "dIzZy", PlexType.Music);
Assert.That(result, Is.True);
}
[Test]
public void IsNotAvailableMusicDirectoryTitleTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Directory = new List<Directory1> { new Directory1 { Title = "titale2", Year = "1992", ParentTitle = "dIzZy" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2013", "dIzZy", PlexType.Music);
Assert.That(result, Is.False);
}
[Test]
public void IsAvailableDirectoryTitleWithoutYearTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Directory = new List<Directory1> { new Directory1 { Title = "title", } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", null, null, PlexType.Movie);
Assert.That(result, Is.True); Assert.That(result, Is.True);
} }
@ -108,7 +150,7 @@ namespace PlexRequests.Services.Tests
var requestMock = new Mock<IRequestService>(); var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>(); var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "wrong tistle", Year = "2011"} } }; var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "wrong title", Year = "2011" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" }); settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" }); authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
@ -116,7 +158,28 @@ namespace PlexRequests.Services.Tests
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object); Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2011"); var result = Checker.IsAvailable("title", "2011", null, PlexType.Movie);
Assert.That(result, Is.False);
}
[Test]
public void IsNotAvailableTestWihtoutYear()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "wrong title" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", null, null, PlexType.Movie);
Assert.That(result, Is.False); Assert.That(result, Is.False);
} }
@ -137,9 +200,348 @@ namespace PlexRequests.Services.Tests
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object); Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2011"); var result = Checker.IsAvailable("title", "2011", null, PlexType.Movie);
Assert.That(result, Is.False); Assert.That(result, Is.False);
} }
[Test]
public void TitleDoesNotMatchTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "title23", Year = "2019" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", "2019", null, PlexType.Movie);
Assert.That(result, Is.False);
}
[Test]
public void TitleDoesNotMatchWithoutYearTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
var searchResult = new PlexSearch { Video = new List<Video> { new Video { Title = "title23" } } };
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "abc" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(searchResult);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
var result = Checker.IsAvailable("title", null, null, PlexType.Movie);
Assert.That(result, Is.False);
}
[Test]
public void CheckAndUpdateNoPlexSettingsTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
}
[Test]
public void CheckAndUpdateNoAuthSettingsTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "123" });
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
}
[Test]
public void CheckAndUpdateNoRequestsTest()
{
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(new List<RequestedModel>());
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Never);
}
[Test]
public void CheckAndUpdateRequestsThatDoNotExistInPlexTest()
{
var requests = new List<RequestedModel> {
new RequestedModel
{
Id = 123,
Title = "title1",
Available = false,
},
new RequestedModel
{
Id=222,
Title = "title3",
Available = false
},
new RequestedModel
{
Id = 333,
Title= "missingTitle",
Available = false
},
new RequestedModel
{
Id= 444,
Title = "already found",
Available = true
}
};
var search = new PlexSearch
{
Video = new List<Video>
{
new Video
{
Title = "Title4",
Year = "2012"
},
new Video
{
Title = "Title2",
}
},
Directory = new List<Directory1> { new Directory1
{
Title = "Title9",
Year = "1978"
}}
};
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(requests);
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(search);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Never);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Exactly(3));
}
[Test]
public void CheckAndUpdateRequestsAllRequestsTest()
{
var requests = new List<RequestedModel> {
new RequestedModel
{
Id = 123,
Title = "title1",
Available = false,
},
new RequestedModel
{
Id=222,
Title = "title3",
Available = false
},
new RequestedModel
{
Id = 333,
Title= "missingTitle",
Available = false
},
new RequestedModel
{
Id= 444,
Title = "Hi",
Available = false
}
};
var search = new PlexSearch
{
Video = new List<Video>
{
new Video
{
Title = "title1",
Year = "2012"
},
new Video
{
Title = "Title3",
}
,
new Video
{
Title = "Hi",
}
},
Directory = new List<Directory1> { new Directory1
{
Title = "missingTitle",
Year = "1978"
}}
};
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(requests);
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(search);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Once);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Exactly(4));
}
[Test]
public void CheckAndUpdateAllMusicRequestsTest()
{
var requests = new List<RequestedModel> {
new RequestedModel
{
Id = 123,
Title = "title1",
Available = false,
ArtistName = "dizzy",
Type = RequestType.Album,
ReleaseDate = new DateTime(2010,1,1)
},
new RequestedModel
{
Id=222,
Title = "title3",
Available = false,
ArtistName = "a",
Type = RequestType.Album,
ReleaseDate = new DateTime(2006,1,1)
},
new RequestedModel
{
Id = 333,
Title= "missingTitle",
Available = false,
ArtistName = "b",
Type = RequestType.Album,
ReleaseDate = new DateTime(1992,1,1)
},
new RequestedModel
{
Id= 444,
Title = "Hi",
Available = false,
ArtistName = "c",
Type = RequestType.Album,
ReleaseDate = new DateTime(2017,1,1)
}
};
var search = new PlexSearch
{
Directory = new List<Directory1> {
new Directory1
{
Title = "missingTitle",
Year = "1978",
ParentTitle = "c"
},
new Directory1
{
Title = "Hi",
Year = "1978",
ParentTitle = "c"
},
new Directory1
{
Title = "Hi",
Year = "2017",
ParentTitle = "c"
},
new Directory1
{
Title = "missingTitle",
Year = "1992",
ParentTitle = "b"
},
new Directory1
{
Title = "title1",
Year = "2010",
ParentTitle = "DiZzY"
},
}
};
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
var requestMock = new Mock<IRequestService>();
var plexMock = new Mock<IPlexApi>();
settingsMock.Setup(x => x.GetSettings()).Returns(new PlexSettings { Ip = "192.168.1.1" });
authMock.Setup(x => x.GetSettings()).Returns(new AuthenticationSettings { PlexAuthToken = "abc" });
requestMock.Setup(x => x.GetAll()).Returns(requests);
plexMock.Setup(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>())).Returns(search);
Checker = new PlexAvailabilityChecker(settingsMock.Object, authMock.Object, requestMock.Object, plexMock.Object);
Checker.CheckAndUpdateAll(1);
requestMock.Verify(x => x.BatchUpdate(It.IsAny<List<RequestedModel>>()), Times.Once);
requestMock.Verify(x => x.Get(It.IsAny<int>()), Times.Never);
plexMock.Verify(x => x.SearchContent(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Uri>()), Times.Exactly(4));
}
} }
} }

View file

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

View file

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

View file

@ -29,6 +29,6 @@ namespace PlexRequests.Services.Interfaces
public interface IAvailabilityChecker public interface IAvailabilityChecker
{ {
void CheckAndUpdateAll(long check); void CheckAndUpdateAll(long check);
bool IsAvailable(string title, string year); bool IsAvailable(string title, string year, string artist, PlexType type);
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,14 +24,17 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Plex;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions; using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Store; using PlexRequests.Store;
@ -52,82 +55,201 @@ namespace PlexRequests.Services
private ISettingsService<AuthenticationSettings> Auth { get; } private ISettingsService<AuthenticationSettings> Auth { get; }
private IRequestService RequestService { get; } private IRequestService RequestService { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
private IPlexApi PlexApi { get; set; } private IPlexApi PlexApi { get; }
public void CheckAndUpdateAll(long check) public void CheckAndUpdateAll(long check)
{ {
Log.Trace("This is check no. {0}", check);
Log.Trace("Getting the settings");
var plexSettings = Plex.GetSettings(); var plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings(); var authSettings = Auth.GetSettings();
Log.Trace("Getting all the requests");
var requests = RequestService.GetAll(); var requests = RequestService.GetAll();
var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray();
if (!ValidateSettings(plexSettings, authSettings, requestedModels)) Log.Trace("Requests Count {0}", requestedModels.Length);
if (!ValidateSettings(plexSettings, authSettings) || !requestedModels.Any())
{ {
Log.Info("Validation of the settings failed or there is no requests.");
return; return;
} }
var modifiedModel = new List<RequestedModel>(); var modifiedModel = new List<RequestedModel>();
foreach (var r in requestedModels) foreach (var r in requestedModels)
{ {
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri); Log.Trace("We are going to see if Plex has the following title: {0}", r.Title);
var result = results.Video.FirstOrDefault(x => x.Title == r.Title); PlexSearch results;
var originalRequest = RequestService.Get(r.Id); try
{
originalRequest.Available = result != null; results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri);
modifiedModel.Add(originalRequest); }
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;
}
Log.Trace("The result from Plex where the title's match was null, so that means the content is not yet in Plex.");
}
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); RequestService.BatchUpdate(modifiedModel);
} }
}
/// <summary> /// <summary>
/// Determines whether the specified search term is available. /// Determines whether the specified title is available.
/// </summary> /// </summary>
/// <param name="title">The search term.</param> /// <param name="title">The title.</param>
/// <param name="year">The year.</param> /// <param name="year">The year.</param>
/// <param name="artist">The artist.</param>
/// <param name="type">The type.</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="ApplicationSettingsException">The settings are not configured for Plex or Authentication</exception> /// <exception cref="ApplicationSettingsException">The settings are not configured for Plex or Authentication</exception>
public bool IsAvailable(string title, string year) /// <exception cref="System.ArgumentOutOfRangeException">null</exception>
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 plexSettings = Plex.GetSettings();
var authSettings = Auth.GetSettings(); var authSettings = Auth.GetSettings();
if (!ValidateSettings(plexSettings, authSettings)) if (!ValidateSettings(plexSettings, authSettings))
{ {
Log.Warn("The settings are not configured");
throw new ApplicationSettingsException("The settings are not configured for Plex or Authentication"); throw new ApplicationSettingsException("The settings are not configured for Plex or Authentication");
} }
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
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);
}
}
/// <summary>
/// Searches the movies and TV shows on Plex.
/// </summary>
/// <param name="results">The results.</param>
/// <param name="title">The title.</param>
/// <param name="year">The year.</param>
/// <returns></returns>
private bool MovieTvSearch(PlexSearch results, string title, string year)
{
try
{
if (!string.IsNullOrEmpty(year)) if (!string.IsNullOrEmpty(year))
{ {
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri); var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase) && x.Year == year);
var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title) && x.Year == year);
var directoryTitle = results.Directory?.Title == title && results.Directory?.Year == year; var directoryResult = false;
return result?.Title != null || directoryTitle; 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 else
{ {
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri); var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase));
var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title)); var directoryResult = false;
var directoryTitle = results.Directory?.Title == title; if (results.Directory != null)
return result?.Title != null || directoryTitle;
}
}
private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth, IEnumerable<RequestedModel> requests)
{ {
if (plex.Ip == null || auth.PlexAuthToken == null || requests == null) if (results.Directory.Any(d => d.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase)))
{ {
Log.Warn("A setting is null, Ensure Plex is configured correctly, and we have a Plex Auth token."); directoryResult = true;
}
}
return result?.Title != null || directoryResult;
}
}
catch (Exception e)
{
Log.Error("Could not finish the Movie/TV check in Plex because of an exception:");
Log.Error(e);
return false; return false;
} }
if (!requests.Any())
{
Log.Info("We have no requests to check if they are available on Plex.");
return false;
} }
/// <summary>
/// Searches the music on Plex.
/// </summary>
/// <param name="results">The results.</param>
/// <param name="title">The title.</param>
/// <param name="artist">The artist.</param>
/// <returns></returns>
private bool AlbumSearch(PlexSearch results, string title, string artist)
{
try
{
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; return true;
} }
}
}
catch (Exception e)
{
Log.Error("Could not finish the Album check in Plex because of an exception:");
Log.Error(e);
}
return false;
}
private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth) private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth)
{ {

View file

@ -86,6 +86,7 @@
<Compile Include="Notification\PushoverNotification.cs" /> <Compile Include="Notification\PushoverNotification.cs" />
<Compile Include="Notification\PushbulletNotification.cs" /> <Compile Include="Notification\PushbulletNotification.cs" />
<Compile Include="PlexAvailabilityChecker.cs" /> <Compile Include="PlexAvailabilityChecker.cs" />
<Compile Include="PlexType.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UpdateInterval.cs" /> <Compile Include="UpdateInterval.cs" />
</ItemGroup> </ItemGroup>

View file

@ -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
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -44,6 +44,7 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using PlexRequests.UI.Modules; using PlexRequests.UI.Modules;
using PlexRequests.Helpers;
namespace PlexRequests.UI.Tests namespace PlexRequests.UI.Tests
{ {
@ -59,6 +60,7 @@ namespace PlexRequests.UI.Tests
private Mock<ISettingsService<EmailNotificationSettings>> EmailMock { get; set; } private Mock<ISettingsService<EmailNotificationSettings>> EmailMock { get; set; }
private Mock<ISettingsService<PushbulletNotificationSettings>> PushbulletSettings { get; set; } private Mock<ISettingsService<PushbulletNotificationSettings>> PushbulletSettings { get; set; }
private Mock<ISettingsService<PushoverNotificationSettings>> PushoverSettings { get; set; } private Mock<ISettingsService<PushoverNotificationSettings>> PushoverSettings { get; set; }
private Mock<ISettingsService<HeadphonesSettings>> HeadphonesSettings { get; set; }
private Mock<IPlexApi> PlexMock { get; set; } private Mock<IPlexApi> PlexMock { get; set; }
private Mock<ISonarrApi> SonarrApiMock { get; set; } private Mock<ISonarrApi> SonarrApiMock { get; set; }
private Mock<IPushbulletApi> PushbulletApi { get; set; } private Mock<IPushbulletApi> PushbulletApi { get; set; }
@ -66,6 +68,7 @@ namespace PlexRequests.UI.Tests
private Mock<ICouchPotatoApi> CpApi { get; set; } private Mock<ICouchPotatoApi> CpApi { get; set; }
private Mock<IRepository<LogEntity>> LogRepo { get; set; } private Mock<IRepository<LogEntity>> LogRepo { get; set; }
private Mock<INotificationService> NotificationService { get; set; } private Mock<INotificationService> NotificationService { get; set; }
private Mock<ICacheProvider> Cache { get; set; }
private ConfigurableBootstrapper Bootstrapper { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; }
@ -94,6 +97,8 @@ namespace PlexRequests.UI.Tests
PushoverSettings = new Mock<ISettingsService<PushoverNotificationSettings>>(); PushoverSettings = new Mock<ISettingsService<PushoverNotificationSettings>>();
PushoverApi = new Mock<IPushoverApi>(); PushoverApi = new Mock<IPushoverApi>();
NotificationService = new Mock<INotificationService>(); NotificationService = new Mock<INotificationService>();
HeadphonesSettings = new Mock<ISettingsService<HeadphonesSettings>>();
Cache = new Mock<ICacheProvider>();
Bootstrapper = new ConfigurableBootstrapper(with => Bootstrapper = new ConfigurableBootstrapper(with =>
{ {
@ -114,6 +119,8 @@ namespace PlexRequests.UI.Tests
with.Dependency(PushoverSettings.Object); with.Dependency(PushoverSettings.Object);
with.Dependency(PushoverApi.Object); with.Dependency(PushoverApi.Object);
with.Dependency(NotificationService.Object); with.Dependency(NotificationService.Object);
with.Dependency(HeadphonesSettings.Object);
with.Dependencies(Cache.Object);
with.RootPathProvider<TestRootPathProvider>(); with.RootPathProvider<TestRootPathProvider>();
with.RequestStartup((container, pipelines, context) => with.RequestStartup((container, pipelines, context) =>
{ {

View file

@ -113,6 +113,10 @@
<Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project> <Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project>
<Name>PlexRequests.Core</Name> <Name>PlexRequests.Core</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\PlexRequests.Helpers\PlexRequests.Helpers.csproj">
<Project>{1252336d-42a3-482a-804c-836e60173dfa}</Project>
<Name>PlexRequests.Helpers</Name>
</ProjectReference>
<ProjectReference Include="..\PlexRequests.Services\PlexRequests.Services.csproj"> <ProjectReference Include="..\PlexRequests.Services\PlexRequests.Services.csproj">
<Project>{566EFA49-68F8-4716-9693-A6B3F2624DEA}</Project> <Project>{566EFA49-68F8-4716-9693-A6B3F2624DEA}</Project>
<Name>PlexRequests.Services</Name> <Name>PlexRequests.Services</Name>

View file

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

View file

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

View file

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

View file

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

View file

@ -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

File diff suppressed because one or more lines are too long

10
PlexRequests.UI/Content/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,76 +1,155 @@
Handlebars.registerHelper('if_eq', function (a, b, opts) { Handlebars.registerHelper('if_eq', function (a, b, opts) {
if (a == b) if (a == b)
return opts.fn(this); return !opts ? null : opts.fn(this);
else else
return opts.inverse(this); return !opts ? null : opts.inverse(this);
}); });
var searchSource = $("#search-template").html(); var searchSource = $("#search-template").html();
var albumSource = $("#album-template").html();
var searchTemplate = Handlebars.compile(searchSource); var searchTemplate = Handlebars.compile(searchSource);
var albumTemplate = Handlebars.compile(albumSource);
var movieTimer = 0; var movieTimer = 0;
var tvimer = 0; var tvimer = 0;
movieLoad(); var mixItUpDefault = {
tvLoad(); animation: { enable: true },
load: {
filter: 'all',
sort: 'requestorder:desc'
},
layout: {
display: 'block'
},
callbacks: {
onMixStart: function (state, futureState) {
$('.mix', this).removeAttr('data-bound').removeData('bound'); // fix for animation issues in other tabs
}
}
};
initLoad();
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr('href'); var target = $(e.target).attr('href');
var activeState = ""; var activeState = "";
if (target === "#TvShowTab") {
if ($('#movieList').mixItUp('isLoaded')) {
activeState = $('#movieList').mixItUp('getState');
$('#movieList').mixItUp('destroy');
}
if (!$('#tvList').mixItUp('isLoaded')) {
$('#tvList').mixItUp({
load: {
filter: activeState.activeFilter || 'all',
sort: activeState.activeSort || 'default:asc'
},
layout: {
display: 'block'
}
}); var $ml = $('#movieList');
var $tvl = $('#tvList');
var $musicL = $('#musicList');
$('.approve-category').hide();
if (target === "#TvShowTab") {
$('#approveTVShows').show();
if ($ml.mixItUp('isLoaded')) {
activeState = $ml.mixItUp('getState');
$ml.mixItUp('destroy');
} }
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 (target === "#MoviesTab") {
if ($('#tvList').mixItUp('isLoaded')) { $('#approveMovies').show();
activeState = $('#tvList').mixItUp('getState'); if ($tvl.mixItUp('isLoaded')) {
$('#tvList').mixItUp('destroy'); activeState = $tvl.mixItUp('getState');
$tvl.mixItUp('destroy');
} }
if (!$('#movieList').mixItUp('isLoaded')) { if ($musicL.mixItUp('isLoaded')) {
$('#movieList').mixItUp({ activeState = $musicL.mixItUp('getState');
load: { $musicL.mixItUp('destroy');
filter: activeState.activeFilter || 'all',
sort: activeState.activeSort || 'default:asc'
},
layout: {
display: 'block'
} }
}); if ($ml.mixItUp('isLoaded')) $ml.mixItUp('destroy');
$ml.mixItUp(mixItUpConfig(activeState)); // init or reinit
} }
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 // Approve all
$('#approveAll').click(function () { $('#approveMovies').click(function (e) {
e.preventDefault();
var buttonId = e.target.id;
var origHtml = $(this).html();
if ($('#' + buttonId).text() === " Loading...") {
return;
}
loadingButton(buttonId, "success");
$.ajax({ $.ajax({
type: 'post', type: 'post',
url: '/approval/approveall', url: '/approval/approveallmovies',
dataType: "json", dataType: "json",
success: function (response) { success: function (response) {
if (checkJsonResponse(response)) { if (checkJsonResponse(response)) {
generateNotify("Success! All requests approved!", "success"); generateNotify("Success! All Movie requests approved!", "success");
movieLoad();
} }
}, },
error: function (e) { error: function (e) {
console.log(e); console.log(e);
generateNotify("Something went wrong!", "danger"); generateNotify("Something went wrong!", "danger");
},
complete: function (e) {
finishLoading(buttonId, "success", origHtml);
} }
}); });
}); });
$('#approveTVShows').click(function (e) {
e.preventDefault();
var buttonId = e.target.id;
var origHtml = $(this).html();
if ($('#' + buttonId).text() === " Loading...") {
return;
}
loadingButton(buttonId, "success");
$.ajax({
type: 'post',
url: '/approval/approvealltvshows',
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) {
generateNotify("Success! All TV Show requests approved!", "success");
tvLoad();
}
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
},
complete: function (e) {
finishLoading(buttonId, "success", origHtml);
}
});
});
// filtering/sorting
$('.filter,.sort', '.dropdown-menu').click(function (e) {
var $this = $(this);
$('.fa-check-square', $this.parents('.dropdown-menu:first')).removeClass('fa-check-square').addClass('fa-square-o');
$this.children('.fa').first().removeClass('fa-square-o').addClass('fa-check-square');
});
// Report Issue // Report Issue
@ -211,37 +290,44 @@ $(document).on("click", ".delete", function (e) {
// Approve single request // Approve single request
$(document).on("click", ".approve", function (e) { $(document).on("click", ".approve", function (e) {
e.preventDefault(); e.preventDefault();
var buttonId = e.target.id; var $this = $(this);
var $form = $('#approve' + buttonId); var $form = $this.parents('form').first();
if ($('#' + buttonId).text() === " Loading...") { if ($this.text() === " Loading...") {
return; return;
} }
loadingButton(buttonId, "success"); loadingButton($this.attr('id'), "success");
$.ajax({ approveRequest($form, null, function () {
type: $form.prop('method'), $("#" + $this.attr('id') + "notapproved").prop("class", "fa fa-check");
url: $form.prop('action'),
data: $form.serialize(),
dataType: "json",
success: function (response) {
if (checkJsonResponse(response)) { var $group = $this.parent('.btn-split');
if (response.message) { if ($group.length > 0) {
generateNotify(response.message, "success"); $group.remove();
} else { }
generateNotify("Success! Request Approved.", "success"); else {
$this.remove();
}
});
});
$(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(); loadingButton($button.attr('id'), "success");
$("#" + buttonId + "notapproved").prop("class", "fa fa-check");
} approveRequest($form, qualityId, function () {
}, $("#" + $button.attr('id') + "notapproved").prop("class", "fa fa-check");
error: function (e) { $this.parents('.btn-split').remove();
console.log(e);
generateNotify("Something went wrong!", "danger");
}
}); });
}); });
@ -315,36 +401,119 @@ $(document).on("click", ".change", function (e) {
}); });
function movieLoad() { function approveRequest($form, qualityId, successCallback) {
$("#movieList").html("");
$.ajax("/requests/movies/").success(function (results) { var formData = $form.serialize();
results.forEach(function (result) { if (qualityId) formData += ("&qualityId=" + qualityId);
var context = buildRequestContext(result, "movie");
var html = searchTemplate(context); $.ajax({
$("#movieList").append(html); type: $form.prop('method'),
}); url: $form.prop('action'),
$('#movieList').mixItUp({ data: formData,
layout: { dataType: "json",
display: 'block' success: function (response) {
if (checkJsonResponse(response)) {
if (response.message) {
generateNotify(response.message, "success");
} else {
generateNotify("Success! Request Approved.", "success");
}
if (successCallback) {
successCallback();
}
}
}, },
load: { error: function (e) {
filter: 'all' 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() {
var $ml = $('#movieList');
if ($ml.mixItUp('isLoaded')) {
activeState = $ml.mixItUp('getState');
$ml.mixItUp('destroy');
}
$ml.html("");
$.ajax("/requests/movies/").success(function (results) {
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() { function tvLoad() {
$("#tvList").html(""); var $tvl = $('#tvList');
if ($tvl.mixItUp('isLoaded')) {
activeState = $tvl.mixItUp('getState');
$tvl.mixItUp('destroy');
}
$tvl.html("");
$.ajax("/requests/tvshows/").success(function (results) { $.ajax("/requests/tvshows/").success(function (results) {
if (results.length > 0) {
results.forEach(function (result) { results.forEach(function (result) {
var context = buildRequestContext(result, "tv"); var context = buildRequestContext(result, "tv");
var html = searchTemplate(context); var html = searchTemplate(context);
$("#tvList").append(html); $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, year: result.releaseYear,
type: type, type: type,
status: result.status, status: result.status,
releaseDate: result.releaseDate, releaseDate: Humanize(result.releaseDate),
releaseDateTicks: result.releaseDateTicks,
approved: result.approved, approved: result.approved,
requestedBy: result.requestedBy, requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '',
requestedDate: result.requestedDate, requestedDate: Humanize(result.requestedDate),
requestedDateTicks: result.requestedDateTicks,
available: result.available, available: result.available,
admin: result.admin, admin: result.admin,
issues: result.issues, issues: result.issues,
@ -369,20 +540,13 @@ function buildRequestContext(result, type) {
requestId: result.id, requestId: result.id,
adminNote: result.adminNotes, adminNote: result.adminNotes,
imdb: result.imdbId, imdb: result.imdbId,
seriesRequested: result.tvSeriesRequestType seriesRequested: result.tvSeriesRequestType,
coverArtUrl: result.coverArtUrl,
qualities: result.qualities,
hasQualities: result.qualities && result.qualities.length > 0,
artist: result.artistName
}; };
return context; return context;
} }
function startFilter(elementId) {
$('#'+element).mixItUp({
load: {
filter: activeState.activeFilter || 'all',
sort: activeState.activeSort || 'default:asc'
},
layout: {
display: 'block'
}
});
}

View file

@ -5,28 +5,53 @@
return opts.inverse(this); return opts.inverse(this);
}); });
$(function () {
var searchSource = $("#search-template").html(); var searchSource = $("#search-template").html();
var musicSource = $("#music-template").html();
var searchTemplate = Handlebars.compile(searchSource); var searchTemplate = Handlebars.compile(searchSource);
var movieTimer = 0; var musicTemplate = Handlebars.compile(musicSource);
var tvimer = 0;
var searchTimer = 0;
// 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');
}
$('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 // Type in movie search
$("#movieSearchContent").on("input", function () { $("#movieSearchContent").on("input", function () {
if (movieTimer) { if (searchTimer) {
clearTimeout(movieTimer); clearTimeout(searchTimer);
} }
$('#movieSearchButton').attr("class", "fa fa-spinner fa-spin"); $('#movieSearchButton').attr("class", "fa fa-spinner fa-spin");
movieTimer = setTimeout(movieSearch, 400); searchTimer = setTimeout(movieSearch, 400);
}); });
$('#moviesComingSoon').on('click', function (e) {
e.preventDefault();
moviesComingSoon();
});
$('#moviesInTheaters').on('click', function (e) {
e.preventDefault();
moviesInTheaters();
});
// Type in TV search // Type in TV search
$("#tvSearchContent").on("input", function () { $("#tvSearchContent").on("input", function () {
if (tvimer) { if (searchTimer) {
clearTimeout(tvimer); clearTimeout(searchTimer);
} }
$('#tvSearchButton').attr("class", "fa fa-spinner fa-spin"); $('#tvSearchButton').attr("class", "fa fa-spinner fa-spin");
tvimer = setTimeout(tvSearch, 400); searchTimer = setTimeout(tvSearch, 400);
}); });
// Click TV dropdown option // Click TV dropdown option
@ -60,6 +85,16 @@ $(document).on("click", ".dropdownTv", function (e) {
sendRequestAjax(data, type, url, buttonId); sendRequestAjax(data, type, url, buttonId);
}); });
// Search Music
$("#musicSearchContent").on("input", function () {
if (searchTimer) {
clearTimeout(searchTimer);
}
$('#musicSearchButton').attr("class", "fa fa-spinner fa-spin");
searchTimer = setTimeout(musicSearch, 400);
});
// Click Request for movie // Click Request for movie
$(document).on("click", ".requestMovie", function (e) { $(document).on("click", ".requestMovie", function (e) {
e.preventDefault(); e.preventDefault();
@ -82,6 +117,33 @@ $(document).on("click", ".requestMovie", function (e) {
}); });
// Click Request for album
$(document).on("click", ".requestAlbum", function (e) {
e.preventDefault();
var buttonId = e.target.id;
if ($("#" + buttonId).attr('disabled')) {
return;
}
$("#" + buttonId).prop("disabled", true);
loadingButton(buttonId, "primary");
var $form = $('#form' + buttonId);
var type = $form.prop('method');
var url = $form.prop('action');
var data = $form.serialize();
sendRequestAjax(data, type, url, buttonId);
});
function focusSearch($content) {
if ($content.length > 0) {
$('input[type=text].form-control', $content).first().focus();
}
}
function sendRequestAjax(data, type, url, buttonId) { function sendRequestAjax(data, type, url, buttonId) {
$.ajax({ $.ajax({
type: type, type: type,
@ -91,7 +153,7 @@ function sendRequestAjax(data, type, url, buttonId) {
success: function (response) { success: function (response) {
console.log(response); console.log(response);
if (response.result === true) { if (response.result === true) {
generateNotify("Success!", "success"); generateNotify(response.message || "Success!", "success");
$('#' + buttonId).html("<i class='fa fa-check'></i> Requested"); $('#' + buttonId).html("<i class='fa fa-check'></i> Requested");
$('#' + buttonId).removeClass("btn-primary-outline"); $('#' + buttonId).removeClass("btn-primary-outline");
@ -112,10 +174,23 @@ function sendRequestAjax(data, type, url, buttonId) {
} }
function movieSearch() { function movieSearch() {
$("#movieList").html("");
var query = $("#movieSearchContent").val(); var query = $("#movieSearchContent").val();
getMovies("/search/movie/" + query);
}
$.ajax("/search/movie/" + query).success(function (results) { function moviesComingSoon() {
getMovies("/search/movie/upcoming");
}
function moviesInTheaters() {
getMovies("/search/movie/playing");
}
function getMovies(url) {
$("#movieList").html("");
$.ajax(url).success(function (results) {
if (results.length > 0) { if (results.length > 0) {
results.forEach(function (result) { results.forEach(function (result) {
var context = buildMovieContext(result); var context = buildMovieContext(result);
@ -124,15 +199,22 @@ function movieSearch() {
$("#movieList").append(html); $("#movieList").append(html);
}); });
} }
else {
$("#movieList").html(noResultsHtml);
}
$('#movieSearchButton').attr("class", "fa fa-search"); $('#movieSearchButton').attr("class", "fa fa-search");
}); });
}; };
function tvSearch() { function tvSearch() {
$("#tvList").html("");
var query = $("#tvSearchContent").val(); var query = $("#tvSearchContent").val();
getTvShows("/search/tv/" + query);
}
$.ajax("/search/tv/" + query).success(function (results) { function getTvShows(url) {
$("#tvList").html("");
$.ajax(url).success(function (results) {
if (results.length > 0) { if (results.length > 0) {
results.forEach(function (result) { results.forEach(function (result) {
var context = buildTvShowContext(result); var context = buildTvShowContext(result);
@ -140,10 +222,45 @@ function tvSearch() {
$("#tvList").append(html); $("#tvList").append(html);
}); });
} }
else {
$("#tvList").html(noResultsHtml);
}
$('#tvSearchButton').attr("class", "fa fa-search"); $('#tvSearchButton').attr("class", "fa fa-search");
}); });
}; };
function musicSearch() {
var query = $("#musicSearchContent").val();
getMusic("/search/music/" + query);
}
function getMusic(url) {
$("#musicList").html("");
$.ajax(url).success(function (results) {
if (results.length > 0) {
results.forEach(function (result) {
var context = buildMusicContext(result);
var html = musicTemplate(context);
$("#musicList").append(html);
getCoverArt(context.id);
});
}
else {
$("#musicList").html(noResultsMusic);
}
$('#musicSearchButton').attr("class", "fa fa-search");
});
};
function getCoverArt(artistId) {
$.ajax("/search/music/coverart/" + artistId).success(function (result) {
if (result) {
$('#' + artistId + "imageDiv").html(" <img class='img-responsive' src='" + result + "' width='150' alt='poster'>");
}
});
};
function buildMovieContext(result) { function buildMovieContext(result) {
var date = new Date(result.releaseDate); var date = new Date(result.releaseDate);
@ -177,3 +294,23 @@ function buildTvShowContext(result) {
}; };
return context; return context;
} }
function buildMusicContext(result) {
var context = {
id: result.id,
title: result.title,
overview: result.overview,
year: result.releaseDate,
type: "album",
trackCount: result.trackCount,
coverArtUrl: result.coverArtUrl,
artist: result.artist,
releaseType: result.releaseType,
country: result.country
};
return context;
}
});

View file

@ -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 // type = danger, warning, info, successs
$.notify({ $.notify({
// options // options
@ -25,13 +41,28 @@ function checkJsonResponse(response) {
} }
function loadingButton(elementId, originalCss) { function loadingButton(elementId, originalCss) {
$('#' + elementId).removeClass("btn-" + originalCss + "-outline"); var $element = $('#' + elementId);
$('#' + elementId).addClass("btn-primary-outline"); $element.removeClass("btn-" + originalCss + "-outline").addClass("btn-primary-outline").addClass('disabled').html("<i class='fa fa-spinner fa-spin'></i> Loading...");
$('#' + elementId).html("<i class='fa fa-spinner fa-spin'></i> 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) { function finishLoading(elementId, originalCss, html) {
$('#' + elementId).removeClass("btn-primary-outline"); var $element = $('#' + elementId);
$('#' + elementId).addClass("btn-" + originalCss + "-outline"); $element.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline").html(html);
$('#' + elementId).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 = "<div class='no-search-results'>" +
"<i class='fa fa-film no-search-results-icon'></i><div class='no-search-results-text'>Sorry, we didn't find any results!</div></div>";
var noResultsMusic = "<div class='no-search-results'>" +
"<i class='fa fa-headphones no-search-results-icon'></i><div class='no-search-results-text'>Sorry, we didn't find any results!</div></div>";

View file

@ -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<bool> 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<bool> 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<bool> 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;
}
}
}

View file

@ -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 ");
}
}
}

View file

@ -24,17 +24,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using Nancy;
using NLog; using NLog;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.SickRage; using PlexRequests.Api.Models.SickRage;
using PlexRequests.Api.Models.Sonarr; using PlexRequests.Api.Models.Sonarr;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Models; using System.Linq;
namespace PlexRequests.UI.Helpers namespace PlexRequests.UI.Helpers
{ {
@ -50,9 +47,19 @@ namespace PlexRequests.UI.Helpers
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model) 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 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); int.TryParse(sonarrSettings.QualityProfile, out qualityProfile);
}
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey, sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
sonarrSettings.FullUri); sonarrSettings.FullUri);
@ -65,13 +72,24 @@ namespace PlexRequests.UI.Helpers
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) 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); sickRageSettings.ApiKey, sickRageSettings.FullUri);
var result = apiResult.Result;
Log.Trace("SickRage Add Result: "); Log.Trace("SickRage Add Result: ");
Log.Trace(result.DumpJson()); Log.Trace(result.DumpJson());
return result.Result; return result;
} }
} }
} }

View file

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

View file

@ -0,0 +1,8 @@
namespace PlexRequests.UI.Models
{
public class QualityModel
{
public string Id { get; set; }
public string Name { get; set; }
}
}

View file

@ -25,6 +25,7 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using PlexRequests.Store; using PlexRequests.Store;
using System;
namespace PlexRequests.UI.Models namespace PlexRequests.UI.Models
{ {
@ -36,12 +37,14 @@ namespace PlexRequests.UI.Models
public string Overview { get; set; } public string Overview { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string PosterPath { get; set; } public string PosterPath { get; set; }
public string ReleaseDate { get; set; } public DateTime ReleaseDate { get; set; }
public long ReleaseDateTicks { get; set; }
public RequestType Type { get; set; } public RequestType Type { get; set; }
public string Status { get; set; } public string Status { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
public string RequestedBy { get; set; } public string[] RequestedUsers { get; set; }
public string RequestedDate { get; set; } public DateTime RequestedDate { get; set; }
public long RequestedDateTicks { get; set; }
public string ReleaseYear { get; set; } public string ReleaseYear { get; set; }
public bool Available { get; set; } public bool Available { get; set; }
public bool Admin { get; set; } public bool Admin { get; set; }
@ -49,5 +52,8 @@ namespace PlexRequests.UI.Models
public string OtherMessage { get; set; } public string OtherMessage { get; set; }
public string AdminNotes { get; set; } public string AdminNotes { get; set; }
public string TvSeriesRequestType { get; set; } public string TvSeriesRequestType { get; set; }
public string MusicBrainzId { get; set; }
public QualityModel[] Qualities { get; set; }
public string ArtistName { get; set; }
} }
} }

View file

@ -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; }
}
}

View file

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

View file

@ -28,14 +28,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.Linq; using System.Linq;
using Humanizer;
using MarkdownSharp; using MarkdownSharp;
using Nancy; using Nancy;
using Nancy.Extensions; using Nancy.Extensions;
using Nancy.ModelBinding; using Nancy.ModelBinding;
using Nancy.Responses.Negotiation; using Nancy.Responses.Negotiation;
using Nancy.Security;
using Nancy.Validation; using Nancy.Validation;
using NLog; using NLog;
@ -51,12 +49,16 @@ using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using System;
using Nancy.Json;
using Nancy.Security;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class AdminModule : NancyModule public class AdminModule : NancyModule
{ {
private ISettingsService<PlexRequestSettings> RpService { get; } private ISettingsService<PlexRequestSettings> PrService { get; }
private ISettingsService<CouchPotatoSettings> CpService { get; } private ISettingsService<CouchPotatoSettings> CpService { get; }
private ISettingsService<AuthenticationSettings> AuthService { get; } private ISettingsService<AuthenticationSettings> AuthService { get; }
private ISettingsService<PlexSettings> PlexService { get; } private ISettingsService<PlexSettings> PlexService { get; }
@ -65,6 +67,7 @@ namespace PlexRequests.UI.Modules
private ISettingsService<EmailNotificationSettings> EmailService { get; } private ISettingsService<EmailNotificationSettings> EmailService { get; }
private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; } private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; }
private ISettingsService<PushoverNotificationSettings> PushoverService { get; } private ISettingsService<PushoverNotificationSettings> PushoverService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISonarrApi SonarrApi { get; } private ISonarrApi SonarrApi { get; }
private IPushbulletApi PushbulletApi { get; } private IPushbulletApi PushbulletApi { get; }
@ -72,9 +75,10 @@ namespace PlexRequests.UI.Modules
private ICouchPotatoApi CpApi { get; } private ICouchPotatoApi CpApi { get; }
private IRepository<LogEntity> LogsRepo { get; } private IRepository<LogEntity> LogsRepo { get; }
private INotificationService NotificationService { get; } private INotificationService NotificationService { get; }
private ICacheProvider Cache { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
public AdminModule(ISettingsService<PlexRequestSettings> rpService, public AdminModule(ISettingsService<PlexRequestSettings> prService,
ISettingsService<CouchPotatoSettings> cpService, ISettingsService<CouchPotatoSettings> cpService,
ISettingsService<AuthenticationSettings> auth, ISettingsService<AuthenticationSettings> auth,
ISettingsService<PlexSettings> plex, ISettingsService<PlexSettings> plex,
@ -89,9 +93,11 @@ namespace PlexRequests.UI.Modules
ISettingsService<PushoverNotificationSettings> pushoverSettings, ISettingsService<PushoverNotificationSettings> pushoverSettings,
IPushoverApi pushoverApi, IPushoverApi pushoverApi,
IRepository<LogEntity> logsRepo, IRepository<LogEntity> logsRepo,
INotificationService notify) : base("admin") INotificationService notify,
ISettingsService<HeadphonesSettings> headphones,
ICacheProvider cache) : base("admin")
{ {
RpService = rpService; PrService = prService;
CpService = cpService; CpService = cpService;
AuthService = auth; AuthService = auth;
PlexService = plex; PlexService = plex;
@ -107,6 +113,8 @@ namespace PlexRequests.UI.Modules
PushoverService = pushoverSettings; PushoverService = pushoverSettings;
PushoverApi = pushoverApi; PushoverApi = pushoverApi;
NotificationService = notify; NotificationService = notify;
HeadphonesService = headphones;
Cache = cache;
#if !DEBUG #if !DEBUG
this.RequiresAuthentication(); this.RequiresAuthentication();
@ -139,18 +147,24 @@ namespace PlexRequests.UI.Modules
Get["/emailnotification"] = _ => EmailNotifications(); Get["/emailnotification"] = _ => EmailNotifications();
Post["/emailnotification"] = _ => SaveEmailNotifications(); Post["/emailnotification"] = _ => SaveEmailNotifications();
Post["/testemailnotification"] = _ => TestEmailNotifications();
Get["/status"] = _ => Status(); Get["/status"] = _ => Status();
Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Get["/pushbulletnotification"] = _ => PushbulletNotifications();
Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications();
Post["/testpushbulletnotification"] = _ => TestPushbulletNotifications();
Get["/pushovernotification"] = _ => PushoverNotifications(); Get["/pushovernotification"] = _ => PushoverNotifications();
Post["/pushovernotification"] = _ => SavePushoverNotifications(); Post["/pushovernotification"] = _ => SavePushoverNotifications();
Post["/testpushovernotification"] = _ => TestPushoverNotifications();
Get["/logs"] = _ => Logs(); Get["/logs"] = _ => Logs();
Get["/loglevel"] = _ => GetLogLevels(); Get["/loglevel"] = _ => GetLogLevels();
Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level); Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level);
Get["/loadlogs"] = _ => LoadLogs(); Get["/loadlogs"] = _ => LoadLogs();
Get["/headphones"] = _ => Headphones();
Post["/headphones"] = _ => SaveHeadphones();
} }
private Negotiator Authentication() private Negotiator Authentication()
@ -174,7 +188,7 @@ namespace PlexRequests.UI.Modules
private Negotiator Admin() private Negotiator Admin()
{ {
var settings = RpService.GetSettings(); var settings = PrService.GetSettings();
Log.Trace("Getting Settings:"); Log.Trace("Getting Settings:");
Log.Trace(settings.DumpJson()); Log.Trace(settings.DumpJson());
@ -185,7 +199,7 @@ namespace PlexRequests.UI.Modules
{ {
var model = this.Bind<PlexRequestSettings>(); var model = this.Bind<PlexRequestSettings>();
RpService.SaveSettings(model); PrService.SaveSettings(model);
return Context.GetRedirect("~/admin"); return Context.GetRedirect("~/admin");
@ -362,6 +376,12 @@ namespace PlexRequests.UI.Modules
var settings = this.Bind<SonarrSettings>(); var settings = this.Bind<SonarrSettings>();
var profiles = SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri); var profiles = SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
// set the cache
if (profiles != null)
{
Cache.Set(CacheKeys.SonarrQualityProfiles, profiles);
}
return Response.AsJson(profiles); return Response.AsJson(profiles);
} }
@ -372,6 +392,37 @@ namespace PlexRequests.UI.Modules
return View["EmailNotifications", settings]; return View["EmailNotifications", settings];
} }
private Response TestEmailNotifications()
{
var settings = this.Bind<EmailNotificationSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
try
{
NotificationService.Subscribe(new EmailMessageNotification(EmailService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
Log.Info("Sent email notification test");
}
catch (Exception)
{
Log.Error("Failed to subscribe and publish test Email Notification");
}
finally
{
NotificationService.UnSubscribe(new EmailMessageNotification(EmailService));
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Email Notification!" });
}
private Response SaveEmailNotifications() private Response SaveEmailNotifications()
{ {
var settings = this.Bind<EmailNotificationSettings>(); var settings = this.Bind<EmailNotificationSettings>();
@ -440,6 +491,37 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
} }
private Response TestPushbulletNotifications()
{
var settings = this.Bind<PushbulletNotificationSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
try
{
NotificationService.Subscribe(new PushbulletNotification(PushbulletApi, PushbulletService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
Log.Info("Sent pushbullet notification test");
}
catch (Exception)
{
Log.Error("Failed to subscribe and publish test Pushbullet Notification");
}
finally
{
NotificationService.UnSubscribe(new PushbulletNotification(PushbulletApi, PushbulletService));
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Pushbullet Notification!" });
}
private Negotiator PushoverNotifications() private Negotiator PushoverNotifications()
{ {
var settings = PushoverService.GetSettings(); var settings = PushoverService.GetSettings();
@ -472,11 +554,48 @@ namespace PlexRequests.UI.Modules
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
} }
private Response TestPushoverNotifications()
{
var settings = this.Bind<PushoverNotificationSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
return Response.AsJson(valid.SendJsonError());
}
var notificationModel = new NotificationModel
{
NotificationType = NotificationType.Test,
DateTime = DateTime.Now
};
try
{
NotificationService.Subscribe(new PushoverNotification(PushoverApi, PushoverService));
settings.Enabled = true;
NotificationService.Publish(notificationModel, settings);
Log.Info("Sent pushover notification test");
}
catch (Exception)
{
Log.Error("Failed to subscribe and publish test Pushover Notification");
}
finally
{
NotificationService.UnSubscribe(new PushoverNotification(PushoverApi, PushoverService));
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Pushover Notification!" });
}
private Response GetCpProfiles() private Response GetCpProfiles()
{ {
var settings = this.Bind<CouchPotatoSettings>(); var settings = this.Bind<CouchPotatoSettings>();
var profiles = CpApi.GetProfiles(settings.FullUri, settings.ApiKey); var profiles = CpApi.GetProfiles(settings.FullUri, settings.ApiKey);
// set the cache
if (profiles != null)
{
Cache.Set(CacheKeys.CouchPotatoQualityProfiles, profiles);
}
return Response.AsJson(profiles); return Response.AsJson(profiles);
} }
@ -487,7 +606,8 @@ namespace PlexRequests.UI.Modules
private Response LoadLogs() 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<LogEntity> {Data = new List<LogEntity>()}; var model = new DatatablesModel<LogEntity> {Data = new List<LogEntity>()};
foreach (var l in allLogs) foreach (var l in allLogs)
{ {
@ -509,5 +629,32 @@ namespace PlexRequests.UI.Modules
LoggingHelper.ReconfigureLogLevel(newLevel); LoggingHelper.ReconfigureLogLevel(newLevel);
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"}); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"});
} }
private Negotiator Headphones()
{
var settings = HeadphonesService.GetSettings();
return View["Headphones", settings];
}
private Response SaveHeadphones()
{
var settings = this.Bind<HeadphonesSettings>();
var valid = this.Validate(settings);
if (!valid.IsValid)
{
var error = valid.SendJsonError();
Log.Info("Error validating Headphones settings, message: {0}", error.Message);
return Response.AsJson(error);
}
Log.Trace(settings.DumpJson());
var result = HeadphonesService.SaveSettings(settings);
Log.Info("Saved headphones settings, result: {0}", result);
return Response.AsJson(result
? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Headphones!" }
: new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." });
}
} }
} }

View file

@ -43,7 +43,7 @@ namespace PlexRequests.UI.Modules
{ {
public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi,
ISettingsService<AuthenticationSettings> authSettings, ISickRageApi srApi) : base("test") ISettingsService<AuthenticationSettings> authSettings, ISickRageApi srApi, IHeadphonesApi hpApi) : base("test")
{ {
this.RequiresAuthentication(); this.RequiresAuthentication();
@ -52,11 +52,13 @@ namespace PlexRequests.UI.Modules
PlexApi = plexApi; PlexApi = plexApi;
AuthSettings = authSettings; AuthSettings = authSettings;
SickRageApi = srApi; SickRageApi = srApi;
HeadphonesApi = hpApi;
Post["/cp"] = _ => CouchPotatoTest(); Post["/cp"] = _ => CouchPotatoTest();
Post["/sonarr"] = _ => SonarrTest(); Post["/sonarr"] = _ => SonarrTest();
Post["/plex"] = _ => PlexTest(); Post["/plex"] = _ => PlexTest();
Post["/sickrage"] = _ => SickRageTest(); Post["/sickrage"] = _ => SickRageTest();
Post["/headphones"] = _ => HeadphonesTest();
} }
@ -65,6 +67,7 @@ namespace PlexRequests.UI.Modules
private ICouchPotatoApi CpApi { get; } private ICouchPotatoApi CpApi { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISickRageApi SickRageApi { get; } private ISickRageApi SickRageApi { get; }
private IHeadphonesApi HeadphonesApi { get; }
private ISettingsService<AuthenticationSettings> AuthSettings { get; } private ISettingsService<AuthenticationSettings> AuthSettings { get; }
private Response CouchPotatoTest() private Response CouchPotatoTest()
@ -168,5 +171,35 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); return Response.AsJson(new JsonResponseModel { Result = false, Message = message });
} }
} }
private Response HeadphonesTest()
{
var settings = this.Bind<HeadphonesSettings>();
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. <strong>Exception Message:</strong> {e.Message}";
if (e.InnerException != null)
{
message = $"Could not connect to Headphones, please check your settings. <strong>Exception Message:</strong> {e.InnerException.Message}";
}
return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); ;
}
}
} }
} }

View file

@ -47,7 +47,8 @@ namespace PlexRequests.UI.Modules
{ {
public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi,
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings) : base("approval") ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi) : base("approval")
{ {
this.RequiresAuthentication(); this.RequiresAuthentication();
@ -58,9 +59,13 @@ namespace PlexRequests.UI.Modules
SonarrSettings = sonarrSettings; SonarrSettings = sonarrSettings;
SickRageApi = srApi; SickRageApi = srApi;
SickRageSettings = srSettings; SickRageSettings = srSettings;
HeadphonesSettings = hpSettings;
HeadphoneApi = hpApi;
Post["/approve"] = parameters => Approve((int)Request.Form.requestid); Post["/approve"] = parameters => Approve((int)Request.Form.requestid, (string)Request.Form.qualityId);
Post["/approveall"] = x => ApproveAll(); Post["/approveall"] = x => ApproveAll();
Post["/approveallmovies"] = x => ApproveAllMovies();
Post["/approvealltvshows"] = x => ApproveAllTVShows();
} }
private IRequestService Service { get; } private IRequestService Service { get; }
@ -69,16 +74,18 @@ namespace PlexRequests.UI.Modules
private ISettingsService<SonarrSettings> SonarrSettings { get; } private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; } private ISettingsService<SickRageSettings> SickRageSettings { get; }
private ISettingsService<CouchPotatoSettings> CpService { get; } private ISettingsService<CouchPotatoSettings> CpService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesSettings { get; }
private ISonarrApi SonarrApi { get; } private ISonarrApi SonarrApi { get; }
private ISickRageApi SickRageApi { get; } private ISickRageApi SickRageApi { get; }
private ICouchPotatoApi CpApi { get; } private ICouchPotatoApi CpApi { get; }
private IHeadphonesApi HeadphoneApi { get; }
/// <summary> /// <summary>
/// Approves the specified request identifier. /// Approves the specified request identifier.
/// </summary> /// </summary>
/// <param name="requestId">The request identifier.</param> /// <param name="requestId">The request identifier.</param>
/// <returns></returns> /// <returns></returns>
private Response Approve(int requestId) private Response Approve(int requestId, string qualityId)
{ {
Log.Info("approving request {0}", requestId); Log.Info("approving request {0}", requestId);
if (!Context.CurrentUser.IsAuthenticated()) if (!Context.CurrentUser.IsAuthenticated())
@ -97,15 +104,17 @@ namespace PlexRequests.UI.Modules
switch (request.Type) switch (request.Type)
{ {
case RequestType.Movie: case RequestType.Movie:
return RequestMovieAndUpdateStatus(request); return RequestMovieAndUpdateStatus(request, qualityId);
case RequestType.TvShow: case RequestType.TvShow:
return RequestTvAndUpdateStatus(request); return RequestTvAndUpdateStatus(request, qualityId);
case RequestType.Album:
return RequestAlbumAndUpdateStatus(request);
default: default:
throw new ArgumentOutOfRangeException(nameof(request)); throw new ArgumentOutOfRangeException(nameof(request));
} }
} }
private Response RequestTvAndUpdateStatus(RequestedModel request) private Response RequestTvAndUpdateStatus(RequestedModel request, string qualityId)
{ {
var sender = new TvSender(SonarrApi, SickRageApi); var sender = new TvSender(SonarrApi, SickRageApi);
@ -113,7 +122,7 @@ namespace PlexRequests.UI.Modules
if (sonarrSettings.Enabled) if (sonarrSettings.Enabled)
{ {
Log.Trace("Sending to Sonarr"); Log.Trace("Sending to Sonarr");
var result = sender.SendToSonarr(sonarrSettings, request); var result = sender.SendToSonarr(sonarrSettings, request, qualityId);
Log.Trace("Sonarr Result: "); Log.Trace("Sonarr Result: ");
Log.Trace(result.DumpJson()); Log.Trace(result.DumpJson());
if (!string.IsNullOrEmpty(result.title)) if (!string.IsNullOrEmpty(result.title))
@ -131,7 +140,7 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel return Response.AsJson(new JsonResponseModel
{ {
Result = false, Result = false,
Message = "Could not add the series to Sonarr" Message = result.ErrorMessage ?? "Could not add the series to Sonarr"
}); });
} }
@ -139,7 +148,7 @@ namespace PlexRequests.UI.Modules
if (srSettings.Enabled) if (srSettings.Enabled)
{ {
Log.Trace("Sending to SickRage"); Log.Trace("Sending to SickRage");
var result = sender.SendToSickRage(srSettings, request); var result = sender.SendToSickRage(srSettings, request, qualityId);
Log.Trace("SickRage Result: "); Log.Trace("SickRage Result: ");
Log.Trace(result.DumpJson()); Log.Trace(result.DumpJson());
if (result?.result == "success") if (result?.result == "success")
@ -167,7 +176,7 @@ namespace PlexRequests.UI.Modules
}); });
} }
private Response RequestMovieAndUpdateStatus(RequestedModel request) private Response RequestMovieAndUpdateStatus(RequestedModel request, string qualityId)
{ {
var cpSettings = CpService.GetSettings(); var cpSettings = CpService.GetSettings();
var cp = new CouchPotatoApi(); var cp = new CouchPotatoApi();
@ -188,7 +197,8 @@ namespace PlexRequests.UI.Modules
Message = "We could not approve this request. Please try again or check the logs." 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); Log.Trace("Adding movie to CP result {0}", result);
if (result) if (result)
{ {
@ -216,6 +226,84 @@ namespace PlexRequests.UI.Modules
}); });
} }
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!" });
}
}
/// <summary> /// <summary>
/// Approves all. /// Approves all.
/// </summary> /// </summary>
@ -227,23 +315,35 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." });
} }
var requests = Service.GetAll().Where(x => x.Approved == false); var requests = Service.GetAll().Where(x => x.CanApprove);
var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
if (!requestedModels.Any()) if (!requestedModels.Any())
{ {
return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no requests to approve. Please refresh." }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no requests to approve. Please refresh." });
} }
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 cpSettings = CpService.GetSettings();
var updatedRequests = new List<RequestedModel>(); var updatedRequests = new List<RequestedModel>();
foreach (var r in requestedModels) foreach (var r in requestedModels)
{ {
if (r.Type == RequestType.Movie) if (r.Type == RequestType.Movie)
{ {
var result = SendMovie(cpSettings, r, CpApi); var res = SendMovie(cpSettings, r, CpApi);
if (result) if (res)
{ {
r.Approved = true; r.Approved = true;
updatedRequests.Add(r); updatedRequests.Add(r);
@ -260,8 +360,8 @@ namespace PlexRequests.UI.Modules
var sonarr = SonarrSettings.GetSettings(); var sonarr = SonarrSettings.GetSettings();
if (sr.Enabled) if (sr.Enabled)
{ {
var result = sender.SendToSickRage(sr, r); var res = sender.SendToSickRage(sr, r);
if (result?.result == "success") if (res?.result == "success")
{ {
r.Approved = true; r.Approved = true;
updatedRequests.Add(r); updatedRequests.Add(r);
@ -269,14 +369,14 @@ namespace PlexRequests.UI.Modules
else else
{ {
Log.Error("Could not approve and send the TV {0} to SickRage!", r.Title); Log.Error("Could not approve and send the TV {0} to SickRage!", r.Title);
Log.Error("SickRage Message: {0}", result?.message); Log.Error("SickRage Message: {0}", res?.message);
} }
} }
if (sonarr.Enabled) if (sonarr.Enabled)
{ {
var result = sender.SendToSonarr(sonarr, r); var res = sender.SendToSonarr(sonarr, r);
if (result != null) if (!string.IsNullOrEmpty(res?.title))
{ {
r.Approved = true; r.Approved = true;
updatedRequests.Add(r); updatedRequests.Add(r);
@ -284,6 +384,7 @@ namespace PlexRequests.UI.Modules
else else
{ {
Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title); Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title);
Log.Error("Error message: {0}", res?.ErrorMessage);
} }
} }
} }
@ -291,17 +392,16 @@ namespace PlexRequests.UI.Modules
try try
{ {
var result = Service.BatchUpdate(updatedRequests); return Response.AsJson(result var result = Service.BatchUpdate(updatedRequests);
return Response.AsJson(result
? new JsonResponseModel { Result = true } ? new JsonResponseModel { Result = true }
: new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." }); : new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." });
} }
catch (Exception e) catch (Exception e)
{ {
Log.Fatal(e); Log.Fatal(e);
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" }); return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
} }
} }
private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp) private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp)

View file

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

View file

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

View file

@ -28,8 +28,6 @@
using System; using System;
using System.Linq; using System.Linq;
using Humanizer;
using Nancy; using Nancy;
using Nancy.Responses.Negotiation; using Nancy.Responses.Negotiation;
using Nancy.Security; using Nancy.Security;
@ -40,22 +38,45 @@ using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
using System.Collections.Generic;
using PlexRequests.Api.Interfaces;
using System.Threading.Tasks;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class RequestsModule : BaseModule public class RequestsModule : BaseModule
{ {
public RequestsModule(
public RequestsModule(IRequestService service, ISettingsService<PlexRequestSettings> prSettings, ISettingsService<PlexSettings> plex, INotificationService notify) : base("requests") IRequestService service,
ISettingsService<PlexRequestSettings> prSettings,
ISettingsService<PlexSettings> plex,
INotificationService notify,
ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<SickRageSettings> sickRageSettings,
ISettingsService<CouchPotatoSettings> cpSettings,
ICouchPotatoApi cpApi,
ISonarrApi sonarrApi,
ISickRageApi sickRageApi,
ICacheProvider cache) : base("requests")
{ {
Service = service; Service = service;
PrSettings = prSettings; PrSettings = prSettings;
PlexSettings = plex; PlexSettings = plex;
NotificationService = notify; NotificationService = notify;
SonarrSettings = sonarrSettings;
SickRageSettings = sickRageSettings;
CpSettings = cpSettings;
SonarrApi = sonarrApi;
SickRageApi = sickRageApi;
CpApi = cpApi;
Cache = cache;
Get["/"] = _ => LoadRequests(); Get["/"] = _ => LoadRequests();
Get["/movies"] = _ => GetMovies(); Get["/movies"] = _ => GetMovies();
Get["/tvshows"] = _ => GetTvShows(); Get["/tvshows"] = _ => GetTvShows();
Get["/albums"] = _ => GetAlbumRequests();
Post["/delete"] = _ => DeleteRequest((int)Request.Form.id); Post["/delete"] = _ => DeleteRequest((int)Request.Form.id);
Post["/reportissue"] = _ => ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null); Post["/reportissue"] = _ => ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null);
Post["/reportissuecomment"] = _ => ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea); Post["/reportissuecomment"] = _ => ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea);
@ -70,6 +91,13 @@ namespace PlexRequests.UI.Modules
private INotificationService NotificationService { get; } private INotificationService NotificationService { get; }
private ISettingsService<PlexRequestSettings> PrSettings { get; } private ISettingsService<PlexRequestSettings> PrSettings { get; }
private ISettingsService<PlexSettings> PlexSettings { get; } private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
private ISettingsService<CouchPotatoSettings> CpSettings { get; }
private ISonarrApi SonarrApi { get; }
private ISickRageApi SickRageApi { get; }
private ICouchPotatoApi CpApi { get; }
private ICacheProvider Cache { get; }
private Negotiator LoadRequests() private Negotiator LoadRequests()
{ {
@ -77,11 +105,54 @@ namespace PlexRequests.UI.Modules
return View["Index", settings]; 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(); var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbMovies = Service.GetAll().Where(x => x.Type == RequestType.Movie);
var viewModel = dbMovies.Select(movie => new RequestViewModel List<Task> taskList = new List<Task>();
List<RequestedModel> dbMovies = new List<RequestedModel>();
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<QualityModel> qualities = new List<QualityModel>();
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, ProviderId = movie.ProviderId,
Type = movie.Type, Type = movie.Type,
@ -89,28 +160,81 @@ namespace PlexRequests.UI.Modules
ImdbId = movie.ImdbId, ImdbId = movie.ImdbId,
Id = movie.Id, Id = movie.Id,
PosterPath = movie.PosterPath, PosterPath = movie.PosterPath,
ReleaseDate = movie.ReleaseDate.Humanize(), ReleaseDate = movie.ReleaseDate,
RequestedDate = movie.RequestedDate.Humanize(), ReleaseDateTicks = movie.ReleaseDate.Ticks,
Approved = movie.Approved, RequestedDate = movie.RequestedDate,
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks,
Approved = movie.Available || movie.Approved,
Title = movie.Title, Title = movie.Title,
Overview = movie.Overview, Overview = movie.Overview,
RequestedBy = movie.RequestedBy, RequestedUsers = isAdmin ? movie.AllUsers.ToArray() : new string[] { },
ReleaseYear = movie.ReleaseDate.Year.ToString(), ReleaseYear = movie.ReleaseDate.Year.ToString(),
Available = movie.Available, Available = movie.Available,
Admin = isAdmin, Admin = isAdmin,
Issues = movie.Issues.Humanize(LetterCasing.Title), Issues = movie.Issues.ToString().CamelCaseToWords(),
OtherMessage = movie.OtherMessage, OtherMessage = movie.OtherMessage,
AdminNotes = movie.AdminNote AdminNotes = movie.AdminNote,
Qualities = qualities.ToArray()
};
}).ToList(); }).ToList();
return Response.AsJson(viewModel); return Response.AsJson(viewModel);
} }
private Response GetTvShows() private Response GetTvShows() // TODO: async await the API calls
{ {
var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated(); var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbTv = Service.GetAll().Where(x => x.Type == RequestType.TvShow);
var viewModel = dbTv.Select(tv => new RequestViewModel List<Task> taskList = new List<Task>();
List<RequestedModel> dbTv = new List<RequestedModel>();
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<QualityModel> qualities = new List<QualityModel>();
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, ProviderId = tv.ProviderId,
Type = tv.Type, Type = tv.Type,
@ -118,19 +242,67 @@ namespace PlexRequests.UI.Modules
ImdbId = tv.ImdbId, ImdbId = tv.ImdbId,
Id = tv.Id, Id = tv.Id,
PosterPath = tv.PosterPath, PosterPath = tv.PosterPath,
ReleaseDate = tv.ReleaseDate.Humanize(), ReleaseDate = tv.ReleaseDate,
RequestedDate = tv.RequestedDate.Humanize(), ReleaseDateTicks = tv.ReleaseDate.Ticks,
Approved = tv.Approved, RequestedDate = tv.RequestedDate,
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks,
Approved = tv.Available || tv.Approved,
Title = tv.Title, Title = tv.Title,
Overview = tv.Overview, Overview = tv.Overview,
RequestedBy = tv.RequestedBy, RequestedUsers = isAdmin ? tv.AllUsers.ToArray() : new string[] { },
ReleaseYear = tv.ReleaseDate.Year.ToString(), ReleaseYear = tv.ReleaseDate.Year.ToString(),
Available = tv.Available, Available = tv.Available,
Admin = isAdmin, Admin = isAdmin,
Issues = tv.Issues.Humanize(LetterCasing.Title), Issues = tv.Issues.ToString().CamelCaseToWords(),
OtherMessage = tv.OtherMessage, OtherMessage = tv.OtherMessage,
AdminNotes = tv.AdminNote, AdminNotes = tv.AdminNote,
TvSeriesRequestType = tv.SeasonsRequested TvSeriesRequestType = tv.SeasonsRequested,
Qualities = qualities.ToArray()
};
}).ToList();
return Response.AsJson(viewModel);
}
private Response GetAlbumRequests()
{
var settings = PrSettings.GetSettings();
var isAdmin = Context.CurrentUser.IsAuthenticated();
var dbAlbum = Service.GetAll().Where(x => x.Type == RequestType.Album);
if (settings.UsersCanViewOnlyOwnRequests && !isAdmin)
{
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(); }).ToList();
return Response.AsJson(viewModel); return Response.AsJson(viewModel);
@ -165,7 +337,7 @@ namespace PlexRequests.UI.Modules
} }
originalRequest.Issues = issue; originalRequest.Issues = issue;
originalRequest.OtherMessage = !string.IsNullOrEmpty(comment) originalRequest.OtherMessage = !string.IsNullOrEmpty(comment)
? $"{Session[SessionKeys.UsernameKey]} - {comment}" ? $"{Username} - {comment}"
: string.Empty; : string.Empty;
@ -173,11 +345,11 @@ namespace PlexRequests.UI.Modules
var model = new NotificationModel var model = new NotificationModel
{ {
User = Session[SessionKeys.UsernameKey].ToString(), User = Username,
NotificationType = NotificationType.Issue, NotificationType = NotificationType.Issue,
Title = originalRequest.Title, Title = originalRequest.Title,
DateTime = DateTime.Now, DateTime = DateTime.Now,
Body = issue == IssueState.Other ? comment : issue.Humanize() Body = issue == IssueState.Other ? comment : issue.ToString().CamelCaseToWords()
}; };
NotificationService.Publish(model); NotificationService.Publish(model);

View file

@ -31,15 +31,18 @@ using System.Linq;
using Nancy; using Nancy;
using Nancy.Responses.Negotiation; using Nancy.Responses.Negotiation;
using Nancy.Security;
using NLog; using NLog;
using PlexRequests.Api; using PlexRequests.Api;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Api.Models.Music;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Helpers.Exceptions; using PlexRequests.Helpers.Exceptions;
using PlexRequests.Services;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Store; using PlexRequests.Store;
@ -54,7 +57,7 @@ namespace PlexRequests.UI.Modules
ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker, ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker,
IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings, IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi,
INotificationService notify) : base("search") INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService<HeadphonesSettings> hpService) : base("search")
{ {
CpService = cpSettings; CpService = cpSettings;
PrService = prSettings; PrService = prSettings;
@ -69,17 +72,24 @@ namespace PlexRequests.UI.Modules
SickRageService = sickRageService; SickRageService = sickRageService;
SickrageApi = srApi; SickrageApi = srApi;
NotificationService = notify; NotificationService = notify;
MusicBrainzApi = mbApi;
HeadphonesApi = hpApi;
HeadphonesService = hpService;
Get["/"] = parameters => RequestLoad(); Get["/"] = parameters => RequestLoad();
Get["movie/{searchTerm}"] = parameters => SearchMovie((string)parameters.searchTerm); Get["movie/{searchTerm}"] = parameters => SearchMovie((string)parameters.searchTerm);
Get["tv/{searchTerm}"] = parameters => SearchTvShow((string)parameters.searchTerm); Get["tv/{searchTerm}"] = parameters => SearchTvShow((string)parameters.searchTerm);
Get["music/{searchTerm}"] = parameters => SearchMusic((string)parameters.searchTerm);
Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id);
Get["movie/upcoming"] = parameters => UpcomingMovies(); Get["movie/upcoming"] = parameters => UpcomingMovies();
Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); Get["movie/playing"] = parameters => CurrentlyPlayingMovies();
Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId); Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId);
Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId);
} }
private TheMovieDbApi MovieApi { get; } private TheMovieDbApi MovieApi { get; }
private INotificationService NotificationService { get; } private INotificationService NotificationService { get; }
@ -93,9 +103,18 @@ namespace PlexRequests.UI.Modules
private ISettingsService<PlexRequestSettings> PrService { get; } private ISettingsService<PlexRequestSettings> PrService { get; }
private ISettingsService<SonarrSettings> SonarrService { get; } private ISettingsService<SonarrSettings> SonarrService { get; }
private ISettingsService<SickRageSettings> SickRageService { get; } private ISettingsService<SickRageSettings> SickRageService { get; }
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
private IAvailabilityChecker Checker { get; } private IAvailabilityChecker Checker { get; }
private IMusicBrainzApi MusicBrainzApi { get; }
private IHeadphonesApi HeadphonesApi { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
private string AuthToken => Cache.GetOrSet(CacheKeys.TvDbToken, TvApi.Authenticate, 50);
private bool IsAdmin {
get
{
return Context.CurrentUser.IsAuthenticated();
}
}
private Negotiator RequestLoad() private Negotiator RequestLoad()
{ {
@ -152,6 +171,28 @@ namespace PlexRequests.UI.Modules
return Response.AsJson(model); return Response.AsJson(model);
} }
private Response SearchMusic(string searchTerm)
{
var albums = MusicBrainzApi.SearchAlbum(searchTerm);
var releases = albums.releases ?? new List<Release>();
var model = new List<SearchMusicViewModel>();
foreach (var a in releases)
{
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 private Response UpcomingMovies() // TODO : Not used
{ {
var movies = MovieApi.GetUpcomingMovies(); var movies = MovieApi.GetUpcomingMovies();
@ -172,30 +213,42 @@ namespace PlexRequests.UI.Modules
private Response RequestMovie(int movieId) 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); Log.Info("Requesting movie with id {0}", movieId);
if (RequestService.CheckRequest(movieId)) var existingRequest = RequestService.CheckRequest(movieId);
if (existingRequest != null)
{ {
Log.Trace("movie with id {0} exists", movieId); // check if the current user is already marked as a requester for this movie, if not, add them
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Movie has already been requested!" }); if (!existingRequest.UserHasRequested(Username))
{
existingRequest.RequestedUsers.Add(Username);
RequestService.UpdateRequest(existingRequest);
}
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} was successfully added!" : $"{fullMovieName} has already been requested!" });
} }
Log.Debug("movie with id {0} doesnt exists", movieId); Log.Debug("movie with id {0} doesnt exists", movieId);
var movieApi = new TheMovieDbApi();
var movieInfo = movieApi.GetMovieInformation(movieId).Result;
Log.Trace("Getting movie info from TheMovieDb");
Log.Trace(movieInfo.DumpJson);
//#if !DEBUG
try 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) 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 //#endif
@ -209,16 +262,14 @@ namespace PlexRequests.UI.Modules
Title = movieInfo.Title, Title = movieInfo.Title,
ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue, ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue,
Status = movieInfo.Status, Status = movieInfo.Status,
RequestedDate = DateTime.Now, RequestedDate = DateTime.UtcNow,
Approved = false, Approved = false,
RequestedBy = Session[SessionKeys.UsernameKey].ToString(), RequestedUsers = new List<string>() { Username },
Issues = IssueState.None, Issues = IssueState.None,
}; };
var settings = PrService.GetSettings();
Log.Trace(settings.DumpJson()); Log.Trace(settings.DumpJson());
if (!settings.RequireMovieApproval) if (ShouldAutoApprove(RequestType.Movie, settings))
{ {
var cpSettings = CpService.GetSettings(); var cpSettings = CpService.GetSettings();
@ -239,13 +290,13 @@ namespace PlexRequests.UI.Modules
var notificationModel = new NotificationModel var notificationModel = new NotificationModel
{ {
Title = model.Title, Title = model.Title,
User = model.RequestedBy, User = Username,
DateTime = DateTime.Now, DateTime = DateTime.Now,
NotificationType = NotificationType.NewRequest NotificationType = NotificationType.NewRequest
}; };
NotificationService.Publish(notificationModel); NotificationService.Publish(notificationModel);
return Response.AsJson(new JsonResponseModel {Result = true}); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
} }
return return
Response.AsJson(new JsonResponseModel Response.AsJson(new JsonResponseModel
@ -264,13 +315,13 @@ namespace PlexRequests.UI.Modules
var notificationModel = new NotificationModel var notificationModel = new NotificationModel
{ {
Title = model.Title, Title = model.Title,
User = model.RequestedBy, User = Username,
DateTime = DateTime.Now, DateTime = DateTime.Now,
NotificationType = NotificationType.NewRequest NotificationType = NotificationType.NewRequest
}; };
NotificationService.Publish(notificationModel); NotificationService.Publish(notificationModel);
return Response.AsJson(new JsonResponseModel { Result = true }); return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
} }
} }
@ -279,10 +330,10 @@ namespace PlexRequests.UI.Modules
Log.Debug("Adding movie to database requests"); Log.Debug("Adding movie to database requests");
var id = RequestService.AddRequest(model); 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); 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) catch (Exception e)
{ {
@ -300,30 +351,44 @@ namespace PlexRequests.UI.Modules
/// <returns></returns> /// <returns></returns>
private Response RequestTvShow(int showId, string seasons) 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 tvApi = new TvMazeApi();
var showInfo = tvApi.ShowLookupByTheTvDbId(showId); var showInfo = tvApi.ShowLookupByTheTvDbId(showId);
DateTime firstAir;
DateTime.TryParse(showInfo.premiered, out firstAir);
string fullShowName = $"{showInfo.name} ({firstAir.Year})";
//#if !DEBUG //#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 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) 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 //#endif
DateTime firstAir;
DateTime.TryParse(showInfo.premiered, out firstAir);
var model = new RequestedModel var model = new RequestedModel
{ {
ProviderId = showInfo.externals?.thetvdb ?? 0, ProviderId = showInfo.externals?.thetvdb ?? 0,
@ -333,9 +398,9 @@ namespace PlexRequests.UI.Modules
Title = showInfo.name, Title = showInfo.name,
ReleaseDate = firstAir, ReleaseDate = firstAir,
Status = showInfo.status, Status = showInfo.status,
RequestedDate = DateTime.Now, RequestedDate = DateTime.UtcNow,
Approved = false, Approved = false,
RequestedBy = Session[SessionKeys.UsernameKey].ToString(), RequestedUsers = new List<string>() { Username },
Issues = IssueState.None, Issues = IssueState.None,
ImdbId = showInfo.externals?.imdb ?? string.Empty, ImdbId = showInfo.externals?.imdb ?? string.Empty,
SeasonCount = showInfo.seasonCount SeasonCount = showInfo.seasonCount
@ -358,26 +423,26 @@ namespace PlexRequests.UI.Modules
model.SeasonList = seasonsList.ToArray(); model.SeasonList = seasonsList.ToArray();
var settings = PrService.GetSettings(); if (ShouldAutoApprove(RequestType.TvShow, settings))
if (!settings.RequireTvShowApproval)
{ {
var sonarrSettings = SonarrService.GetSettings(); var sonarrSettings = SonarrService.GetSettings();
var sender = new TvSender(SonarrApi, SickrageApi); var sender = new TvSender(SonarrApi, SickrageApi);
if (sonarrSettings.Enabled) if (sonarrSettings.Enabled)
{ {
var result = sender.SendToSonarr(sonarrSettings, model); var result = sender.SendToSonarr(sonarrSettings, model);
if (result != null) if (result != null && !string.IsNullOrEmpty(result.title))
{ {
model.Approved = true; model.Approved = true;
Log.Debug("Adding tv to database requests (No approval required & Sonarr)"); Log.Debug("Adding tv to database requests (No approval required & Sonarr)");
RequestService.AddRequest(model); RequestService.AddRequest(model);
var notify1 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
return Response.AsJson(new JsonResponseModel { Result = true });
}
var notify1 = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
NotificationService.Publish(notify1); 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 = true, Message = $"{fullShowName} was successfully added!" });
}
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 @@ namespace PlexRequests.UI.Modules
Log.Debug("Adding tv to database requests (No approval required & SickRage)"); Log.Debug("Adding tv to database requests (No approval required & SickRage)");
RequestService.AddRequest(model); 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); 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 ? "<b>Message From SickRage: </b>" + result.message : "Something went wrong adding the movie to SickRage! Please check your settings." }); return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message != null ? "<b>Message From SickRage: </b>" + result.message : "Something went wrong adding the movie to SickRage! Please check your settings." });
} }
@ -405,16 +470,157 @@ namespace PlexRequests.UI.Modules
RequestService.AddRequest(model); 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); 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; 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<string> { 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;
}
}
} }
} }

View file

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

View file

@ -69,10 +69,6 @@
<HintPath>..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll</HintPath> <HintPath>..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="Humanizer, Version=2.0.1.0, Culture=neutral, PublicKeyToken=979442b78dfc278e, processorArchitecture=MSIL">
<HintPath>..\packages\Humanizer.Core.2.0.1\lib\dotnet\Humanizer.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="MarkdownSharp, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="MarkdownSharp, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll</HintPath> <HintPath>..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll</HintPath>
<Private>True</Private> <Private>True</Private>
@ -168,9 +164,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Bootstrapper.cs" /> <Compile Include="Bootstrapper.cs" />
<Compile Include="Helpers\HeadphonesSender.cs" />
<Compile Include="Helpers\StringHelper.cs" />
<Compile Include="Helpers\TvSender.cs" /> <Compile Include="Helpers\TvSender.cs" />
<Compile Include="Helpers\ValidationHelper.cs" /> <Compile Include="Helpers\ValidationHelper.cs" />
<Compile Include="Models\DatatablesModel.cs" /> <Compile Include="Models\DatatablesModel.cs" />
<Compile Include="Models\QualityModel.cs" />
<Compile Include="Models\SearchMusicViewModel.cs" />
<Compile Include="Validators\PushoverSettingsValidator.cs" /> <Compile Include="Validators\PushoverSettingsValidator.cs" />
<Compile Include="Validators\PushbulletSettingsValidator.cs" /> <Compile Include="Validators\PushbulletSettingsValidator.cs" />
<Compile Include="Validators\EmailNotificationSettingsValidator.cs" /> <Compile Include="Validators\EmailNotificationSettingsValidator.cs" />
@ -248,6 +248,15 @@
<Content Include="Content\jquery.mixitup.js"> <Content Include="Content\jquery.mixitup.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Content\moment.min.es5.js">
<DependentUpon>moment.min.js</DependentUpon>
</Content>
<Content Include="Content\moment.min.es5.min.js">
<DependentUpon>moment.min.es5.js</DependentUpon>
</Content>
<Content Include="Content\moment.min.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Content\pace.css"> <Content Include="Content\pace.css">
<DependentUpon>pace.scss</DependentUpon> <DependentUpon>pace.scss</DependentUpon>
</Content> </Content>
@ -371,6 +380,9 @@
<Content Include="Views\Admin\PushoverNotifications.cshtml"> <Content Include="Views\Admin\PushoverNotifications.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Views\Admin\Headphones.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Web.Debug.config"> <None Include="Web.Debug.config">
<DependentUpon>web.config</DependentUpon> <DependentUpon>web.config</DependentUpon>
</None> </None>

View file

@ -64,8 +64,10 @@ namespace PlexRequests.UI
var s = new Setup(); var s = new Setup();
var cn = s.SetupDb(); var cn = s.SetupDb();
s.CacheQualityProfiles();
ConfigureTargets(cn); ConfigureTargets(cn);
if (port == -1) if (port == -1)
port = GetStartupPort(); port = GetStartupPort();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -75,15 +75,10 @@
<label for="profiles" class="control-label ">Quality Profiles</label> <label for="profiles" class="control-label ">Quality Profiles</label>
<div id="profiles"> <div id="profiles">
<select class="form-control form-control-custom" value="selected"> <select class="form-control form-control-custom" value="selected">
<option id="default" value="default">Use Deafult</option> @foreach (var quality in Model.Qualities)
<option id="sdtv" value="sdtv">SD TV</option> {
<option id="sddvd" value="sddvd">SD DVD</option> <option id="@quality.Key" value="@quality.Key">@quality.Value</option>
<option id="hdtv" value="hdtv">HD TV</option> }
<option id="rawhdtv" value="rawhdtv">Raw HD TV</option>
<option id="hdwebdl" value="hdwebdl">HD Web DL</option>
<option id="fullhdwebdl" value="fullhdwebdl">Full HD Web DL</option>
<option id="hdbluray" value="hdbluray">HD Bluray</option>
<option id="fullhdbluray" value="fullhdbluray">Full HD Bluray</option>
</select> </select>
</div> </div>
</div> </div>

View file

@ -131,6 +131,9 @@
{ {
<text> <text>
var qualitySelected = @Model.QualityProfile; var qualitySelected = @Model.QualityProfile;
if (!qualitySelected) {
return;
}
var $form = $("#mainForm"); var $form = $("#mainForm");
$.ajax({ $.ajax({
type: $form.prop("method"), type: $form.prop("method"),

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