mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-21 05:43:19 -07:00
Merge pull request #140 from tidusjar/dev
Merge dev to master. Release 1.6
This commit is contained in:
commit
3277830de4
113 changed files with 5764 additions and 724 deletions
|
@ -33,6 +33,7 @@ namespace PlexRequests.Api.Interfaces
|
|||
public interface IApiRequest
|
||||
{
|
||||
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 ExecuteJson<T>(IRestRequest request, Uri baseUri) where T : new();
|
||||
}
|
||||
|
|
44
PlexRequests.Api.Interfaces/IHeadphonesApi.cs
Normal file
44
PlexRequests.Api.Interfaces/IHeadphonesApi.cs
Normal 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);
|
||||
}
|
||||
}
|
37
PlexRequests.Api.Interfaces/IMusicBrainzApi.cs
Normal file
37
PlexRequests.Api.Interfaces/IMusicBrainzApi.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -47,6 +47,8 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="IApiRequest.cs" />
|
||||
<Compile Include="ICouchPotatoApi.cs" />
|
||||
<Compile Include="IHeadphonesApi.cs" />
|
||||
<Compile Include="IMusicBrainzApi.cs" />
|
||||
<Compile Include="IPlexApi.cs" />
|
||||
<Compile Include="IPushbulletApi.cs" />
|
||||
<Compile Include="IPushoverApi.cs" />
|
||||
|
|
45
PlexRequests.Api.Models/Music/HeadphonesAlbumSearchResult.cs
Normal file
45
PlexRequests.Api.Models/Music/HeadphonesAlbumSearchResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
49
PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs
Normal file
49
PlexRequests.Api.Models/Music/HeadphonesGetIndex.cs
Normal 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; }
|
||||
}
|
||||
}
|
37
PlexRequests.Api.Models/Music/HeadphonesVersion.cs
Normal file
37
PlexRequests.Api.Models/Music/HeadphonesVersion.cs
Normal 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; }
|
||||
}
|
||||
}
|
55
PlexRequests.Api.Models/Music/MusicBrainzCoverArt.cs
Normal file
55
PlexRequests.Api.Models/Music/MusicBrainzCoverArt.cs
Normal 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; }
|
||||
}
|
||||
}
|
68
PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs
Normal file
68
PlexRequests.Api.Models/Music/MusicBrainzReleaseInfo.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
154
PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs
Normal file
154
PlexRequests.Api.Models/Music/MusicBrainzSearchResults.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
|
@ -303,6 +303,8 @@ namespace PlexRequests.Api.Models.Plex
|
|||
public string AddedAt { get; set; }
|
||||
[XmlAttribute(AttributeName = "updatedAt")]
|
||||
public string UpdatedAt { get; set; }
|
||||
[XmlAttribute(AttributeName = "parentTitle")]
|
||||
public string ParentTitle { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
@ -310,7 +312,7 @@ namespace PlexRequests.Api.Models.Plex
|
|||
public class PlexSearch
|
||||
{
|
||||
[XmlElement(ElementName = "Directory")]
|
||||
public Directory1 Directory { get; set; }
|
||||
public List<Directory1> Directory { get; set; }
|
||||
[XmlElement(ElementName = "Video")]
|
||||
public List<Video> Video { get; set; }
|
||||
[XmlElement(ElementName = "Provider")]
|
||||
|
|
|
@ -48,6 +48,13 @@
|
|||
<Compile Include="Movie\CouchPotatoAdd.cs" />
|
||||
<Compile Include="Movie\CouchPotatoProfiles.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\PushbulletResponse.cs" />
|
||||
<Compile Include="Notifications\PushoverResponse.cs" />
|
||||
|
@ -59,11 +66,14 @@
|
|||
<Compile Include="Plex\PlexStatus.cs" />
|
||||
<Compile Include="Plex\PlexUserRequest.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SickRage\SickRageBase.cs" />
|
||||
<Compile Include="SickRage\SickRagePing.cs" />
|
||||
<Compile Include="SickRage\SickRageSeasonList.cs" />
|
||||
<Compile Include="SickRage\SickRageShowInformation.cs" />
|
||||
<Compile Include="SickRage\SickRageStatus.cs" />
|
||||
<Compile Include="SickRage\SickRageTvAdd.cs" />
|
||||
<Compile Include="Sonarr\SonarrAddSeries.cs" />
|
||||
<Compile Include="Sonarr\SonarrError.cs" />
|
||||
<Compile Include="Sonarr\SonarrProfile.cs" />
|
||||
<Compile Include="Sonarr\SystemStatus.cs" />
|
||||
<Compile Include="Tv\Authentication.cs" />
|
||||
|
|
9
PlexRequests.Api.Models/SickRage/SickRageBase.cs
Normal file
9
PlexRequests.Api.Models/SickRage/SickRageBase.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -31,10 +31,7 @@ namespace PlexRequests.Api.Models.SickRage
|
|||
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; }
|
||||
}
|
||||
}
|
6
PlexRequests.Api.Models/SickRage/SickRageSeasonList.cs
Normal file
6
PlexRequests.Api.Models/SickRage/SickRageSeasonList.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace PlexRequests.Api.Models.SickRage
|
||||
{
|
||||
public class SickRageSeasonList : SickRageBase<int[]>
|
||||
{
|
||||
}
|
||||
}
|
|
@ -75,11 +75,8 @@ namespace PlexRequests.Api.Models.SickRage
|
|||
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; }
|
||||
}
|
||||
|
||||
}
|
|
@ -31,11 +31,8 @@ namespace PlexRequests.Api.Models.SickRage
|
|||
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; }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PlexRequests.Api.Models.Sonarr
|
||||
{
|
||||
public class Season
|
||||
|
@ -23,6 +25,8 @@ namespace PlexRequests.Api.Models.Sonarr
|
|||
public string imdbId { get; set; }
|
||||
public string titleSlug { get; set; }
|
||||
public int id { get; set; }
|
||||
[JsonIgnore]
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AddOptions
|
||||
|
|
36
PlexRequests.Api.Models/Sonarr/SonarrError.cs
Normal file
36
PlexRequests.Api.Models/Sonarr/SonarrError.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -26,13 +26,9 @@
|
|||
#endregion
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using NLog;
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
var client = new RestClient { BaseUrl = baseUri };
|
||||
|
@ -96,20 +107,13 @@ namespace PlexRequests.Api
|
|||
throw new ApplicationException(message, response.ErrorException);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
var json = JsonConvert.DeserializeObject<T>(response.Content);
|
||||
|
||||
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
|
||||
{
|
||||
var ser = new XmlSerializer(typeof(T));
|
||||
|
|
204
PlexRequests.Api/HeadphonesApi.cs
Normal file
204
PlexRequests.Api/HeadphonesApi.cs
Normal 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".
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
PlexRequests.Api/MusicBrainzApi.cs
Normal file
116
PlexRequests.Api/MusicBrainzApi.cs
Normal 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".
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ApiRequest.cs" />
|
||||
<Compile Include="MusicBrainzApi.cs" />
|
||||
<Compile Include="MockApiData.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
|
@ -75,6 +76,7 @@
|
|||
<Compile Include="PushoverApi.cs" />
|
||||
<Compile Include="PushbulletApi.cs" />
|
||||
<Compile Include="SickrageApi.cs" />
|
||||
<Compile Include="HeadphonesApi.cs" />
|
||||
<Compile Include="SonarrApi.cs" />
|
||||
<Compile Include="CouchPotatoApi.cs" />
|
||||
<Compile Include="MovieBase.cs" />
|
||||
|
|
|
@ -74,17 +74,18 @@ namespace PlexRequests.Api
|
|||
|
||||
var obj = Api.Execute<SickRageTvAdd>(request, baseUrl);
|
||||
|
||||
|
||||
if (obj.result != "failure")
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
// Check to see if it's been added yet.
|
||||
var showInfo = new SickRageShowInformation { message = "Show not found" };
|
||||
while (showInfo.message.Equals("Show not found", StringComparison.CurrentCultureIgnoreCase))
|
||||
var seasonIncrement = 0;
|
||||
var seasonList = new SickRageSeasonList();
|
||||
while (seasonIncrement < seasonCount)
|
||||
{
|
||||
showInfo = CheckShowHasBeenAdded(tvdbId, apiKey, baseUrl);
|
||||
seasonList = VerifyShowHasLoaded(tvdbId, apiKey, baseUrl);
|
||||
seasonIncrement = seasonList.data?.Length ?? 0;
|
||||
|
||||
if (sw.ElapsedMilliseconds > 30000) // Break out after 30 seconds, it's not going to get added
|
||||
{
|
||||
Log.Warn("Couldn't find out if the show had been added after 10 seconds. I doubt we can change the status to wanted.");
|
||||
|
@ -93,8 +94,6 @@ namespace PlexRequests.Api
|
|||
}
|
||||
sw.Stop();
|
||||
}
|
||||
|
||||
|
||||
if (seasons.Length > 0)
|
||||
{
|
||||
//handle the seasons requested
|
||||
|
@ -123,6 +122,28 @@ namespace PlexRequests.Api
|
|||
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)
|
||||
{
|
||||
var request = new RestRequest
|
||||
|
@ -138,21 +159,5 @@ namespace PlexRequests.Api
|
|||
await Task.Run(() => Thread.Sleep(2000));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,9 +27,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using NLog;
|
||||
using PlexRequests.Api.Interfaces;
|
||||
using PlexRequests.Api.Models.Sonarr;
|
||||
using PlexRequests.Helpers;
|
||||
|
||||
using RestSharp;
|
||||
|
||||
namespace PlexRequests.Api
|
||||
|
@ -56,7 +61,8 @@ namespace PlexRequests.Api
|
|||
|
||||
public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl)
|
||||
{
|
||||
|
||||
Log.Debug("Adding series {0}", title);
|
||||
Log.Debug("Seasons = {0}, out of {1} seasons", seasons.DumpJson(), seasonCount);
|
||||
var request = new RestRequest
|
||||
{
|
||||
Resource = "/api/Series?",
|
||||
|
@ -74,7 +80,6 @@ namespace PlexRequests.Api
|
|||
rootFolderPath = rootPath
|
||||
};
|
||||
|
||||
|
||||
for (var i = 1; i <= seasonCount; i++)
|
||||
{
|
||||
var season = new Season
|
||||
|
@ -85,12 +90,25 @@ namespace PlexRequests.Api
|
|||
options.seasons.Add(season);
|
||||
}
|
||||
|
||||
Log.Debug("Sonarr API Options:");
|
||||
Log.Debug(options.DumpJson());
|
||||
|
||||
request.AddHeader("X-Api-Key", apiKey);
|
||||
request.AddJsonBody(options);
|
||||
|
||||
var obj = Api.ExecuteJson<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)
|
||||
|
|
|
@ -29,5 +29,8 @@ namespace PlexRequests.Core
|
|||
public class CacheKeys
|
||||
{
|
||||
public const string TvDbToken = "TheTvDbApiToken";
|
||||
public const string SonarrQualityProfiles = "SonarrQualityProfiles";
|
||||
public const string SickRageQualityProfiles = "SickRageQualityProfiles";
|
||||
public const string CouchPotatoQualityProfiles = "CouchPotatoQualityProfiles";
|
||||
}
|
||||
}
|
|
@ -33,7 +33,9 @@ namespace PlexRequests.Core
|
|||
public interface IRequestService
|
||||
{
|
||||
long AddRequest(RequestedModel model);
|
||||
bool CheckRequest(int providerId);
|
||||
RequestedModel CheckRequest(int providerId);
|
||||
RequestedModel CheckRequest(string musicId);
|
||||
|
||||
void DeleteRequest(RequestedModel request);
|
||||
bool UpdateRequest(RequestedModel model);
|
||||
RequestedModel Get(int id);
|
||||
|
|
|
@ -52,16 +52,24 @@ namespace PlexRequests.Core
|
|||
// TODO Keep an eye on this, since we are now doing 2 DB update for 1 single request, inserting and then updating
|
||||
model.Id = (int)id;
|
||||
|
||||
entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id };
|
||||
entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id, MusicId = model.MusicBrainzId};
|
||||
var result = Repo.Update(entity);
|
||||
|
||||
return result ? id : -1;
|
||||
}
|
||||
|
||||
public bool CheckRequest(int providerId)
|
||||
public RequestedModel CheckRequest(int providerId)
|
||||
{
|
||||
var blobs = Repo.GetAll();
|
||||
return blobs.Any(x => x.ProviderId == providerId);
|
||||
var blob = blobs.FirstOrDefault(x => x.ProviderId == providerId);
|
||||
return blob != null ? ByteConverterHelper.ReturnObject<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)
|
||||
|
@ -79,6 +87,10 @@ namespace PlexRequests.Core
|
|||
public RequestedModel Get(int id)
|
||||
{
|
||||
var blob = Repo.Get(id);
|
||||
if (blob == null)
|
||||
{
|
||||
return new RequestedModel();
|
||||
}
|
||||
var model = ByteConverterHelper.ReturnObject<RequestedModel>(blob.Content);
|
||||
return model;
|
||||
}
|
||||
|
|
|
@ -25,11 +25,20 @@
|
|||
// ************************************************************************/
|
||||
#endregion
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PlexRequests.Core.Models
|
||||
{
|
||||
public class StatusModel
|
||||
{
|
||||
public string Version { get; set; }
|
||||
public int DBVersion {
|
||||
get
|
||||
{
|
||||
string trimStatus = new Regex("[^0-9]", RegexOptions.Compiled).Replace(Version, string.Empty).PadRight(4, '0');
|
||||
return int.Parse(trimStatus);
|
||||
}
|
||||
}
|
||||
public bool UpdateAvailable { get; set; }
|
||||
public string UpdateUri { get; set; }
|
||||
public string DownloadUri { get; set; }
|
||||
|
|
|
@ -46,6 +46,10 @@
|
|||
<HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</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">
|
||||
<HintPath>..\packages\Octokit.0.19.0\lib\net45\Octokit.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
|
@ -74,6 +78,7 @@
|
|||
<Compile Include="Models\StatusModel.cs" />
|
||||
<Compile Include="Models\UserProperties.cs" />
|
||||
<Compile Include="SettingModels\AuthenticationSettings.cs" />
|
||||
<Compile Include="SettingModels\HeadphonesSettings.cs" />
|
||||
<Compile Include="SettingModels\PushoverNotificationSettings.cs" />
|
||||
<Compile Include="SettingModels\PushBulletNotificationSettings.cs" />
|
||||
<Compile Include="SettingModels\EmailNotificationSettings.cs" />
|
||||
|
@ -95,6 +100,10 @@
|
|||
<None Include="packages.config" />
|
||||
</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">
|
||||
<Project>{CB37A5F8-6DFC-4554-99D3-A42B502E4591}</Project>
|
||||
<Name>PlexRequests.Api.Models</Name>
|
||||
|
|
58
PlexRequests.Core/SettingModels/HeadphonesSettings.cs
Normal file
58
PlexRequests.Core/SettingModels/HeadphonesSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,10 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// ************************************************************************/
|
||||
#endregion
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PlexRequests.Core.SettingModels
|
||||
{
|
||||
public class PlexRequestSettings : Settings
|
||||
|
@ -32,8 +36,33 @@ namespace PlexRequests.Core.SettingModels
|
|||
|
||||
public bool SearchForMovies { get; set; }
|
||||
public bool SearchForTvShows { get; set; }
|
||||
public bool SearchForMusic { get; set; }
|
||||
public bool RequireMovieApproval { get; set; }
|
||||
public bool RequireTvShowApproval { get; set; }
|
||||
public bool RequireMusicApproval { get; set; }
|
||||
public bool UsersCanViewOnlyOwnRequests { get; set; }
|
||||
public int WeeklyRequestLimit { get; set; }
|
||||
public string NoApprovalUsers { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using PlexRequests.Helpers;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PlexRequests.Core.SettingModels
|
||||
{
|
||||
|
@ -40,6 +41,23 @@ namespace PlexRequests.Core.SettingModels
|
|||
public string QualityProfile { get; set; }
|
||||
public bool Ssl { 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]
|
||||
public Uri FullUri
|
||||
|
|
|
@ -30,16 +30,19 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
|
||||
using Mono.Data.Sqlite;
|
||||
using NLog;
|
||||
using PlexRequests.Api;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
using PlexRequests.Helpers;
|
||||
using PlexRequests.Store;
|
||||
using PlexRequests.Store.Repository;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PlexRequests.Core
|
||||
{
|
||||
public class Setup
|
||||
{
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
private static DbConfiguration Db { get; set; }
|
||||
public string SetupDb()
|
||||
{
|
||||
|
@ -52,12 +55,40 @@ namespace PlexRequests.Core
|
|||
CreateDefaultSettingsPage();
|
||||
}
|
||||
|
||||
MigrateDb();
|
||||
var version = CheckSchema();
|
||||
if (version > 0)
|
||||
{
|
||||
if (version > 1300 && version <= 1699)
|
||||
{
|
||||
MigrateDbFrom1300();
|
||||
UpdateRequestBlobsTable();
|
||||
}
|
||||
}
|
||||
|
||||
return Db.DbConnection().ConnectionString;
|
||||
}
|
||||
|
||||
public static string ConnectionString => Db.DbConnection().ConnectionString;
|
||||
|
||||
|
||||
private int CheckSchema()
|
||||
{
|
||||
var checker = new StatusChecker();
|
||||
var status = checker.GetStatus();
|
||||
|
||||
var connection = Db.DbConnection();
|
||||
var schema = connection.GetSchemaVersion();
|
||||
if (schema == null)
|
||||
{
|
||||
connection.CreateSchema(status.DBVersion); // Set the default.
|
||||
schema = connection.GetSchemaVersion();
|
||||
}
|
||||
|
||||
var version = schema.SchemaVersion;
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private void CreateDefaultSettingsPage()
|
||||
{
|
||||
var defaultSettings = new PlexRequestSettings
|
||||
|
@ -72,8 +103,82 @@ namespace PlexRequests.Core
|
|||
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>();
|
||||
RequestedModel[] requestedModels;
|
||||
var repo = new GenericRepository<RequestedModel>(Db, new MemoryCacheProvider());
|
||||
|
@ -113,7 +218,7 @@ namespace PlexRequests.Core
|
|||
Issues = r.Issues,
|
||||
OtherMessage = r.OtherMessage,
|
||||
Overview = show.summary.RemoveHtml(),
|
||||
RequestedBy = r.RequestedBy,
|
||||
RequestedUsers = r.AllUsers, // should pull in the RequestedBy property and merge with RequestedUsers
|
||||
RequestedDate = r.ReleaseDate,
|
||||
Status = show.status
|
||||
};
|
||||
|
|
|
@ -26,8 +26,6 @@
|
|||
#endregion
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Octokit;
|
||||
|
@ -62,7 +60,10 @@ namespace PlexRequests.Core
|
|||
};
|
||||
|
||||
var latestRelease = GetLatestRelease();
|
||||
|
||||
if (latestRelease.Result == null)
|
||||
{
|
||||
return new StatusModel { Version = "Unknown" };
|
||||
}
|
||||
var latestVersionArray = latestRelease.Result.Name.Split(new[] { 'v' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var latestVersion = latestVersionArray.Length > 1 ? latestVersionArray[1] : string.Empty;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<package id="Nancy" version="1.4.3" 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="NLog" version="4.2.3" targetFramework="net46" />
|
||||
<package id="Octokit" version="0.19.0" targetFramework="net46" />
|
||||
<package id="valueinjecter" version="3.1.1.2" targetFramework="net452" />
|
||||
</packages>
|
42
PlexRequests.Helpers/DateTimeHelper.cs
Normal file
42
PlexRequests.Helpers/DateTimeHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ namespace PlexRequests.Helpers
|
|||
/// <param name="key">The key.</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>
|
||||
void Set(string key, object data, int cacheTime);
|
||||
void Set(string key, object data, int cacheTime = 20);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified object from the cache.
|
||||
|
|
|
@ -83,7 +83,7 @@ namespace PlexRequests.Helpers
|
|||
/// <param name="key">The key.</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>
|
||||
public void Set(string key, object data, int cacheTime)
|
||||
public void Set(string key, object data, int cacheTime = 20)
|
||||
{
|
||||
var policy = new CacheItemPolicy { AbsoluteExpiration = DateTime.Now + TimeSpan.FromMinutes(cacheTime) };
|
||||
Cache.Add(new CacheItem(key, data), policy);
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="AssemblyHelper.cs" />
|
||||
<Compile Include="ByteConverterHelper.cs" />
|
||||
<Compile Include="DateTimeHelper.cs" />
|
||||
<Compile Include="Exceptions\ApplicationSettingsException.cs" />
|
||||
<Compile Include="HtmlRemover.cs" />
|
||||
<Compile Include="ICacheProvider.cs" />
|
||||
|
|
|
@ -32,12 +32,12 @@ using Moq;
|
|||
using NUnit.Framework;
|
||||
|
||||
using PlexRequests.Api.Interfaces;
|
||||
using PlexRequests.Api.Models;
|
||||
using PlexRequests.Api.Models.Plex;
|
||||
using PlexRequests.Core;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
using PlexRequests.Helpers.Exceptions;
|
||||
using PlexRequests.Services.Interfaces;
|
||||
using PlexRequests.Store;
|
||||
|
||||
namespace PlexRequests.Services.Tests
|
||||
{
|
||||
|
@ -55,7 +55,7 @@ namespace PlexRequests.Services.Tests
|
|||
var plexMock = new Mock<IPlexApi>();
|
||||
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]
|
||||
|
@ -74,20 +74,20 @@ namespace PlexRequests.Services.Tests
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAvailableDirectoryTitleTest()
|
||||
public void IsAvailableMusicDirectoryTitleTest()
|
||||
{
|
||||
var settingsMock = new Mock<ISettingsService<PlexSettings>>();
|
||||
var authMock = new Mock<ISettingsService<AuthenticationSettings>>();
|
||||
var requestMock = new Mock<IRequestService>();
|
||||
var plexMock = new Mock<IPlexApi>();
|
||||
|
||||
var searchResult = new PlexSearch { Directory = new Directory1 {Title = "title", 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" });
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -108,7 +150,7 @@ namespace PlexRequests.Services.Tests
|
|||
var requestMock = new Mock<IRequestService>();
|
||||
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" });
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -137,9 +200,348 @@ namespace PlexRequests.Services.Tests
|
|||
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,6 +84,10 @@
|
|||
<Project>{566EFA49-68F8-4716-9693-A6B3F2624DEA}</Project>
|
||||
<Name>PlexRequests.Services</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\PlexRequests.Store\PlexRequests.Store.csproj">
|
||||
<Project>{92433867-2B7B-477B-A566-96C382427525}</Project>
|
||||
<Name>PlexRequests.Store</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
|
||||
|
|
|
@ -48,9 +48,12 @@ namespace PlexRequests.Services
|
|||
{
|
||||
public AvailabilityUpdateService()
|
||||
{
|
||||
var memCache = new MemoryCacheProvider();
|
||||
var dbConfig = new DbConfiguration(new SqliteFactory());
|
||||
var repo = new SettingsJsonRepository(dbConfig, memCache);
|
||||
|
||||
ConfigurationReader = new ConfigurationReader();
|
||||
var repo = new SettingsJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider());
|
||||
Checker = new PlexAvailabilityChecker(new SettingsServiceV2<PlexSettings>(repo), new SettingsServiceV2<AuthenticationSettings>(repo), new JsonRequestService(new RequestJsonRepository(new DbConfiguration(new SqliteFactory()), new MemoryCacheProvider())), new PlexApi());
|
||||
Checker = new PlexAvailabilityChecker(new SettingsServiceV2<PlexSettings>(repo), new SettingsServiceV2<AuthenticationSettings>(repo), new JsonRequestService(new RequestJsonRepository(dbConfig, memCache)), new PlexApi());
|
||||
HostingEnvironment.RegisterObject(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,6 @@ namespace PlexRequests.Services.Interfaces
|
|||
public interface IAvailabilityChecker
|
||||
{
|
||||
void CheckAndUpdateAll(long check);
|
||||
bool IsAvailable(string title, string year);
|
||||
bool IsAvailable(string title, string year, string artist, PlexType type);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using PlexRequests.Services.Notification;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
|
||||
namespace PlexRequests.Services.Interfaces
|
||||
{
|
||||
|
@ -35,5 +36,7 @@ namespace PlexRequests.Services.Interfaces
|
|||
string NotificationName { get; }
|
||||
|
||||
Task NotifyAsync(NotificationModel model);
|
||||
|
||||
Task NotifyAsync(NotificationModel model, Settings settings);
|
||||
}
|
||||
}
|
|
@ -27,12 +27,14 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using PlexRequests.Services.Notification;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
|
||||
namespace PlexRequests.Services.Interfaces
|
||||
{
|
||||
public interface INotificationService
|
||||
{
|
||||
Task Publish(NotificationModel model);
|
||||
Task Publish(NotificationModel model, Settings settings);
|
||||
void Subscribe(INotification notification);
|
||||
void UnSubscribe(INotification notification);
|
||||
|
||||
|
|
|
@ -46,24 +46,29 @@ namespace PlexRequests.Services.Notification
|
|||
|
||||
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||
private ISettingsService<EmailNotificationSettings> EmailNotificationSettings { get; }
|
||||
private EmailNotificationSettings Settings => GetConfiguration();
|
||||
public string NotificationName => "EmailMessageNotification";
|
||||
|
||||
public async Task NotifyAsync(NotificationModel model)
|
||||
{
|
||||
var configuration = GetConfiguration();
|
||||
if (!ValidateConfiguration(configuration))
|
||||
{
|
||||
return;
|
||||
await NotifyAsync(model, configuration);
|
||||
}
|
||||
|
||||
public async Task NotifyAsync(NotificationModel model, Settings settings)
|
||||
{
|
||||
if (settings == null) await NotifyAsync(model);
|
||||
|
||||
var emailSettings = (EmailNotificationSettings)settings;
|
||||
|
||||
if (!ValidateConfiguration(emailSettings)) return;
|
||||
|
||||
switch (model.NotificationType)
|
||||
{
|
||||
case NotificationType.NewRequest:
|
||||
await EmailNewRequest(model);
|
||||
await EmailNewRequest(model, emailSettings);
|
||||
break;
|
||||
case NotificationType.Issue:
|
||||
await EmailIssue(model);
|
||||
await EmailIssue(model, emailSettings);
|
||||
break;
|
||||
case NotificationType.RequestAvailable:
|
||||
throw new NotImplementedException();
|
||||
|
@ -74,6 +79,10 @@ namespace PlexRequests.Services.Notification
|
|||
case NotificationType.AdminNote:
|
||||
throw new NotImplementedException();
|
||||
|
||||
case NotificationType.Test:
|
||||
await EmailTest(model, emailSettings);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
@ -100,23 +109,23 @@ namespace PlexRequests.Services.Notification
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task EmailNewRequest(NotificationModel model)
|
||||
private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
|
||||
{
|
||||
var message = new MailMessage
|
||||
{
|
||||
IsBodyHtml = true,
|
||||
To = { new MailAddress(Settings.RecipientEmail) },
|
||||
To = { new MailAddress(settings.RecipientEmail) },
|
||||
Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}",
|
||||
From = new MailAddress(Settings.EmailSender),
|
||||
From = new MailAddress(settings.EmailSender),
|
||||
Subject = $"Plex Requests: New request for {model.Title}!"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort))
|
||||
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
|
||||
{
|
||||
smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword);
|
||||
smtp.EnableSsl = Settings.Ssl;
|
||||
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
|
||||
smtp.EnableSsl = settings.Ssl;
|
||||
await smtp.SendMailAsync(message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
@ -130,23 +139,53 @@ namespace PlexRequests.Services.Notification
|
|||
}
|
||||
}
|
||||
|
||||
private async Task EmailIssue(NotificationModel model)
|
||||
private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings)
|
||||
{
|
||||
var message = new MailMessage
|
||||
{
|
||||
IsBodyHtml = true,
|
||||
To = { new MailAddress(Settings.RecipientEmail) },
|
||||
To = { new MailAddress(settings.RecipientEmail) },
|
||||
Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
|
||||
From = new MailAddress(Settings.RecipientEmail),
|
||||
From = new MailAddress(settings.RecipientEmail),
|
||||
Subject = $"Plex Requests: New issue for {model.Title}!"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort))
|
||||
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
|
||||
{
|
||||
smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword);
|
||||
smtp.EnableSsl = Settings.Ssl;
|
||||
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
|
||||
smtp.EnableSsl = settings.Ssl;
|
||||
await smtp.SendMailAsync(message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (SmtpException smtp)
|
||||
{
|
||||
Log.Error(smtp);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings)
|
||||
{
|
||||
var message = new MailMessage
|
||||
{
|
||||
IsBodyHtml = true,
|
||||
To = { new MailAddress(settings.RecipientEmail) },
|
||||
Body = "This is just a test! Success!",
|
||||
From = new MailAddress(settings.RecipientEmail),
|
||||
Subject = "Plex Requests: Test Message!"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (var smtp = new SmtpClient(settings.EmailHost, settings.EmailPort))
|
||||
{
|
||||
smtp.Credentials = new NetworkCredential(settings.EmailUsername, settings.EmailPassword);
|
||||
smtp.EnableSsl = settings.Ssl;
|
||||
await smtp.SendMailAsync(message).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ using System.Threading.Tasks;
|
|||
using NLog;
|
||||
|
||||
using PlexRequests.Services.Interfaces;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
|
||||
namespace PlexRequests.Services.Notification
|
||||
{
|
||||
|
@ -47,6 +48,13 @@ namespace PlexRequests.Services.Notification
|
|||
await Task.WhenAll(notificationTasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task Publish(NotificationModel model, Settings settings)
|
||||
{
|
||||
var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model, settings));
|
||||
|
||||
await Task.WhenAll(notificationTasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Subscribe(INotification notification)
|
||||
{
|
||||
Observers.TryAdd(notification.NotificationName, notification);
|
||||
|
@ -67,6 +75,19 @@ namespace PlexRequests.Services.Notification
|
|||
{
|
||||
Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static async Task NotifyAsync(INotification notification, NotificationModel model, Settings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
await notification.NotifyAsync(model, settings).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,5 +33,6 @@ namespace PlexRequests.Services.Notification
|
|||
RequestAvailable,
|
||||
RequestApproved,
|
||||
AdminNote,
|
||||
Test
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification
|
|||
public string NotificationName => "PushbulletNotification";
|
||||
public async Task NotifyAsync(NotificationModel model)
|
||||
{
|
||||
if (!ValidateConfiguration())
|
||||
{
|
||||
return;
|
||||
var configuration = GetSettings();
|
||||
await NotifyAsync(model, configuration);
|
||||
}
|
||||
|
||||
public async Task NotifyAsync(NotificationModel model, Settings settings)
|
||||
{
|
||||
if (settings == null) await NotifyAsync(model);
|
||||
|
||||
var pushSettings = (PushbulletNotificationSettings)settings;
|
||||
|
||||
if (!ValidateConfiguration(pushSettings)) return;
|
||||
|
||||
switch (model.NotificationType)
|
||||
{
|
||||
case NotificationType.NewRequest:
|
||||
await PushNewRequestAsync(model);
|
||||
await PushNewRequestAsync(model, pushSettings);
|
||||
break;
|
||||
case NotificationType.Issue:
|
||||
await PushIssueAsync(model);
|
||||
await PushIssueAsync(model, pushSettings);
|
||||
break;
|
||||
case NotificationType.RequestAvailable:
|
||||
break;
|
||||
|
@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification
|
|||
break;
|
||||
case NotificationType.AdminNote:
|
||||
break;
|
||||
case NotificationType.Test:
|
||||
await PushTestAsync(model, pushSettings);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateConfiguration()
|
||||
private bool ValidateConfiguration(PushbulletNotificationSettings settings)
|
||||
{
|
||||
if (!Settings.Enabled)
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrEmpty(Settings.AccessToken))
|
||||
if (string.IsNullOrEmpty(settings.AccessToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -93,13 +103,13 @@ namespace PlexRequests.Services.Notification
|
|||
return SettingsService.GetSettings();
|
||||
}
|
||||
|
||||
private async Task PushNewRequestAsync(NotificationModel model)
|
||||
private async Task PushNewRequestAsync(NotificationModel model, PushbulletNotificationSettings settings)
|
||||
{
|
||||
var message = $"{model.Title} has been requested by user: {model.User}";
|
||||
var pushTitle = $"Plex Requests: {model.Title} has been requested!";
|
||||
try
|
||||
{
|
||||
var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier);
|
||||
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
|
||||
if (result == null)
|
||||
{
|
||||
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
|
||||
|
@ -111,13 +121,31 @@ namespace PlexRequests.Services.Notification
|
|||
}
|
||||
}
|
||||
|
||||
private async Task PushIssueAsync(NotificationModel model)
|
||||
private async Task PushIssueAsync(NotificationModel model, PushbulletNotificationSettings settings)
|
||||
{
|
||||
var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
|
||||
var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}";
|
||||
try
|
||||
{
|
||||
var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier);
|
||||
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
|
||||
if (result != null)
|
||||
{
|
||||
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PushTestAsync(NotificationModel model, PushbulletNotificationSettings settings)
|
||||
{
|
||||
var message = "This is just a test! Success!";
|
||||
var pushTitle = "Plex Requests: Test Message!";
|
||||
try
|
||||
{
|
||||
var result = await PushbulletApi.PushAsync(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier);
|
||||
if (result != null)
|
||||
{
|
||||
Log.Error("Pushbullet api returned a null value, the notification did not get pushed");
|
||||
|
|
|
@ -51,18 +51,25 @@ namespace PlexRequests.Services.Notification
|
|||
public string NotificationName => "PushoverNotification";
|
||||
public async Task NotifyAsync(NotificationModel model)
|
||||
{
|
||||
if (!ValidateConfiguration())
|
||||
{
|
||||
return;
|
||||
var configuration = GetSettings();
|
||||
await NotifyAsync(model, configuration);
|
||||
}
|
||||
|
||||
public async Task NotifyAsync(NotificationModel model, Settings settings)
|
||||
{
|
||||
if (settings == null) await NotifyAsync(model);
|
||||
|
||||
var pushSettings = (PushoverNotificationSettings)settings;
|
||||
|
||||
if (!ValidateConfiguration(pushSettings)) return;
|
||||
|
||||
switch (model.NotificationType)
|
||||
{
|
||||
case NotificationType.NewRequest:
|
||||
await PushNewRequestAsync(model);
|
||||
await PushNewRequestAsync(model, pushSettings);
|
||||
break;
|
||||
case NotificationType.Issue:
|
||||
await PushIssueAsync(model);
|
||||
await PushIssueAsync(model, pushSettings);
|
||||
break;
|
||||
case NotificationType.RequestAvailable:
|
||||
break;
|
||||
|
@ -70,18 +77,21 @@ namespace PlexRequests.Services.Notification
|
|||
break;
|
||||
case NotificationType.AdminNote:
|
||||
break;
|
||||
case NotificationType.Test:
|
||||
await PushTestAsync(model, pushSettings);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateConfiguration()
|
||||
private bool ValidateConfiguration(PushoverNotificationSettings settings)
|
||||
{
|
||||
if (!Settings.Enabled)
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrEmpty(Settings.AccessToken) || string.IsNullOrEmpty(Settings.UserToken))
|
||||
if (string.IsNullOrEmpty(settings.AccessToken) || string.IsNullOrEmpty(settings.UserToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -93,12 +103,12 @@ namespace PlexRequests.Services.Notification
|
|||
return SettingsService.GetSettings();
|
||||
}
|
||||
|
||||
private async Task PushNewRequestAsync(NotificationModel model)
|
||||
private async Task PushNewRequestAsync(NotificationModel model, PushoverNotificationSettings settings)
|
||||
{
|
||||
var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}";
|
||||
try
|
||||
{
|
||||
var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken);
|
||||
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
|
||||
if (result?.status != 1)
|
||||
{
|
||||
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
|
||||
|
@ -110,12 +120,29 @@ namespace PlexRequests.Services.Notification
|
|||
}
|
||||
}
|
||||
|
||||
private async Task PushIssueAsync(NotificationModel model)
|
||||
private async Task PushIssueAsync(NotificationModel model, PushoverNotificationSettings settings)
|
||||
{
|
||||
var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}";
|
||||
try
|
||||
{
|
||||
var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken);
|
||||
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
|
||||
if (result?.status != 1)
|
||||
{
|
||||
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PushTestAsync(NotificationModel model, PushoverNotificationSettings settings)
|
||||
{
|
||||
var message = $"Plex Requests: Test Message!";
|
||||
try
|
||||
{
|
||||
var result = await PushoverApi.PushAsync(settings.AccessToken, message, settings.UserToken);
|
||||
if (result?.status != 1)
|
||||
{
|
||||
Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed");
|
||||
|
|
|
@ -24,14 +24,17 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// ************************************************************************/
|
||||
#endregion
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using NLog;
|
||||
|
||||
using PlexRequests.Api.Interfaces;
|
||||
using PlexRequests.Api.Models.Plex;
|
||||
using PlexRequests.Core;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
using PlexRequests.Helpers;
|
||||
using PlexRequests.Helpers.Exceptions;
|
||||
using PlexRequests.Services.Interfaces;
|
||||
using PlexRequests.Store;
|
||||
|
@ -52,82 +55,201 @@ namespace PlexRequests.Services
|
|||
private ISettingsService<AuthenticationSettings> Auth { get; }
|
||||
private IRequestService RequestService { get; }
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
private IPlexApi PlexApi { get; set; }
|
||||
private IPlexApi PlexApi { get; }
|
||||
|
||||
|
||||
public void CheckAndUpdateAll(long check)
|
||||
{
|
||||
Log.Trace("This is check no. {0}", check);
|
||||
Log.Trace("Getting the settings");
|
||||
var plexSettings = Plex.GetSettings();
|
||||
var authSettings = Auth.GetSettings();
|
||||
Log.Trace("Getting all the requests");
|
||||
var requests = RequestService.GetAll();
|
||||
|
||||
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
|
||||
if (!ValidateSettings(plexSettings, authSettings, requestedModels))
|
||||
var requestedModels = requests as RequestedModel[] ?? requests.Where(x => !x.Available).ToArray();
|
||||
Log.Trace("Requests Count {0}", requestedModels.Length);
|
||||
|
||||
if (!ValidateSettings(plexSettings, authSettings) || !requestedModels.Any())
|
||||
{
|
||||
Log.Info("Validation of the settings failed or there is no requests.");
|
||||
return;
|
||||
}
|
||||
|
||||
var modifiedModel = new List<RequestedModel>();
|
||||
foreach (var r in requestedModels)
|
||||
{
|
||||
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri);
|
||||
var result = results.Video.FirstOrDefault(x => x.Title == r.Title);
|
||||
var originalRequest = RequestService.Get(r.Id);
|
||||
|
||||
originalRequest.Available = result != null;
|
||||
modifiedModel.Add(originalRequest);
|
||||
Log.Trace("We are going to see if Plex has the following title: {0}", r.Title);
|
||||
PlexSearch results;
|
||||
try
|
||||
{
|
||||
results = PlexApi.SearchContent(authSettings.PlexAuthToken, r.Title, plexSettings.FullUri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("We failed to search Plex for the following request:");
|
||||
Log.Error(r.DumpJson());
|
||||
Log.Error(e);
|
||||
break; // Let's finish processing and not crash the process, there is a reason why we cannot connect.
|
||||
}
|
||||
|
||||
if (results == null)
|
||||
{
|
||||
Log.Trace("Could not find any matching result for this title.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Trace("Search results from Plex for the following request: {0}", r.Title);
|
||||
Log.Trace(results.DumpJson());
|
||||
bool matchResult;
|
||||
var releaseDate = r.ReleaseDate == DateTime.MinValue ? string.Empty : r.ReleaseDate.ToString("yyyy");
|
||||
switch (r.Type)
|
||||
{
|
||||
case RequestType.Movie:
|
||||
matchResult = MovieTvSearch(results, r.Title, releaseDate);
|
||||
break;
|
||||
case RequestType.TvShow:
|
||||
matchResult = MovieTvSearch(results, r.Title, releaseDate);
|
||||
break;
|
||||
case RequestType.Album:
|
||||
matchResult = AlbumSearch(results, r.Title, r.ArtistName);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (matchResult)
|
||||
{
|
||||
r.Available = true;
|
||||
modifiedModel.Add(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified search term is available.
|
||||
/// Determines whether the specified title is available.
|
||||
/// </summary>
|
||||
/// <param name="title">The search term.</param>
|
||||
/// <param name="title">The title.</param>
|
||||
/// <param name="year">The year.</param>
|
||||
/// <param name="artist">The artist.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns></returns>
|
||||
/// <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 authSettings = Auth.GetSettings();
|
||||
|
||||
if (!ValidateSettings(plexSettings, authSettings))
|
||||
{
|
||||
Log.Warn("The settings are not configured");
|
||||
throw new ApplicationSettingsException("The settings are not configured for Plex or Authentication");
|
||||
}
|
||||
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
|
||||
|
||||
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))
|
||||
{
|
||||
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
|
||||
var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title) && x.Year == year);
|
||||
var directoryTitle = results.Directory?.Title == title && results.Directory?.Year == year;
|
||||
return result?.Title != null || directoryTitle;
|
||||
var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase) && x.Year == year);
|
||||
|
||||
var directoryResult = false;
|
||||
if (results.Directory != null)
|
||||
{
|
||||
if (results.Directory.Any(d => d.Title.Equals(title, StringComparison.CurrentCultureIgnoreCase) && d.Year == year))
|
||||
{
|
||||
directoryResult = true;
|
||||
}
|
||||
}
|
||||
return result?.Title != null || directoryResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
var results = PlexApi.SearchContent(authSettings.PlexAuthToken, title, plexSettings.FullUri);
|
||||
var result = results.Video?.FirstOrDefault(x => x.Title.Contains(title));
|
||||
var directoryTitle = results.Directory?.Title == title;
|
||||
return result?.Title != null || directoryTitle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private bool ValidateSettings(PlexSettings plex, AuthenticationSettings auth, IEnumerable<RequestedModel> requests)
|
||||
var result = results.Video?.FirstOrDefault(x => x.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase));
|
||||
var directoryResult = false;
|
||||
if (results.Directory != null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
<Compile Include="Notification\PushoverNotification.cs" />
|
||||
<Compile Include="Notification\PushbulletNotification.cs" />
|
||||
<Compile Include="PlexAvailabilityChecker.cs" />
|
||||
<Compile Include="PlexType.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UpdateInterval.cs" />
|
||||
</ItemGroup>
|
||||
|
|
35
PlexRequests.Services/PlexType.cs
Normal file
35
PlexRequests.Services/PlexType.cs
Normal 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
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ namespace PlexRequests.Services
|
|||
{
|
||||
public class UpdateInterval : IIntervals
|
||||
{
|
||||
public TimeSpan Notification => TimeSpan.FromMinutes(5);
|
||||
public TimeSpan Notification => TimeSpan.FromMinutes(10);
|
||||
|
||||
}
|
||||
}
|
|
@ -27,12 +27,11 @@
|
|||
using System;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Mono.Data.Sqlite;
|
||||
|
||||
using NLog;
|
||||
using PlexRequests.Helpers;
|
||||
using PlexRequests.Store.Repository;
|
||||
|
||||
namespace PlexRequests.Store
|
||||
{
|
||||
|
@ -44,12 +43,14 @@ namespace PlexRequests.Store
|
|||
Factory = provider;
|
||||
}
|
||||
|
||||
private SqliteFactory Factory { get; set; }
|
||||
private SqliteFactory Factory { get; }
|
||||
private string CurrentPath =>Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, DbFile);
|
||||
|
||||
public virtual bool CheckDb()
|
||||
{
|
||||
Log.Trace("Checking DB");
|
||||
if (!File.Exists(DbFile))
|
||||
Console.WriteLine("Location of the database: {0}",CurrentPath);
|
||||
if (!File.Exists(CurrentPath))
|
||||
{
|
||||
Log.Trace("DB doesn't exist, creating a new one");
|
||||
CreateDatabase();
|
||||
|
@ -72,7 +73,7 @@ namespace PlexRequests.Store
|
|||
{
|
||||
throw new SqliteException("Factory returned null");
|
||||
}
|
||||
fact.ConnectionString = "Data Source=" + DbFile;
|
||||
fact.ConnectionString = "Data Source=" + CurrentPath;
|
||||
return fact;
|
||||
}
|
||||
|
||||
|
@ -83,14 +84,16 @@ namespace PlexRequests.Store
|
|||
{
|
||||
try
|
||||
{
|
||||
using (File.Create(DbFile))
|
||||
using (File.Create(CurrentPath))
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,5 +34,6 @@ namespace PlexRequests.Store.Models
|
|||
public int ProviderId { get; set; }
|
||||
public byte[] Content { get; set; }
|
||||
public RequestType Type { get; set; }
|
||||
public string MusicId { get; set; }
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@
|
|||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
|
|
|
@ -38,12 +38,10 @@ namespace PlexRequests.Store.Repository
|
|||
{
|
||||
private ICacheProvider Cache { get; }
|
||||
|
||||
private string TypeName { get; }
|
||||
public RequestJsonRepository(ISqliteConfiguration config, ICacheProvider cacheProvider)
|
||||
{
|
||||
Db = config;
|
||||
Cache = cacheProvider;
|
||||
TypeName = typeof(RequestJsonRepository).Name;
|
||||
}
|
||||
|
||||
private ISqliteConfiguration Db { get; }
|
||||
|
@ -60,7 +58,7 @@ namespace PlexRequests.Store.Repository
|
|||
|
||||
public IEnumerable<RequestBlobs> GetAll()
|
||||
{
|
||||
var key = TypeName + "GetAll";
|
||||
var key = "GetAll";
|
||||
var item = Cache.GetOrSet(key, () =>
|
||||
{
|
||||
using (var con = Db.DbConnection())
|
||||
|
@ -74,7 +72,7 @@ namespace PlexRequests.Store.Repository
|
|||
|
||||
public RequestBlobs Get(int id)
|
||||
{
|
||||
var key = TypeName + "Get" + id;
|
||||
var key = "Get" + id;
|
||||
var item = Cache.GetOrSet(key, () =>
|
||||
{
|
||||
using (var con = Db.DbConnection())
|
||||
|
@ -107,7 +105,7 @@ namespace PlexRequests.Store.Repository
|
|||
private void ResetCache()
|
||||
{
|
||||
Cache.Remove("Get");
|
||||
Cache.Remove(TypeName + "GetAll");
|
||||
Cache.Remove("GetAll");
|
||||
}
|
||||
|
||||
public bool UpdateAll(IEnumerable<RequestBlobs> entity)
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using Dapper.Contrib.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PlexRequests.Store
|
||||
{
|
||||
[Table("Requested")]
|
||||
public class RequestedModel : Entity
|
||||
{
|
||||
public RequestedModel()
|
||||
{
|
||||
RequestedUsers = new List<string>();
|
||||
}
|
||||
|
||||
// ReSharper disable once IdentifierTypo
|
||||
public int ProviderId { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
|
@ -18,7 +24,10 @@ namespace PlexRequests.Store
|
|||
public RequestType Type { get; set; }
|
||||
public string Status { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
|
||||
[Obsolete("Use RequestedUsers")]
|
||||
public string RequestedBy { get; set; }
|
||||
|
||||
public DateTime RequestedDate { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public IssueState Issues { get; set; }
|
||||
|
@ -27,12 +36,50 @@ namespace PlexRequests.Store
|
|||
public int[] SeasonList { get; set; }
|
||||
public int SeasonCount { get; set; }
|
||||
public string SeasonsRequested { get; set; }
|
||||
public string MusicBrainzId { get; set; }
|
||||
public List<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
|
||||
{
|
||||
Movie,
|
||||
TvShow
|
||||
TvShow,
|
||||
Album
|
||||
}
|
||||
|
||||
public enum IssueState
|
||||
|
|
|
@ -25,7 +25,8 @@ CREATE TABLE IF NOT EXISTS RequestBlobs
|
|||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ProviderId INTEGER NOT NULL,
|
||||
Type INTEGER NOT NULL,
|
||||
Content BLOB NOT NULL
|
||||
Content BLOB NOT NULL,
|
||||
MusicId TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS RequestBlobs_Id ON RequestBlobs (Id);
|
||||
|
||||
|
@ -40,3 +41,9 @@ CREATE TABLE IF NOT EXISTS Logs
|
|||
Exception varchar(100) NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS Logs_Id ON Logs (Id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DBInfo
|
||||
(
|
||||
SchemaVersion INTEGER
|
||||
|
||||
);
|
|
@ -25,7 +25,7 @@
|
|||
// ***********************************************************************
|
||||
#endregion
|
||||
using System.Data;
|
||||
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using Dapper.Contrib.Extensions;
|
||||
|
||||
|
@ -44,6 +44,57 @@ namespace PlexRequests.Store
|
|||
connection.Close();
|
||||
}
|
||||
|
||||
public static void AlterTable(IDbConnection connection, string tableName, string alterType, string newColumn, bool isNullable, string dataType)
|
||||
{
|
||||
connection.Open();
|
||||
var result = connection.Query<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")]
|
||||
public class SqliteMasterTable
|
||||
{
|
||||
|
@ -54,5 +105,17 @@ namespace PlexRequests.Store
|
|||
public long rootpage { get; set; }
|
||||
public string sql { get; set; }
|
||||
}
|
||||
|
||||
[Table("table_info")]
|
||||
public class TableInfo
|
||||
{
|
||||
public int cid { get; set; }
|
||||
public string name { get; set; }
|
||||
public int notnull { get; set; }
|
||||
public string dflt_value { get; set; }
|
||||
public int pk { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ using PlexRequests.Store.Models;
|
|||
using PlexRequests.Store.Repository;
|
||||
using PlexRequests.UI.Models;
|
||||
using PlexRequests.UI.Modules;
|
||||
using PlexRequests.Helpers;
|
||||
|
||||
namespace PlexRequests.UI.Tests
|
||||
{
|
||||
|
@ -59,6 +60,7 @@ namespace PlexRequests.UI.Tests
|
|||
private Mock<ISettingsService<EmailNotificationSettings>> EmailMock { get; set; }
|
||||
private Mock<ISettingsService<PushbulletNotificationSettings>> PushbulletSettings { get; set; }
|
||||
private Mock<ISettingsService<PushoverNotificationSettings>> PushoverSettings { get; set; }
|
||||
private Mock<ISettingsService<HeadphonesSettings>> HeadphonesSettings { get; set; }
|
||||
private Mock<IPlexApi> PlexMock { get; set; }
|
||||
private Mock<ISonarrApi> SonarrApiMock { get; set; }
|
||||
private Mock<IPushbulletApi> PushbulletApi { get; set; }
|
||||
|
@ -66,6 +68,7 @@ namespace PlexRequests.UI.Tests
|
|||
private Mock<ICouchPotatoApi> CpApi { get; set; }
|
||||
private Mock<IRepository<LogEntity>> LogRepo { get; set; }
|
||||
private Mock<INotificationService> NotificationService { get; set; }
|
||||
private Mock<ICacheProvider> Cache { get; set; }
|
||||
|
||||
private ConfigurableBootstrapper Bootstrapper { get; set; }
|
||||
|
||||
|
@ -94,6 +97,8 @@ namespace PlexRequests.UI.Tests
|
|||
PushoverSettings = new Mock<ISettingsService<PushoverNotificationSettings>>();
|
||||
PushoverApi = new Mock<IPushoverApi>();
|
||||
NotificationService = new Mock<INotificationService>();
|
||||
HeadphonesSettings = new Mock<ISettingsService<HeadphonesSettings>>();
|
||||
Cache = new Mock<ICacheProvider>();
|
||||
|
||||
Bootstrapper = new ConfigurableBootstrapper(with =>
|
||||
{
|
||||
|
@ -114,6 +119,8 @@ namespace PlexRequests.UI.Tests
|
|||
with.Dependency(PushoverSettings.Object);
|
||||
with.Dependency(PushoverApi.Object);
|
||||
with.Dependency(NotificationService.Object);
|
||||
with.Dependency(HeadphonesSettings.Object);
|
||||
with.Dependencies(Cache.Object);
|
||||
with.RootPathProvider<TestRootPathProvider>();
|
||||
with.RequestStartup((container, pipelines, context) =>
|
||||
{
|
||||
|
|
|
@ -113,6 +113,10 @@
|
|||
<Project>{DD7DC444-D3BF-4027-8AB9-EFC71F5EC581}</Project>
|
||||
<Name>PlexRequests.Core</Name>
|
||||
</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">
|
||||
<Project>{566EFA49-68F8-4716-9693-A6B3F2624DEA}</Project>
|
||||
<Name>PlexRequests.Services</Name>
|
||||
|
|
|
@ -76,9 +76,9 @@ namespace PlexRequests.UI
|
|||
container.Register<ISettingsService<EmailNotificationSettings>, SettingsServiceV2<EmailNotificationSettings>>();
|
||||
container.Register<ISettingsService<PushbulletNotificationSettings>, SettingsServiceV2<PushbulletNotificationSettings>>();
|
||||
container.Register<ISettingsService<PushoverNotificationSettings>, SettingsServiceV2<PushoverNotificationSettings>>();
|
||||
container.Register<ISettingsService<HeadphonesSettings>, SettingsServiceV2<HeadphonesSettings>>();
|
||||
|
||||
// Repo's
|
||||
container.Register<IRepository<RequestedModel>, GenericRepository<RequestedModel>>();
|
||||
container.Register<IRepository<LogEntity>, GenericRepository<LogEntity>>();
|
||||
container.Register<IRequestService, JsonRequestService>();
|
||||
container.Register<ISettingsRepository, SettingsJsonRepository>();
|
||||
|
@ -95,19 +95,21 @@ namespace PlexRequests.UI
|
|||
container.Register<ISickRageApi, SickrageApi>();
|
||||
container.Register<ISonarrApi, SonarrApi>();
|
||||
container.Register<IPlexApi, PlexApi>();
|
||||
container.Register<IMusicBrainzApi, MusicBrainzApi>();
|
||||
container.Register<IHeadphonesApi, HeadphonesApi>();
|
||||
|
||||
// NotificationService
|
||||
container.Register<INotificationService, NotificationService>().AsSingleton();
|
||||
|
||||
SubscribeAllObservers(container);
|
||||
base.ConfigureRequestContainer(container, context);
|
||||
|
||||
TaskManager.TaskFactory = new PlexTaskFactory();
|
||||
TaskManager.Initialize(new PlexRegistry());
|
||||
}
|
||||
|
||||
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
|
||||
{
|
||||
TaskManager.TaskFactory = new PlexTaskFactory();
|
||||
TaskManager.Initialize(new PlexRegistry());
|
||||
|
||||
CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default);
|
||||
|
||||
StaticConfiguration.DisableErrorTraces = false;
|
||||
|
@ -123,6 +125,7 @@ namespace PlexRequests.UI
|
|||
|
||||
FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
|
||||
|
||||
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
|
||||
ServicePointManager.ServerCertificateValidationCallback +=
|
||||
(sender, certificate, chain, sslPolicyErrors) => true;
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
|
||||
.form-control-custom {
|
||||
background-color: #4e5d6c !important;
|
||||
color: white !important; }
|
||||
color: white !important;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 0 !important; }
|
||||
|
||||
h1 {
|
||||
font-size: 3.5rem !important;
|
||||
|
@ -40,6 +42,22 @@ label {
|
|||
margin-bottom: 0.5rem !important;
|
||||
font-size: 16px !important; }
|
||||
|
||||
.nav-tabs > li.active > a,
|
||||
.nav-tabs > li.active > a:hover,
|
||||
.nav-tabs > li.active > a:focus {
|
||||
background: #4e5d6c; }
|
||||
|
||||
.navbar .nav a .fa,
|
||||
.dropdown-menu a .fa {
|
||||
font-size: 130%;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 5px; }
|
||||
|
||||
.dropdown-menu a .fa {
|
||||
top: 2px; }
|
||||
|
||||
.btn-danger-outline {
|
||||
color: #d9534f !important;
|
||||
background-color: transparent;
|
||||
|
@ -126,3 +144,78 @@ label {
|
|||
#tvList .mix {
|
||||
display: none; }
|
||||
|
||||
.scroll-top-wrapper {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
z-index: 99999999;
|
||||
background-color: #4e5d6c;
|
||||
color: #eeeeee;
|
||||
width: 50px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
padding-top: 2px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
-webkit-transition: all 0.5s ease-in-out;
|
||||
-moz-transition: all 0.5s ease-in-out;
|
||||
-ms-transition: all 0.5s ease-in-out;
|
||||
-o-transition: all 0.5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out; }
|
||||
|
||||
.scroll-top-wrapper:hover {
|
||||
background-color: #637689; }
|
||||
|
||||
.scroll-top-wrapper.show {
|
||||
visibility: visible;
|
||||
cursor: pointer;
|
||||
opacity: 1.0; }
|
||||
|
||||
.scroll-top-wrapper i.fa {
|
||||
line-height: inherit; }
|
||||
|
||||
.no-search-results {
|
||||
text-align: center; }
|
||||
|
||||
.no-search-results .no-search-results-icon {
|
||||
font-size: 10em;
|
||||
color: #4e5d6c; }
|
||||
|
||||
.no-search-results .no-search-results-text {
|
||||
margin: 20px 0;
|
||||
color: #ccc; }
|
||||
|
||||
.form-control-search {
|
||||
padding: 25px 105px 25px 16px; }
|
||||
|
||||
.form-control-withbuttons {
|
||||
padding-right: 105px; }
|
||||
|
||||
.input-group-addon .btn-group {
|
||||
position: absolute;
|
||||
right: 45px;
|
||||
z-index: 3;
|
||||
top: 13px;
|
||||
box-shadow: 0 0 0; }
|
||||
|
||||
.input-group-addon .btn-group .btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.7) !important;
|
||||
padding: 3px 12px;
|
||||
color: rgba(255, 255, 255, 0.7) !important; }
|
||||
|
||||
.btn-split .btn {
|
||||
border-radius: 0 !important; }
|
||||
|
||||
.btn-split .btn:not(.dropdown-toggle) {
|
||||
border-radius: 0.25rem 0 0 0.25rem !important; }
|
||||
|
||||
.btn-split .btn.dropdown-toggle {
|
||||
border-radius: 0 0.25rem 0.25rem 0 !important;
|
||||
padding: 12px 8px; }
|
||||
|
||||
|
|
2
PlexRequests.UI/Content/custom.min.css
vendored
2
PlexRequests.UI/Content/custom.min.css
vendored
|
@ -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;}
|
|
@ -1,11 +1,14 @@
|
|||
$form-color: #4e5d6c;
|
||||
$form-color-lighter: #637689;
|
||||
$primary-colour: #df691a;
|
||||
$primary-colour-outline: #ff761b;
|
||||
$info-colour: #5bc0de;
|
||||
$warning-colour: #f0ad4e;
|
||||
$danger-colour: #d9534f;
|
||||
$success-colour: #5cb85c;
|
||||
$i:!important;
|
||||
$i:
|
||||
!important
|
||||
;
|
||||
|
||||
@media (min-width: 768px ) {
|
||||
.row {
|
||||
|
@ -42,6 +45,8 @@ $i:!important;
|
|||
.form-control-custom {
|
||||
background-color: $form-color $i;
|
||||
color: white $i;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 0 !important;
|
||||
}
|
||||
|
||||
|
||||
|
@ -65,6 +70,25 @@ label {
|
|||
font-size: 16px $i;
|
||||
}
|
||||
|
||||
.nav-tabs > li.active > a,
|
||||
.nav-tabs > li.active > a:hover,
|
||||
.nav-tabs > li.active > a:focus {
|
||||
background: #4e5d6c;
|
||||
}
|
||||
|
||||
.navbar .nav a .fa,
|
||||
.dropdown-menu a .fa {
|
||||
font-size: 130%;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.dropdown-menu a .fa {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.btn-danger-outline {
|
||||
color: $danger-colour $i;
|
||||
background-color: transparent;
|
||||
|
@ -159,6 +183,99 @@ label {
|
|||
#movieList .mix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tvList .mix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
$border-radius: 10px;
|
||||
|
||||
.scroll-top-wrapper {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
z-index: 99999999;
|
||||
background-color: $form-color;
|
||||
color: #eeeeee;
|
||||
width: 50px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
padding-top: 2px;
|
||||
border-top-left-radius: $border-radius;
|
||||
border-top-right-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
-webkit-transition: all 0.5s ease-in-out;
|
||||
-moz-transition: all 0.5s ease-in-out;
|
||||
-ms-transition: all 0.5s ease-in-out;
|
||||
-o-transition: all 0.5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.scroll-top-wrapper:hover {
|
||||
background-color: $form-color-lighter;
|
||||
}
|
||||
|
||||
.scroll-top-wrapper.show {
|
||||
visibility: visible;
|
||||
cursor: pointer;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.scroll-top-wrapper i.fa {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
|
||||
.no-search-results {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-search-results .no-search-results-icon {
|
||||
font-size: 10em;
|
||||
color: $form-color;
|
||||
}
|
||||
|
||||
.no-search-results .no-search-results-text {
|
||||
margin: 20px 0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.form-control-search {
|
||||
padding: 25px 105px 25px 16px;
|
||||
}
|
||||
|
||||
.form-control-withbuttons {
|
||||
padding-right: 105px;
|
||||
}
|
||||
|
||||
.input-group-addon .btn-group {
|
||||
position: absolute;
|
||||
right: 45px;
|
||||
z-index: 3;
|
||||
top: 13px;
|
||||
box-shadow: 0 0 0;
|
||||
}
|
||||
|
||||
.input-group-addon .btn-group .btn {
|
||||
border: 1px solid rgba(255,255,255,.7) !important;
|
||||
padding: 3px 12px;
|
||||
color: rgba(255,255,255,.7) !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
822
PlexRequests.UI/Content/moment.min.es5.js
Normal file
822
PlexRequests.UI/Content/moment.min.es5.js
Normal 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
|
||||
|
6
PlexRequests.UI/Content/moment.min.es5.min.js
vendored
Normal file
6
PlexRequests.UI/Content/moment.min.es5.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
PlexRequests.UI/Content/moment.min.js
vendored
Normal file
10
PlexRequests.UI/Content/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,76 +1,155 @@
|
|||
Handlebars.registerHelper('if_eq', function (a, b, opts) {
|
||||
if (a == b)
|
||||
return opts.fn(this);
|
||||
return !opts ? null : opts.fn(this);
|
||||
else
|
||||
return opts.inverse(this);
|
||||
return !opts ? null : opts.inverse(this);
|
||||
});
|
||||
|
||||
var searchSource = $("#search-template").html();
|
||||
var albumSource = $("#album-template").html();
|
||||
var searchTemplate = Handlebars.compile(searchSource);
|
||||
var albumTemplate = Handlebars.compile(albumSource);
|
||||
var movieTimer = 0;
|
||||
var tvimer = 0;
|
||||
|
||||
movieLoad();
|
||||
tvLoad();
|
||||
var mixItUpDefault = {
|
||||
animation: { enable: true },
|
||||
load: {
|
||||
filter: 'all',
|
||||
sort: 'requestorder:desc'
|
||||
},
|
||||
layout: {
|
||||
display: 'block'
|
||||
},
|
||||
callbacks: {
|
||||
onMixStart: function (state, futureState) {
|
||||
$('.mix', this).removeAttr('data-bound').removeData('bound'); // fix for animation issues in other tabs
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initLoad();
|
||||
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
var target = $(e.target).attr('href');
|
||||
var activeState = "";
|
||||
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 ($('#tvList').mixItUp('isLoaded')) {
|
||||
activeState = $('#tvList').mixItUp('getState');
|
||||
$('#tvList').mixItUp('destroy');
|
||||
$('#approveMovies').show();
|
||||
if ($tvl.mixItUp('isLoaded')) {
|
||||
activeState = $tvl.mixItUp('getState');
|
||||
$tvl.mixItUp('destroy');
|
||||
}
|
||||
if (!$('#movieList').mixItUp('isLoaded')) {
|
||||
$('#movieList').mixItUp({
|
||||
load: {
|
||||
filter: activeState.activeFilter || 'all',
|
||||
sort: activeState.activeSort || 'default:asc'
|
||||
},
|
||||
layout: {
|
||||
display: 'block'
|
||||
if ($musicL.mixItUp('isLoaded')) {
|
||||
activeState = $musicL.mixItUp('getState');
|
||||
$musicL.mixItUp('destroy');
|
||||
}
|
||||
});
|
||||
if ($ml.mixItUp('isLoaded')) $ml.mixItUp('destroy');
|
||||
$ml.mixItUp(mixItUpConfig(activeState)); // init or reinit
|
||||
}
|
||||
|
||||
if (target === "#MusicTab") {
|
||||
$('#approveMusic').show();
|
||||
if ($tvl.mixItUp('isLoaded')) {
|
||||
activeState = $tvl.mixItUp('getState');
|
||||
$tvl.mixItUp('destroy');
|
||||
}
|
||||
if ($ml.mixItUp('isLoaded')) {
|
||||
activeState = $ml.mixItUp('getState');
|
||||
$ml.mixItUp('destroy');
|
||||
}
|
||||
if ($musicL.mixItUp('isLoaded')) $musicL.mixItUp('destroy');
|
||||
$musicL.mixItUp(mixItUpConfig(activeState)); // init or reinit
|
||||
}
|
||||
});
|
||||
|
||||
// Approve all
|
||||
$('#approveAll').click(function () {
|
||||
$('#approveMovies').click(function (e) {
|
||||
e.preventDefault();
|
||||
var buttonId = e.target.id;
|
||||
var origHtml = $(this).html();
|
||||
|
||||
if ($('#' + buttonId).text() === " Loading...") {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingButton(buttonId, "success");
|
||||
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: '/approval/approveall',
|
||||
url: '/approval/approveallmovies',
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
if (checkJsonResponse(response)) {
|
||||
generateNotify("Success! All requests approved!", "success");
|
||||
generateNotify("Success! All Movie requests approved!", "success");
|
||||
movieLoad();
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
console.log(e);
|
||||
generateNotify("Something went wrong!", "danger");
|
||||
},
|
||||
complete: function (e) {
|
||||
finishLoading(buttonId, "success", origHtml);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#approveTVShows').click(function (e) {
|
||||
e.preventDefault();
|
||||
var buttonId = e.target.id;
|
||||
var origHtml = $(this).html();
|
||||
|
||||
if ($('#' + buttonId).text() === " Loading...") {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingButton(buttonId, "success");
|
||||
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: '/approval/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
|
||||
|
@ -211,37 +290,44 @@ $(document).on("click", ".delete", function (e) {
|
|||
// Approve single request
|
||||
$(document).on("click", ".approve", function (e) {
|
||||
e.preventDefault();
|
||||
var buttonId = e.target.id;
|
||||
var $form = $('#approve' + buttonId);
|
||||
var $this = $(this);
|
||||
var $form = $this.parents('form').first();
|
||||
|
||||
if ($('#' + buttonId).text() === " Loading...") {
|
||||
if ($this.text() === " Loading...") {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingButton(buttonId, "success");
|
||||
loadingButton($this.attr('id'), "success");
|
||||
|
||||
$.ajax({
|
||||
type: $form.prop('method'),
|
||||
url: $form.prop('action'),
|
||||
data: $form.serialize(),
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
approveRequest($form, null, function () {
|
||||
$("#" + $this.attr('id') + "notapproved").prop("class", "fa fa-check");
|
||||
|
||||
if (checkJsonResponse(response)) {
|
||||
if (response.message) {
|
||||
generateNotify(response.message, "success");
|
||||
} else {
|
||||
generateNotify("Success! Request Approved.", "success");
|
||||
var $group = $this.parent('.btn-split');
|
||||
if ($group.length > 0) {
|
||||
$group.remove();
|
||||
}
|
||||
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();
|
||||
$("#" + buttonId + "notapproved").prop("class", "fa fa-check");
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
console.log(e);
|
||||
generateNotify("Something went wrong!", "danger");
|
||||
}
|
||||
loadingButton($button.attr('id'), "success");
|
||||
|
||||
approveRequest($form, qualityId, function () {
|
||||
$("#" + $button.attr('id') + "notapproved").prop("class", "fa fa-check");
|
||||
$this.parents('.btn-split').remove();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -315,36 +401,119 @@ $(document).on("click", ".change", function (e) {
|
|||
|
||||
});
|
||||
|
||||
function movieLoad() {
|
||||
$("#movieList").html("");
|
||||
function approveRequest($form, qualityId, successCallback) {
|
||||
|
||||
$.ajax("/requests/movies/").success(function (results) {
|
||||
results.forEach(function (result) {
|
||||
var context = buildRequestContext(result, "movie");
|
||||
var formData = $form.serialize();
|
||||
if (qualityId) formData += ("&qualityId=" + qualityId);
|
||||
|
||||
var html = searchTemplate(context);
|
||||
$("#movieList").append(html);
|
||||
});
|
||||
$('#movieList').mixItUp({
|
||||
layout: {
|
||||
display: 'block'
|
||||
$.ajax({
|
||||
type: $form.prop('method'),
|
||||
url: $form.prop('action'),
|
||||
data: formData,
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
|
||||
if (checkJsonResponse(response)) {
|
||||
if (response.message) {
|
||||
generateNotify(response.message, "success");
|
||||
} else {
|
||||
generateNotify("Success! Request Approved.", "success");
|
||||
}
|
||||
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
},
|
||||
load: {
|
||||
filter: 'all'
|
||||
error: function (e) {
|
||||
console.log(e);
|
||||
generateNotify("Something went wrong!", "danger");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mixItUpConfig(activeState) {
|
||||
var conf = mixItUpDefault;
|
||||
|
||||
if (activeState) {
|
||||
if (activeState.activeFilter) conf['load']['filter'] = activeState.activeFilter;
|
||||
if (activeState.activeSort) conf['load']['sort'] = activeState.activeSort;
|
||||
}
|
||||
return conf;
|
||||
};
|
||||
|
||||
function initLoad() {
|
||||
movieLoad();
|
||||
tvLoad();
|
||||
albumLoad();
|
||||
}
|
||||
|
||||
function movieLoad() {
|
||||
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() {
|
||||
$("#tvList").html("");
|
||||
var $tvl = $('#tvList');
|
||||
if ($tvl.mixItUp('isLoaded')) {
|
||||
activeState = $tvl.mixItUp('getState');
|
||||
$tvl.mixItUp('destroy');
|
||||
}
|
||||
$tvl.html("");
|
||||
|
||||
$.ajax("/requests/tvshows/").success(function (results) {
|
||||
if (results.length > 0) {
|
||||
results.forEach(function (result) {
|
||||
var context = buildRequestContext(result, "tv");
|
||||
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,
|
||||
type: type,
|
||||
status: result.status,
|
||||
releaseDate: result.releaseDate,
|
||||
releaseDate: Humanize(result.releaseDate),
|
||||
releaseDateTicks: result.releaseDateTicks,
|
||||
approved: result.approved,
|
||||
requestedBy: result.requestedBy,
|
||||
requestedDate: result.requestedDate,
|
||||
requestedUsers: result.requestedUsers ? result.requestedUsers.join(', ') : '',
|
||||
requestedDate: Humanize(result.requestedDate),
|
||||
requestedDateTicks: result.requestedDateTicks,
|
||||
available: result.available,
|
||||
admin: result.admin,
|
||||
issues: result.issues,
|
||||
|
@ -369,20 +540,13 @@ function buildRequestContext(result, type) {
|
|||
requestId: result.id,
|
||||
adminNote: result.adminNotes,
|
||||
imdb: result.imdbId,
|
||||
seriesRequested: result.tvSeriesRequestType
|
||||
seriesRequested: result.tvSeriesRequestType,
|
||||
coverArtUrl: result.coverArtUrl,
|
||||
qualities: result.qualities,
|
||||
hasQualities: result.qualities && result.qualities.length > 0,
|
||||
artist: result.artistName
|
||||
};
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function startFilter(elementId) {
|
||||
$('#'+element).mixItUp({
|
||||
load: {
|
||||
filter: activeState.activeFilter || 'all',
|
||||
sort: activeState.activeSort || 'default:asc'
|
||||
},
|
||||
layout: {
|
||||
display: 'block'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -5,28 +5,53 @@
|
|||
return opts.inverse(this);
|
||||
});
|
||||
|
||||
$(function () {
|
||||
|
||||
var searchSource = $("#search-template").html();
|
||||
var musicSource = $("#music-template").html();
|
||||
var searchTemplate = Handlebars.compile(searchSource);
|
||||
var movieTimer = 0;
|
||||
var tvimer = 0;
|
||||
var musicTemplate = Handlebars.compile(musicSource);
|
||||
|
||||
var searchTimer = 0;
|
||||
|
||||
// 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
|
||||
$("#movieSearchContent").on("input", function () {
|
||||
if (movieTimer) {
|
||||
clearTimeout(movieTimer);
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
}
|
||||
$('#movieSearchButton').attr("class", "fa fa-spinner fa-spin");
|
||||
movieTimer = setTimeout(movieSearch, 400);
|
||||
searchTimer = setTimeout(movieSearch, 400);
|
||||
|
||||
});
|
||||
|
||||
$('#moviesComingSoon').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
moviesComingSoon();
|
||||
});
|
||||
|
||||
$('#moviesInTheaters').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
moviesInTheaters();
|
||||
});
|
||||
|
||||
// Type in TV search
|
||||
$("#tvSearchContent").on("input", function () {
|
||||
if (tvimer) {
|
||||
clearTimeout(tvimer);
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
}
|
||||
$('#tvSearchButton').attr("class", "fa fa-spinner fa-spin");
|
||||
tvimer = setTimeout(tvSearch, 400);
|
||||
searchTimer = setTimeout(tvSearch, 400);
|
||||
});
|
||||
|
||||
// Click TV dropdown option
|
||||
|
@ -60,6 +85,16 @@ $(document).on("click", ".dropdownTv", function (e) {
|
|||
sendRequestAjax(data, type, url, buttonId);
|
||||
});
|
||||
|
||||
// Search Music
|
||||
$("#musicSearchContent").on("input", function () {
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
}
|
||||
$('#musicSearchButton').attr("class", "fa fa-spinner fa-spin");
|
||||
searchTimer = setTimeout(musicSearch, 400);
|
||||
|
||||
});
|
||||
|
||||
// Click Request for movie
|
||||
$(document).on("click", ".requestMovie", function (e) {
|
||||
e.preventDefault();
|
||||
|
@ -82,6 +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) {
|
||||
$.ajax({
|
||||
type: type,
|
||||
|
@ -91,7 +153,7 @@ function sendRequestAjax(data, type, url, buttonId) {
|
|||
success: function (response) {
|
||||
console.log(response);
|
||||
if (response.result === true) {
|
||||
generateNotify("Success!", "success");
|
||||
generateNotify(response.message || "Success!", "success");
|
||||
|
||||
$('#' + buttonId).html("<i class='fa fa-check'></i> Requested");
|
||||
$('#' + buttonId).removeClass("btn-primary-outline");
|
||||
|
@ -112,10 +174,23 @@ function sendRequestAjax(data, type, url, buttonId) {
|
|||
}
|
||||
|
||||
function movieSearch() {
|
||||
$("#movieList").html("");
|
||||
var query = $("#movieSearchContent").val();
|
||||
getMovies("/search/movie/" + query);
|
||||
}
|
||||
|
||||
$.ajax("/search/movie/" + query).success(function (results) {
|
||||
function moviesComingSoon() {
|
||||
getMovies("/search/movie/upcoming");
|
||||
}
|
||||
|
||||
function moviesInTheaters() {
|
||||
getMovies("/search/movie/playing");
|
||||
}
|
||||
|
||||
function getMovies(url) {
|
||||
$("#movieList").html("");
|
||||
|
||||
|
||||
$.ajax(url).success(function (results) {
|
||||
if (results.length > 0) {
|
||||
results.forEach(function (result) {
|
||||
var context = buildMovieContext(result);
|
||||
|
@ -124,15 +199,22 @@ function movieSearch() {
|
|||
$("#movieList").append(html);
|
||||
});
|
||||
}
|
||||
else {
|
||||
$("#movieList").html(noResultsHtml);
|
||||
}
|
||||
$('#movieSearchButton').attr("class", "fa fa-search");
|
||||
});
|
||||
};
|
||||
|
||||
function tvSearch() {
|
||||
$("#tvList").html("");
|
||||
var query = $("#tvSearchContent").val();
|
||||
getTvShows("/search/tv/" + query);
|
||||
}
|
||||
|
||||
$.ajax("/search/tv/" + query).success(function (results) {
|
||||
function getTvShows(url) {
|
||||
$("#tvList").html("");
|
||||
|
||||
$.ajax(url).success(function (results) {
|
||||
if (results.length > 0) {
|
||||
results.forEach(function (result) {
|
||||
var context = buildTvShowContext(result);
|
||||
|
@ -140,10 +222,45 @@ function tvSearch() {
|
|||
$("#tvList").append(html);
|
||||
});
|
||||
}
|
||||
else {
|
||||
$("#tvList").html(noResultsHtml);
|
||||
}
|
||||
$('#tvSearchButton').attr("class", "fa fa-search");
|
||||
});
|
||||
};
|
||||
|
||||
function musicSearch() {
|
||||
var query = $("#musicSearchContent").val();
|
||||
getMusic("/search/music/" + query);
|
||||
}
|
||||
|
||||
function getMusic(url) {
|
||||
$("#musicList").html("");
|
||||
|
||||
$.ajax(url).success(function (results) {
|
||||
if (results.length > 0) {
|
||||
results.forEach(function (result) {
|
||||
var context = buildMusicContext(result);
|
||||
|
||||
var html = musicTemplate(context);
|
||||
$("#musicList").append(html);
|
||||
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) {
|
||||
var date = new Date(result.releaseDate);
|
||||
|
@ -177,3 +294,23 @@ function buildTvShowContext(result) {
|
|||
};
|
||||
return context;
|
||||
}
|
||||
|
||||
function buildMusicContext(result) {
|
||||
|
||||
var context = {
|
||||
id: result.id,
|
||||
title: result.title,
|
||||
overview: result.overview,
|
||||
year: result.releaseDate,
|
||||
type: "album",
|
||||
trackCount: result.trackCount,
|
||||
coverArtUrl: result.coverArtUrl,
|
||||
artist: result.artist,
|
||||
releaseType: result.releaseType,
|
||||
country: result.country
|
||||
};
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -1,4 +1,20 @@
|
|||
function generateNotify(message, type) {
|
||||
String.prototype.format = String.prototype.f = function () {
|
||||
var s = this,
|
||||
i = arguments.length;
|
||||
|
||||
while (i--) {
|
||||
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function Humanize(date) {
|
||||
var mNow = moment();
|
||||
var mDate = moment(date).local();
|
||||
return moment.duration(mNow - mDate).humanize() + (mNow.isBefore(mDate) ? ' from now' : ' ago');
|
||||
}
|
||||
|
||||
function generateNotify(message, type) {
|
||||
// type = danger, warning, info, successs
|
||||
$.notify({
|
||||
// options
|
||||
|
@ -25,13 +41,28 @@ function checkJsonResponse(response) {
|
|||
}
|
||||
|
||||
function loadingButton(elementId, originalCss) {
|
||||
$('#' + elementId).removeClass("btn-" + originalCss + "-outline");
|
||||
$('#' + elementId).addClass("btn-primary-outline");
|
||||
$('#' + elementId).html("<i class='fa fa-spinner fa-spin'></i> Loading...");
|
||||
var $element = $('#' + elementId);
|
||||
$element.removeClass("btn-" + originalCss + "-outline").addClass("btn-primary-outline").addClass('disabled').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) {
|
||||
$('#' + elementId).removeClass("btn-primary-outline");
|
||||
$('#' + elementId).addClass("btn-" + originalCss + "-outline");
|
||||
$('#' + elementId).html(html);
|
||||
var $element = $('#' + elementId);
|
||||
$element.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline").html(html);
|
||||
|
||||
// handle split-buttons
|
||||
var $dropdown = $element.next('.dropdown-toggle')
|
||||
if ($dropdown.length > 0) {
|
||||
$dropdown.removeClass("btn-primary-outline").removeClass('disabled').addClass("btn-" + originalCss + "-outline");
|
||||
}
|
||||
}
|
||||
|
||||
var noResultsHtml = "<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>";
|
176
PlexRequests.UI/Helpers/HeadphonesSender.cs
Normal file
176
PlexRequests.UI/Helpers/HeadphonesSender.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
22
PlexRequests.UI/Helpers/StringHelper.cs
Normal file
22
PlexRequests.UI/Helpers/StringHelper.cs
Normal 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 ");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,17 +24,14 @@
|
|||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// ************************************************************************/
|
||||
#endregion
|
||||
|
||||
using Nancy;
|
||||
using NLog;
|
||||
using PlexRequests.Api.Interfaces;
|
||||
using PlexRequests.Api.Models.SickRage;
|
||||
using PlexRequests.Api.Models.Sonarr;
|
||||
using PlexRequests.Core;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
using PlexRequests.Helpers;
|
||||
using PlexRequests.Store;
|
||||
using PlexRequests.UI.Models;
|
||||
using System.Linq;
|
||||
|
||||
namespace PlexRequests.UI.Helpers
|
||||
{
|
||||
|
@ -50,9 +47,19 @@ namespace PlexRequests.UI.Helpers
|
|||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model)
|
||||
{
|
||||
return SendToSonarr(sonarrSettings, model, string.Empty);
|
||||
}
|
||||
|
||||
public SonarrAddSeries SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model, string qualityId)
|
||||
{
|
||||
int qualityProfile;
|
||||
|
||||
if (!string.IsNullOrEmpty(qualityId) || !int.TryParse(qualityId, out qualityProfile)) // try to parse the passed in quality, otherwise use the settings default quality
|
||||
{
|
||||
int.TryParse(sonarrSettings.QualityProfile, out qualityProfile);
|
||||
}
|
||||
|
||||
var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile,
|
||||
sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey,
|
||||
sonarrSettings.FullUri);
|
||||
|
@ -65,13 +72,24 @@ namespace PlexRequests.UI.Helpers
|
|||
|
||||
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model)
|
||||
{
|
||||
var result = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, sickRageSettings.QualityProfile,
|
||||
return SendToSickRage(sickRageSettings, model, sickRageSettings.QualityProfile);
|
||||
}
|
||||
|
||||
public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model, string qualityId)
|
||||
{
|
||||
if (!sickRageSettings.Qualities.Any(x => x.Key == qualityId))
|
||||
{
|
||||
qualityId = sickRageSettings.QualityProfile;
|
||||
}
|
||||
|
||||
var apiResult = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, qualityId,
|
||||
sickRageSettings.ApiKey, sickRageSettings.FullUri);
|
||||
|
||||
var result = apiResult.Result;
|
||||
Log.Trace("SickRage Add Result: ");
|
||||
Log.Trace(result.DumpJson());
|
||||
|
||||
return result.Result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,12 +12,12 @@ namespace PlexRequests.UI.Jobs
|
|||
//typeof(AvailabilityUpdateService);
|
||||
var container = TinyIoCContainer.Current;
|
||||
|
||||
var a= container.ResolveAll(typeof(T));
|
||||
var a= container.Resolve(typeof(T));
|
||||
|
||||
object outT;
|
||||
container.TryResolve(typeof(T), out outT);
|
||||
|
||||
return (T)outT;
|
||||
return (T)a;
|
||||
}
|
||||
}
|
||||
}
|
8
PlexRequests.UI/Models/QualityModel.cs
Normal file
8
PlexRequests.UI/Models/QualityModel.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace PlexRequests.UI.Models
|
||||
{
|
||||
public class QualityModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@
|
|||
// ************************************************************************/
|
||||
#endregion
|
||||
using PlexRequests.Store;
|
||||
using System;
|
||||
|
||||
namespace PlexRequests.UI.Models
|
||||
{
|
||||
|
@ -36,12 +37,14 @@ namespace PlexRequests.UI.Models
|
|||
public string Overview { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string PosterPath { get; set; }
|
||||
public string ReleaseDate { get; set; }
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public long ReleaseDateTicks { get; set; }
|
||||
public RequestType Type { get; set; }
|
||||
public string Status { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public string RequestedBy { get; set; }
|
||||
public string RequestedDate { get; set; }
|
||||
public string[] RequestedUsers { get; set; }
|
||||
public DateTime RequestedDate { get; set; }
|
||||
public long RequestedDateTicks { get; set; }
|
||||
public string ReleaseYear { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public bool Admin { get; set; }
|
||||
|
@ -49,5 +52,8 @@ namespace PlexRequests.UI.Models
|
|||
public string OtherMessage { get; set; }
|
||||
public string AdminNotes { get; set; }
|
||||
public string TvSeriesRequestType { get; set; }
|
||||
public string MusicBrainzId { get; set; }
|
||||
public QualityModel[] Qualities { get; set; }
|
||||
public string ArtistName { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
41
PlexRequests.UI/Models/SearchMusicViewModel.cs
Normal file
41
PlexRequests.UI/Models/SearchMusicViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -29,5 +29,6 @@ namespace PlexRequests.UI.Models
|
|||
public class SessionKeys
|
||||
{
|
||||
public const string UsernameKey = "Username";
|
||||
public const string ClientDateTimeOffsetKey = "ClientDateTimeOffset";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,14 +28,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using MarkdownSharp;
|
||||
|
||||
using Nancy;
|
||||
using Nancy.Extensions;
|
||||
using Nancy.ModelBinding;
|
||||
using Nancy.Responses.Negotiation;
|
||||
using Nancy.Security;
|
||||
using Nancy.Validation;
|
||||
|
||||
using NLog;
|
||||
|
@ -51,12 +49,16 @@ using PlexRequests.Store.Models;
|
|||
using PlexRequests.Store.Repository;
|
||||
using PlexRequests.UI.Helpers;
|
||||
using PlexRequests.UI.Models;
|
||||
using System;
|
||||
|
||||
using Nancy.Json;
|
||||
using Nancy.Security;
|
||||
|
||||
namespace PlexRequests.UI.Modules
|
||||
{
|
||||
public class AdminModule : NancyModule
|
||||
{
|
||||
private ISettingsService<PlexRequestSettings> RpService { get; }
|
||||
private ISettingsService<PlexRequestSettings> PrService { get; }
|
||||
private ISettingsService<CouchPotatoSettings> CpService { get; }
|
||||
private ISettingsService<AuthenticationSettings> AuthService { get; }
|
||||
private ISettingsService<PlexSettings> PlexService { get; }
|
||||
|
@ -65,6 +67,7 @@ namespace PlexRequests.UI.Modules
|
|||
private ISettingsService<EmailNotificationSettings> EmailService { get; }
|
||||
private ISettingsService<PushbulletNotificationSettings> PushbulletService { get; }
|
||||
private ISettingsService<PushoverNotificationSettings> PushoverService { get; }
|
||||
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
|
||||
private IPlexApi PlexApi { get; }
|
||||
private ISonarrApi SonarrApi { get; }
|
||||
private IPushbulletApi PushbulletApi { get; }
|
||||
|
@ -72,9 +75,10 @@ namespace PlexRequests.UI.Modules
|
|||
private ICouchPotatoApi CpApi { get; }
|
||||
private IRepository<LogEntity> LogsRepo { get; }
|
||||
private INotificationService NotificationService { get; }
|
||||
private ICacheProvider Cache { get; }
|
||||
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
public AdminModule(ISettingsService<PlexRequestSettings> rpService,
|
||||
public AdminModule(ISettingsService<PlexRequestSettings> prService,
|
||||
ISettingsService<CouchPotatoSettings> cpService,
|
||||
ISettingsService<AuthenticationSettings> auth,
|
||||
ISettingsService<PlexSettings> plex,
|
||||
|
@ -89,9 +93,11 @@ namespace PlexRequests.UI.Modules
|
|||
ISettingsService<PushoverNotificationSettings> pushoverSettings,
|
||||
IPushoverApi pushoverApi,
|
||||
IRepository<LogEntity> logsRepo,
|
||||
INotificationService notify) : base("admin")
|
||||
INotificationService notify,
|
||||
ISettingsService<HeadphonesSettings> headphones,
|
||||
ICacheProvider cache) : base("admin")
|
||||
{
|
||||
RpService = rpService;
|
||||
PrService = prService;
|
||||
CpService = cpService;
|
||||
AuthService = auth;
|
||||
PlexService = plex;
|
||||
|
@ -107,6 +113,8 @@ namespace PlexRequests.UI.Modules
|
|||
PushoverService = pushoverSettings;
|
||||
PushoverApi = pushoverApi;
|
||||
NotificationService = notify;
|
||||
HeadphonesService = headphones;
|
||||
Cache = cache;
|
||||
|
||||
#if !DEBUG
|
||||
this.RequiresAuthentication();
|
||||
|
@ -139,18 +147,24 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
Get["/emailnotification"] = _ => EmailNotifications();
|
||||
Post["/emailnotification"] = _ => SaveEmailNotifications();
|
||||
Post["/testemailnotification"] = _ => TestEmailNotifications();
|
||||
Get["/status"] = _ => Status();
|
||||
|
||||
Get["/pushbulletnotification"] = _ => PushbulletNotifications();
|
||||
Post["/pushbulletnotification"] = _ => SavePushbulletNotifications();
|
||||
Post["/testpushbulletnotification"] = _ => TestPushbulletNotifications();
|
||||
|
||||
Get["/pushovernotification"] = _ => PushoverNotifications();
|
||||
Post["/pushovernotification"] = _ => SavePushoverNotifications();
|
||||
Post["/testpushovernotification"] = _ => TestPushoverNotifications();
|
||||
|
||||
Get["/logs"] = _ => Logs();
|
||||
Get["/loglevel"] = _ => GetLogLevels();
|
||||
Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level);
|
||||
Get["/loadlogs"] = _ => LoadLogs();
|
||||
|
||||
Get["/headphones"] = _ => Headphones();
|
||||
Post["/headphones"] = _ => SaveHeadphones();
|
||||
}
|
||||
|
||||
private Negotiator Authentication()
|
||||
|
@ -174,7 +188,7 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
private Negotiator Admin()
|
||||
{
|
||||
var settings = RpService.GetSettings();
|
||||
var settings = PrService.GetSettings();
|
||||
Log.Trace("Getting Settings:");
|
||||
Log.Trace(settings.DumpJson());
|
||||
|
||||
|
@ -185,7 +199,7 @@ namespace PlexRequests.UI.Modules
|
|||
{
|
||||
var model = this.Bind<PlexRequestSettings>();
|
||||
|
||||
RpService.SaveSettings(model);
|
||||
PrService.SaveSettings(model);
|
||||
|
||||
|
||||
return Context.GetRedirect("~/admin");
|
||||
|
@ -362,6 +376,12 @@ namespace PlexRequests.UI.Modules
|
|||
var settings = this.Bind<SonarrSettings>();
|
||||
var profiles = SonarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
|
||||
|
||||
// set the cache
|
||||
if (profiles != null)
|
||||
{
|
||||
Cache.Set(CacheKeys.SonarrQualityProfiles, profiles);
|
||||
}
|
||||
|
||||
return Response.AsJson(profiles);
|
||||
}
|
||||
|
||||
|
@ -372,6 +392,37 @@ namespace PlexRequests.UI.Modules
|
|||
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()
|
||||
{
|
||||
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." });
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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." });
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
var settings = this.Bind<CouchPotatoSettings>();
|
||||
var profiles = CpApi.GetProfiles(settings.FullUri, settings.ApiKey);
|
||||
|
||||
// set the cache
|
||||
if (profiles != null)
|
||||
{
|
||||
Cache.Set(CacheKeys.CouchPotatoQualityProfiles, profiles);
|
||||
}
|
||||
|
||||
return Response.AsJson(profiles);
|
||||
}
|
||||
|
||||
|
@ -487,7 +606,8 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
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>()};
|
||||
foreach (var l in allLogs)
|
||||
{
|
||||
|
@ -509,5 +629,32 @@ namespace PlexRequests.UI.Modules
|
|||
LoggingHelper.ReconfigureLogLevel(newLevel);
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"});
|
||||
}
|
||||
|
||||
private Negotiator Headphones()
|
||||
{
|
||||
var settings = HeadphonesService.GetSettings();
|
||||
return View["Headphones", settings];
|
||||
}
|
||||
|
||||
private Response SaveHeadphones()
|
||||
{
|
||||
var settings = this.Bind<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." });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ namespace PlexRequests.UI.Modules
|
|||
{
|
||||
|
||||
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();
|
||||
|
||||
|
@ -52,11 +52,13 @@ namespace PlexRequests.UI.Modules
|
|||
PlexApi = plexApi;
|
||||
AuthSettings = authSettings;
|
||||
SickRageApi = srApi;
|
||||
HeadphonesApi = hpApi;
|
||||
|
||||
Post["/cp"] = _ => CouchPotatoTest();
|
||||
Post["/sonarr"] = _ => SonarrTest();
|
||||
Post["/plex"] = _ => PlexTest();
|
||||
Post["/sickrage"] = _ => SickRageTest();
|
||||
Post["/headphones"] = _ => HeadphonesTest();
|
||||
|
||||
}
|
||||
|
||||
|
@ -65,6 +67,7 @@ namespace PlexRequests.UI.Modules
|
|||
private ICouchPotatoApi CpApi { get; }
|
||||
private IPlexApi PlexApi { get; }
|
||||
private ISickRageApi SickRageApi { get; }
|
||||
private IHeadphonesApi HeadphonesApi { get; }
|
||||
private ISettingsService<AuthenticationSettings> AuthSettings { get; }
|
||||
|
||||
private Response CouchPotatoTest()
|
||||
|
@ -168,5 +171,35 @@ namespace PlexRequests.UI.Modules
|
|||
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 }); ;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,7 +47,8 @@ namespace PlexRequests.UI.Modules
|
|||
{
|
||||
|
||||
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();
|
||||
|
||||
|
@ -58,9 +59,13 @@ namespace PlexRequests.UI.Modules
|
|||
SonarrSettings = sonarrSettings;
|
||||
SickRageApi = srApi;
|
||||
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["/approveallmovies"] = x => ApproveAllMovies();
|
||||
Post["/approvealltvshows"] = x => ApproveAllTVShows();
|
||||
}
|
||||
|
||||
private IRequestService Service { get; }
|
||||
|
@ -69,16 +74,18 @@ namespace PlexRequests.UI.Modules
|
|||
private ISettingsService<SonarrSettings> SonarrSettings { get; }
|
||||
private ISettingsService<SickRageSettings> SickRageSettings { get; }
|
||||
private ISettingsService<CouchPotatoSettings> CpService { get; }
|
||||
private ISettingsService<HeadphonesSettings> HeadphonesSettings { get; }
|
||||
private ISonarrApi SonarrApi { get; }
|
||||
private ISickRageApi SickRageApi { get; }
|
||||
private ICouchPotatoApi CpApi { get; }
|
||||
private IHeadphonesApi HeadphoneApi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Approves the specified request identifier.
|
||||
/// </summary>
|
||||
/// <param name="requestId">The request identifier.</param>
|
||||
/// <returns></returns>
|
||||
private Response Approve(int requestId)
|
||||
private Response Approve(int requestId, string qualityId)
|
||||
{
|
||||
Log.Info("approving request {0}", requestId);
|
||||
if (!Context.CurrentUser.IsAuthenticated())
|
||||
|
@ -97,15 +104,17 @@ namespace PlexRequests.UI.Modules
|
|||
switch (request.Type)
|
||||
{
|
||||
case RequestType.Movie:
|
||||
return RequestMovieAndUpdateStatus(request);
|
||||
return RequestMovieAndUpdateStatus(request, qualityId);
|
||||
case RequestType.TvShow:
|
||||
return RequestTvAndUpdateStatus(request);
|
||||
return RequestTvAndUpdateStatus(request, qualityId);
|
||||
case RequestType.Album:
|
||||
return RequestAlbumAndUpdateStatus(request);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(request));
|
||||
}
|
||||
}
|
||||
|
||||
private Response RequestTvAndUpdateStatus(RequestedModel request)
|
||||
private Response RequestTvAndUpdateStatus(RequestedModel request, string qualityId)
|
||||
{
|
||||
var sender = new TvSender(SonarrApi, SickRageApi);
|
||||
|
||||
|
@ -113,7 +122,7 @@ namespace PlexRequests.UI.Modules
|
|||
if (sonarrSettings.Enabled)
|
||||
{
|
||||
Log.Trace("Sending to Sonarr");
|
||||
var result = sender.SendToSonarr(sonarrSettings, request);
|
||||
var result = sender.SendToSonarr(sonarrSettings, request, qualityId);
|
||||
Log.Trace("Sonarr Result: ");
|
||||
Log.Trace(result.DumpJson());
|
||||
if (!string.IsNullOrEmpty(result.title))
|
||||
|
@ -131,7 +140,7 @@ namespace PlexRequests.UI.Modules
|
|||
return Response.AsJson(new JsonResponseModel
|
||||
{
|
||||
Result = false,
|
||||
Message = "Could not add the series to Sonarr"
|
||||
Message = result.ErrorMessage ?? "Could not add the series to Sonarr"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -139,7 +148,7 @@ namespace PlexRequests.UI.Modules
|
|||
if (srSettings.Enabled)
|
||||
{
|
||||
Log.Trace("Sending to SickRage");
|
||||
var result = sender.SendToSickRage(srSettings, request);
|
||||
var result = sender.SendToSickRage(srSettings, request, qualityId);
|
||||
Log.Trace("SickRage Result: ");
|
||||
Log.Trace(result.DumpJson());
|
||||
if (result?.result == "success")
|
||||
|
@ -167,7 +176,7 @@ namespace PlexRequests.UI.Modules
|
|||
});
|
||||
}
|
||||
|
||||
private Response RequestMovieAndUpdateStatus(RequestedModel request)
|
||||
private Response RequestMovieAndUpdateStatus(RequestedModel request, string qualityId)
|
||||
{
|
||||
var cpSettings = CpService.GetSettings();
|
||||
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."
|
||||
});
|
||||
}
|
||||
var result = cp.AddMovie(request.ImdbId, cpSettings.ApiKey, request.Title, cpSettings.FullUri, cpSettings.ProfileId);
|
||||
|
||||
var result = cp.AddMovie(request.ImdbId, cpSettings.ApiKey, request.Title, cpSettings.FullUri, string.IsNullOrEmpty(qualityId) ? cpSettings.ProfileId : qualityId);
|
||||
Log.Trace("Adding movie to CP result {0}", result);
|
||||
if (result)
|
||||
{
|
||||
|
@ -216,6 +226,84 @@ 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>
|
||||
/// Approves all.
|
||||
/// </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." });
|
||||
}
|
||||
|
||||
var requests = Service.GetAll().Where(x => x.Approved == false);
|
||||
var requests = Service.GetAll().Where(x => x.CanApprove);
|
||||
var requestedModels = requests as RequestedModel[] ?? requests.ToArray();
|
||||
if (!requestedModels.Any())
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "There are no requests to approve. Please refresh." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return UpdateRequests(requestedModels);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Fatal(e);
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Response UpdateRequests(RequestedModel[] requestedModels)
|
||||
{
|
||||
var cpSettings = CpService.GetSettings();
|
||||
|
||||
|
||||
var updatedRequests = new List<RequestedModel>();
|
||||
foreach (var r in requestedModels)
|
||||
{
|
||||
if (r.Type == RequestType.Movie)
|
||||
{
|
||||
var result = SendMovie(cpSettings, r, CpApi);
|
||||
if (result)
|
||||
var res = SendMovie(cpSettings, r, CpApi);
|
||||
if (res)
|
||||
{
|
||||
r.Approved = true;
|
||||
updatedRequests.Add(r);
|
||||
|
@ -260,8 +360,8 @@ namespace PlexRequests.UI.Modules
|
|||
var sonarr = SonarrSettings.GetSettings();
|
||||
if (sr.Enabled)
|
||||
{
|
||||
var result = sender.SendToSickRage(sr, r);
|
||||
if (result?.result == "success")
|
||||
var res = sender.SendToSickRage(sr, r);
|
||||
if (res?.result == "success")
|
||||
{
|
||||
r.Approved = true;
|
||||
updatedRequests.Add(r);
|
||||
|
@ -269,14 +369,14 @@ namespace PlexRequests.UI.Modules
|
|||
else
|
||||
{
|
||||
Log.Error("Could not approve and send the TV {0} to SickRage!", r.Title);
|
||||
Log.Error("SickRage Message: {0}", result?.message);
|
||||
Log.Error("SickRage Message: {0}", res?.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (sonarr.Enabled)
|
||||
{
|
||||
var result = sender.SendToSonarr(sonarr, r);
|
||||
if (result != null)
|
||||
var res = sender.SendToSonarr(sonarr, r);
|
||||
if (!string.IsNullOrEmpty(res?.title))
|
||||
{
|
||||
r.Approved = true;
|
||||
updatedRequests.Add(r);
|
||||
|
@ -284,6 +384,7 @@ namespace PlexRequests.UI.Modules
|
|||
else
|
||||
{
|
||||
Log.Error("Could not approve and send the TV {0} to Sonarr!", r.Title);
|
||||
Log.Error("Error message: {0}", res?.ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,17 +392,16 @@ namespace PlexRequests.UI.Modules
|
|||
try
|
||||
{
|
||||
|
||||
var result = Service.BatchUpdate(updatedRequests); return Response.AsJson(result
|
||||
var result = Service.BatchUpdate(updatedRequests);
|
||||
return Response.AsJson(result
|
||||
? new JsonResponseModel { Result = true }
|
||||
: new JsonResponseModel { Result = false, Message = "We could not approve all of the requests. Please try again or check the logs." });
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Fatal(e);
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something bad happened, please check the logs!" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp)
|
||||
|
|
|
@ -28,11 +28,40 @@
|
|||
using Nancy;
|
||||
using Nancy.Extensions;
|
||||
using PlexRequests.UI.Models;
|
||||
using System;
|
||||
|
||||
namespace PlexRequests.UI.Modules
|
||||
{
|
||||
public class BaseModule : NancyModule
|
||||
{
|
||||
private string _username;
|
||||
private int _dateTimeOffset = -1;
|
||||
|
||||
protected string Username
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_username))
|
||||
{
|
||||
_username = Session[SessionKeys.UsernameKey].ToString();
|
||||
}
|
||||
return _username;
|
||||
}
|
||||
}
|
||||
|
||||
protected int DateTimeOffset
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_dateTimeOffset == -1)
|
||||
{
|
||||
_dateTimeOffset = Session[SessionKeys.ClientDateTimeOffsetKey] != null ?
|
||||
(int)Session[SessionKeys.ClientDateTimeOffsetKey] : (new DateTimeOffset().Offset).Minutes;
|
||||
}
|
||||
return _dateTimeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
public BaseModule()
|
||||
{
|
||||
Before += (ctx) => CheckAuth();
|
||||
|
|
|
@ -60,6 +60,7 @@ namespace PlexRequests.UI.Modules
|
|||
{
|
||||
var username = (string)Request.Form.Username;
|
||||
var password = (string)Request.Form.Password;
|
||||
var dtOffset = (int)Request.Form.DateTimeOffset;
|
||||
|
||||
var userId = UserMapper.ValidateUser(username, password);
|
||||
|
||||
|
@ -73,6 +74,7 @@ namespace PlexRequests.UI.Modules
|
|||
expiry = DateTime.Now.AddDays(7);
|
||||
}
|
||||
Session[SessionKeys.UsernameKey] = username;
|
||||
Session[SessionKeys.ClientDateTimeOffsetKey] = dtOffset;
|
||||
return this.LoginAndRedirect(userId.Value, expiry);
|
||||
};
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Humanizer;
|
||||
|
||||
using Nancy;
|
||||
using Nancy.Responses.Negotiation;
|
||||
using Nancy.Security;
|
||||
|
@ -40,22 +38,45 @@ using PlexRequests.Services.Interfaces;
|
|||
using PlexRequests.Services.Notification;
|
||||
using PlexRequests.Store;
|
||||
using PlexRequests.UI.Models;
|
||||
using PlexRequests.Helpers;
|
||||
using PlexRequests.UI.Helpers;
|
||||
using System.Collections.Generic;
|
||||
using PlexRequests.Api.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PlexRequests.UI.Modules
|
||||
{
|
||||
public class RequestsModule : BaseModule
|
||||
{
|
||||
|
||||
public RequestsModule(IRequestService service, ISettingsService<PlexRequestSettings> prSettings, ISettingsService<PlexSettings> plex, INotificationService notify) : base("requests")
|
||||
public RequestsModule(
|
||||
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;
|
||||
PrSettings = prSettings;
|
||||
PlexSettings = plex;
|
||||
NotificationService = notify;
|
||||
SonarrSettings = sonarrSettings;
|
||||
SickRageSettings = sickRageSettings;
|
||||
CpSettings = cpSettings;
|
||||
SonarrApi = sonarrApi;
|
||||
SickRageApi = sickRageApi;
|
||||
CpApi = cpApi;
|
||||
Cache = cache;
|
||||
|
||||
Get["/"] = _ => LoadRequests();
|
||||
Get["/movies"] = _ => GetMovies();
|
||||
Get["/tvshows"] = _ => GetTvShows();
|
||||
Get["/albums"] = _ => GetAlbumRequests();
|
||||
Post["/delete"] = _ => DeleteRequest((int)Request.Form.id);
|
||||
Post["/reportissue"] = _ => ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null);
|
||||
Post["/reportissuecomment"] = _ => ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea);
|
||||
|
@ -70,6 +91,13 @@ namespace PlexRequests.UI.Modules
|
|||
private INotificationService NotificationService { get; }
|
||||
private ISettingsService<PlexRequestSettings> PrSettings { 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()
|
||||
{
|
||||
|
@ -77,11 +105,54 @@ namespace PlexRequests.UI.Modules
|
|||
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 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,
|
||||
Type = movie.Type,
|
||||
|
@ -89,28 +160,81 @@ namespace PlexRequests.UI.Modules
|
|||
ImdbId = movie.ImdbId,
|
||||
Id = movie.Id,
|
||||
PosterPath = movie.PosterPath,
|
||||
ReleaseDate = movie.ReleaseDate.Humanize(),
|
||||
RequestedDate = movie.RequestedDate.Humanize(),
|
||||
Approved = movie.Approved,
|
||||
ReleaseDate = movie.ReleaseDate,
|
||||
ReleaseDateTicks = movie.ReleaseDate.Ticks,
|
||||
RequestedDate = movie.RequestedDate,
|
||||
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks,
|
||||
Approved = movie.Available || movie.Approved,
|
||||
Title = movie.Title,
|
||||
Overview = movie.Overview,
|
||||
RequestedBy = movie.RequestedBy,
|
||||
RequestedUsers = isAdmin ? movie.AllUsers.ToArray() : new string[] { },
|
||||
ReleaseYear = movie.ReleaseDate.Year.ToString(),
|
||||
Available = movie.Available,
|
||||
Admin = isAdmin,
|
||||
Issues = movie.Issues.Humanize(LetterCasing.Title),
|
||||
Issues = movie.Issues.ToString().CamelCaseToWords(),
|
||||
OtherMessage = movie.OtherMessage,
|
||||
AdminNotes = movie.AdminNote
|
||||
AdminNotes = movie.AdminNote,
|
||||
Qualities = qualities.ToArray()
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
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 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,
|
||||
Type = tv.Type,
|
||||
|
@ -118,19 +242,67 @@ namespace PlexRequests.UI.Modules
|
|||
ImdbId = tv.ImdbId,
|
||||
Id = tv.Id,
|
||||
PosterPath = tv.PosterPath,
|
||||
ReleaseDate = tv.ReleaseDate.Humanize(),
|
||||
RequestedDate = tv.RequestedDate.Humanize(),
|
||||
Approved = tv.Approved,
|
||||
ReleaseDate = tv.ReleaseDate,
|
||||
ReleaseDateTicks = tv.ReleaseDate.Ticks,
|
||||
RequestedDate = tv.RequestedDate,
|
||||
RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks,
|
||||
Approved = tv.Available || tv.Approved,
|
||||
Title = tv.Title,
|
||||
Overview = tv.Overview,
|
||||
RequestedBy = tv.RequestedBy,
|
||||
RequestedUsers = isAdmin ? tv.AllUsers.ToArray() : new string[] { },
|
||||
ReleaseYear = tv.ReleaseDate.Year.ToString(),
|
||||
Available = tv.Available,
|
||||
Admin = isAdmin,
|
||||
Issues = tv.Issues.Humanize(LetterCasing.Title),
|
||||
Issues = tv.Issues.ToString().CamelCaseToWords(),
|
||||
OtherMessage = tv.OtherMessage,
|
||||
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();
|
||||
|
||||
return Response.AsJson(viewModel);
|
||||
|
@ -165,7 +337,7 @@ namespace PlexRequests.UI.Modules
|
|||
}
|
||||
originalRequest.Issues = issue;
|
||||
originalRequest.OtherMessage = !string.IsNullOrEmpty(comment)
|
||||
? $"{Session[SessionKeys.UsernameKey]} - {comment}"
|
||||
? $"{Username} - {comment}"
|
||||
: string.Empty;
|
||||
|
||||
|
||||
|
@ -173,11 +345,11 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
var model = new NotificationModel
|
||||
{
|
||||
User = Session[SessionKeys.UsernameKey].ToString(),
|
||||
User = Username,
|
||||
NotificationType = NotificationType.Issue,
|
||||
Title = originalRequest.Title,
|
||||
DateTime = DateTime.Now,
|
||||
Body = issue == IssueState.Other ? comment : issue.Humanize()
|
||||
Body = issue == IssueState.Other ? comment : issue.ToString().CamelCaseToWords()
|
||||
};
|
||||
NotificationService.Publish(model);
|
||||
|
||||
|
|
|
@ -31,15 +31,18 @@ using System.Linq;
|
|||
|
||||
using Nancy;
|
||||
using Nancy.Responses.Negotiation;
|
||||
using Nancy.Security;
|
||||
|
||||
using NLog;
|
||||
|
||||
using PlexRequests.Api;
|
||||
using PlexRequests.Api.Interfaces;
|
||||
using PlexRequests.Api.Models.Music;
|
||||
using PlexRequests.Core;
|
||||
using PlexRequests.Core.SettingModels;
|
||||
using PlexRequests.Helpers;
|
||||
using PlexRequests.Helpers.Exceptions;
|
||||
using PlexRequests.Services;
|
||||
using PlexRequests.Services.Interfaces;
|
||||
using PlexRequests.Services.Notification;
|
||||
using PlexRequests.Store;
|
||||
|
@ -54,7 +57,7 @@ namespace PlexRequests.UI.Modules
|
|||
ISettingsService<PlexRequestSettings> prSettings, IAvailabilityChecker checker,
|
||||
IRequestService request, ISonarrApi sonarrApi, ISettingsService<SonarrSettings> sonarrSettings,
|
||||
ISettingsService<SickRageSettings> sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi,
|
||||
INotificationService notify) : base("search")
|
||||
INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService<HeadphonesSettings> hpService) : base("search")
|
||||
{
|
||||
CpService = cpSettings;
|
||||
PrService = prSettings;
|
||||
|
@ -69,17 +72,24 @@ namespace PlexRequests.UI.Modules
|
|||
SickRageService = sickRageService;
|
||||
SickrageApi = srApi;
|
||||
NotificationService = notify;
|
||||
MusicBrainzApi = mbApi;
|
||||
HeadphonesApi = hpApi;
|
||||
HeadphonesService = hpService;
|
||||
|
||||
|
||||
Get["/"] = parameters => RequestLoad();
|
||||
|
||||
Get["movie/{searchTerm}"] = parameters => SearchMovie((string)parameters.searchTerm);
|
||||
Get["tv/{searchTerm}"] = parameters => SearchTvShow((string)parameters.searchTerm);
|
||||
Get["music/{searchTerm}"] = parameters => SearchMusic((string)parameters.searchTerm);
|
||||
Get["music/coverArt/{id}"] = p => GetMusicBrainzCoverArt((string)p.id);
|
||||
|
||||
Get["movie/upcoming"] = parameters => UpcomingMovies();
|
||||
Get["movie/playing"] = parameters => CurrentlyPlayingMovies();
|
||||
|
||||
Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId);
|
||||
Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons);
|
||||
Post["request/album"] = parameters => RequestAlbum((string)Request.Form.albumId);
|
||||
}
|
||||
private TheMovieDbApi MovieApi { get; }
|
||||
private INotificationService NotificationService { get; }
|
||||
|
@ -93,9 +103,18 @@ namespace PlexRequests.UI.Modules
|
|||
private ISettingsService<PlexRequestSettings> PrService { get; }
|
||||
private ISettingsService<SonarrSettings> SonarrService { get; }
|
||||
private ISettingsService<SickRageSettings> SickRageService { get; }
|
||||
private ISettingsService<HeadphonesSettings> HeadphonesService { get; }
|
||||
private IAvailabilityChecker Checker { get; }
|
||||
private IMusicBrainzApi MusicBrainzApi { get; }
|
||||
private IHeadphonesApi HeadphonesApi { get; }
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
private string AuthToken => Cache.GetOrSet(CacheKeys.TvDbToken, TvApi.Authenticate, 50);
|
||||
|
||||
private bool IsAdmin {
|
||||
get
|
||||
{
|
||||
return Context.CurrentUser.IsAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
private Negotiator RequestLoad()
|
||||
{
|
||||
|
@ -152,6 +171,28 @@ namespace PlexRequests.UI.Modules
|
|||
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
|
||||
{
|
||||
var movies = MovieApi.GetUpcomingMovies();
|
||||
|
@ -172,30 +213,42 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
private Response RequestMovie(int movieId)
|
||||
{
|
||||
var movieApi = new TheMovieDbApi();
|
||||
var movieInfo = movieApi.GetMovieInformation(movieId).Result;
|
||||
var fullMovieName = $"{movieInfo.Title}{(movieInfo.ReleaseDate.HasValue ? $" ({movieInfo.ReleaseDate.Value.Year})" : string.Empty)}";
|
||||
Log.Trace("Getting movie info from TheMovieDb");
|
||||
Log.Trace(movieInfo.DumpJson);
|
||||
//#if !DEBUG
|
||||
|
||||
var settings = PrService.GetSettings();
|
||||
|
||||
// check if the movie has already been requested
|
||||
Log.Info("Requesting movie with id {0}", movieId);
|
||||
if (RequestService.CheckRequest(movieId))
|
||||
var existingRequest = RequestService.CheckRequest(movieId);
|
||||
if (existingRequest != null)
|
||||
{
|
||||
Log.Trace("movie with id {0} exists", movieId);
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Movie has already been requested!" });
|
||||
// check if the current user is already marked as a requester for this movie, if not, add them
|
||||
if (!existingRequest.UserHasRequested(Username))
|
||||
{
|
||||
existingRequest.RequestedUsers.Add(Username);
|
||||
RequestService.UpdateRequest(existingRequest);
|
||||
}
|
||||
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullMovieName} was successfully added!" : $"{fullMovieName} has already been requested!" });
|
||||
}
|
||||
|
||||
Log.Debug("movie with id {0} doesnt exists", movieId);
|
||||
|
||||
var movieApi = new TheMovieDbApi();
|
||||
var movieInfo = movieApi.GetMovieInformation(movieId).Result;
|
||||
Log.Trace("Getting movie info from TheMovieDb");
|
||||
Log.Trace(movieInfo.DumpJson);
|
||||
//#if !DEBUG
|
||||
try
|
||||
{
|
||||
if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString()))
|
||||
if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString(),null, PlexType.Movie))
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{movieInfo.Title} is already in Plex!" });
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullMovieName} is already in Plex!" });
|
||||
}
|
||||
}
|
||||
catch (ApplicationSettingsException)
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {movieInfo.Title} is in Plex, are you sure it's correctly setup?" });
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {fullMovieName} is in Plex, are you sure it's correctly setup?" });
|
||||
}
|
||||
//#endif
|
||||
|
||||
|
@ -209,16 +262,14 @@ namespace PlexRequests.UI.Modules
|
|||
Title = movieInfo.Title,
|
||||
ReleaseDate = movieInfo.ReleaseDate ?? DateTime.MinValue,
|
||||
Status = movieInfo.Status,
|
||||
RequestedDate = DateTime.Now,
|
||||
RequestedDate = DateTime.UtcNow,
|
||||
Approved = false,
|
||||
RequestedBy = Session[SessionKeys.UsernameKey].ToString(),
|
||||
RequestedUsers = new List<string>() { Username },
|
||||
Issues = IssueState.None,
|
||||
};
|
||||
|
||||
|
||||
var settings = PrService.GetSettings();
|
||||
Log.Trace(settings.DumpJson());
|
||||
if (!settings.RequireMovieApproval)
|
||||
if (ShouldAutoApprove(RequestType.Movie, settings))
|
||||
{
|
||||
var cpSettings = CpService.GetSettings();
|
||||
|
||||
|
@ -239,13 +290,13 @@ namespace PlexRequests.UI.Modules
|
|||
var notificationModel = new NotificationModel
|
||||
{
|
||||
Title = model.Title,
|
||||
User = model.RequestedBy,
|
||||
User = Username,
|
||||
DateTime = DateTime.Now,
|
||||
NotificationType = NotificationType.NewRequest
|
||||
};
|
||||
NotificationService.Publish(notificationModel);
|
||||
|
||||
return Response.AsJson(new JsonResponseModel {Result = true});
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
|
||||
}
|
||||
return
|
||||
Response.AsJson(new JsonResponseModel
|
||||
|
@ -264,13 +315,13 @@ namespace PlexRequests.UI.Modules
|
|||
var notificationModel = new NotificationModel
|
||||
{
|
||||
Title = model.Title,
|
||||
User = model.RequestedBy,
|
||||
User = Username,
|
||||
DateTime = DateTime.Now,
|
||||
NotificationType = NotificationType.NewRequest
|
||||
};
|
||||
NotificationService.Publish(notificationModel);
|
||||
|
||||
return Response.AsJson(new JsonResponseModel { Result = true });
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,10 +330,10 @@ namespace PlexRequests.UI.Modules
|
|||
Log.Debug("Adding movie to database requests");
|
||||
var id = RequestService.AddRequest(model);
|
||||
|
||||
var notificationModel = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
NotificationService.Publish(notificationModel);
|
||||
|
||||
return Response.AsJson(new JsonResponseModel { Result = true });
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullMovieName} was successfully added!" });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -300,30 +351,44 @@ namespace PlexRequests.UI.Modules
|
|||
/// <returns></returns>
|
||||
private Response RequestTvShow(int showId, string seasons)
|
||||
{
|
||||
if (RequestService.CheckRequest(showId))
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "TV Show has already been requested!" });
|
||||
}
|
||||
|
||||
var tvApi = new TvMazeApi();
|
||||
|
||||
var showInfo = tvApi.ShowLookupByTheTvDbId(showId);
|
||||
DateTime firstAir;
|
||||
DateTime.TryParse(showInfo.premiered, out firstAir);
|
||||
string fullShowName = $"{showInfo.name} ({firstAir.Year})";
|
||||
//#if !DEBUG
|
||||
|
||||
var settings = PrService.GetSettings();
|
||||
|
||||
// check if the show has already been requested
|
||||
Log.Info("Requesting tv show with id {0}", showId);
|
||||
var existingRequest = RequestService.CheckRequest(showId);
|
||||
if (existingRequest != null)
|
||||
{
|
||||
// check if the current user is already marked as a requester for this show, if not, add them
|
||||
if (!existingRequest.UserHasRequested(Username))
|
||||
{
|
||||
existingRequest.RequestedUsers.Add(Username);
|
||||
RequestService.UpdateRequest(existingRequest);
|
||||
}
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{fullShowName} was successfully added!" : $"{fullShowName} has already been requested!" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4))) // Take only the year Format = 2014-01-01
|
||||
if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4), null, PlexType.TvShow)) // Take only the year Format = 2014-01-01
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{showInfo.name} is already in Plex!" });
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"{fullShowName} is already in Plex!" });
|
||||
}
|
||||
}
|
||||
catch (ApplicationSettingsException)
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {showInfo.name} is in Plex, are you sure it's correctly setup?" });
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {fullShowName} is in Plex, are you sure it's correctly setup?" });
|
||||
}
|
||||
//#endif
|
||||
|
||||
DateTime firstAir;
|
||||
DateTime.TryParse(showInfo.premiered, out firstAir);
|
||||
|
||||
var model = new RequestedModel
|
||||
{
|
||||
ProviderId = showInfo.externals?.thetvdb ?? 0,
|
||||
|
@ -333,9 +398,9 @@ namespace PlexRequests.UI.Modules
|
|||
Title = showInfo.name,
|
||||
ReleaseDate = firstAir,
|
||||
Status = showInfo.status,
|
||||
RequestedDate = DateTime.Now,
|
||||
RequestedDate = DateTime.UtcNow,
|
||||
Approved = false,
|
||||
RequestedBy = Session[SessionKeys.UsernameKey].ToString(),
|
||||
RequestedUsers = new List<string>() { Username },
|
||||
Issues = IssueState.None,
|
||||
ImdbId = showInfo.externals?.imdb ?? string.Empty,
|
||||
SeasonCount = showInfo.seasonCount
|
||||
|
@ -358,26 +423,26 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
model.SeasonList = seasonsList.ToArray();
|
||||
|
||||
var settings = PrService.GetSettings();
|
||||
if (!settings.RequireTvShowApproval)
|
||||
if (ShouldAutoApprove(RequestType.TvShow, settings))
|
||||
{
|
||||
var sonarrSettings = SonarrService.GetSettings();
|
||||
var sender = new TvSender(SonarrApi, SickrageApi);
|
||||
if (sonarrSettings.Enabled)
|
||||
{
|
||||
var result = sender.SendToSonarr(sonarrSettings, model);
|
||||
if (result != null)
|
||||
if (result != null && !string.IsNullOrEmpty(result.title))
|
||||
{
|
||||
model.Approved = true;
|
||||
Log.Debug("Adding tv to database requests (No approval required & Sonarr)");
|
||||
RequestService.AddRequest(model);
|
||||
|
||||
return Response.AsJson(new JsonResponseModel { Result = true });
|
||||
}
|
||||
var notify1 = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
var notify1 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
NotificationService.Publish(notify1);
|
||||
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to Sonarr! Please check your settings." });
|
||||
return Response.AsJson(new JsonResponseModel { Result = 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)");
|
||||
RequestService.AddRequest(model);
|
||||
|
||||
var notify2 = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
var notify2 = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
NotificationService.Publish(notify2);
|
||||
|
||||
return Response.AsJson(new JsonResponseModel { Result = true });
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" });
|
||||
}
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = result?.message != null ? "<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);
|
||||
|
||||
var notificationModel = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
var notificationModel = new NotificationModel { Title = model.Title, User = Username, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest };
|
||||
NotificationService.Publish(notificationModel);
|
||||
|
||||
return Response.AsJson(new { Result = true });
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = $"{fullShowName} was successfully added!" });
|
||||
}
|
||||
|
||||
private bool CheckIfTitleExistsInPlex(string title, string year)
|
||||
private bool CheckIfTitleExistsInPlex(string title, string year, string artist, PlexType type)
|
||||
{
|
||||
var result = Checker.IsAvailable(title, year);
|
||||
var result = Checker.IsAvailable(title, year, artist, type);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Response RequestAlbum(string releaseId)
|
||||
{
|
||||
var settings = PrService.GetSettings();
|
||||
var existingRequest = RequestService.CheckRequest(releaseId);
|
||||
Log.Debug("Checking for an existing request");
|
||||
|
||||
if (existingRequest != null)
|
||||
{
|
||||
Log.Debug("We do have an existing album request");
|
||||
if (!existingRequest.UserHasRequested(Username))
|
||||
{
|
||||
Log.Debug("Not in the requested list so adding them and updating the request. User: {0}", Username);
|
||||
existingRequest.RequestedUsers.Add(Username);
|
||||
RequestService.UpdateRequest(existingRequest);
|
||||
}
|
||||
return Response.AsJson(new JsonResponseModel { Result = true, Message = settings.UsersCanViewOnlyOwnRequests ? $"{existingRequest.Title} was successfully added!" : $"{existingRequest.Title} has already been requested!" });
|
||||
}
|
||||
|
||||
|
||||
Log.Debug("This is a new request");
|
||||
|
||||
var albumInfo = MusicBrainzApi.GetAlbum(releaseId);
|
||||
DateTime release;
|
||||
DateTimeHelper.CustomParse(albumInfo.ReleaseEvents?.FirstOrDefault()?.date, out release);
|
||||
|
||||
var artist = albumInfo.ArtistCredits?.FirstOrDefault()?.artist;
|
||||
if (artist == null)
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = "We could not find the artist on MusicBrainz. Please try again later or contact your admin" });
|
||||
}
|
||||
|
||||
var alreadyInPlex = CheckIfTitleExistsInPlex(albumInfo.title, release.ToString("yyyy"), artist.name, PlexType.Music);
|
||||
|
||||
if (alreadyInPlex)
|
||||
{
|
||||
return Response.AsJson(new JsonResponseModel
|
||||
{
|
||||
Result = false,
|
||||
Message = $"{albumInfo.title} is already in Plex!"
|
||||
});
|
||||
}
|
||||
|
||||
var img = GetMusicBrainzCoverArt(albumInfo.id);
|
||||
|
||||
Log.Trace("Album Details:");
|
||||
Log.Trace(albumInfo.DumpJson());
|
||||
Log.Trace("CoverArt Details:");
|
||||
Log.Trace(img.DumpJson());
|
||||
|
||||
|
||||
var model = new RequestedModel
|
||||
{
|
||||
Title = albumInfo.title,
|
||||
MusicBrainzId = albumInfo.id,
|
||||
Overview = albumInfo.disambiguation,
|
||||
PosterPath = img,
|
||||
Type = RequestType.Album,
|
||||
ProviderId = 0,
|
||||
RequestedUsers = new List<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ namespace PlexRequests.UI.Modules
|
|||
|
||||
private Response LoginUser()
|
||||
{
|
||||
var dateTimeOffset = Request.Form.DateTimeOffset;
|
||||
var username = Request.Form.username.Value;
|
||||
Log.Debug("Username \"{0}\" attempting to login",username);
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
|
@ -138,6 +139,8 @@ namespace PlexRequests.UI.Modules
|
|||
Session[SessionKeys.UsernameKey] = (string)username;
|
||||
}
|
||||
|
||||
Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset;
|
||||
|
||||
return Response.AsJson(authenticated
|
||||
? new JsonResponseModel { Result = true }
|
||||
: new JsonResponseModel { Result = false, Message = "Incorrect User or Password"});
|
||||
|
@ -170,7 +173,7 @@ namespace PlexRequests.UI.Modules
|
|||
var users = Api.GetUsers(authToken);
|
||||
Log.Debug("Plex Users: ");
|
||||
Log.Debug(users.DumpJson());
|
||||
var allUsers = users.User?.Where(x => !string.IsNullOrEmpty(x.Username));
|
||||
var allUsers = users?.User?.Where(x => !string.IsNullOrEmpty(x.Username));
|
||||
return allUsers != null && allUsers.Any(x => x.Username.Equals(username, StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
|
||||
|
|
|
@ -69,10 +69,6 @@
|
|||
<HintPath>..\packages\FluentValidation.6.2.1.0\lib\Net45\FluentValidation.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</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">
|
||||
<HintPath>..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
|
@ -168,9 +164,13 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Bootstrapper.cs" />
|
||||
<Compile Include="Helpers\HeadphonesSender.cs" />
|
||||
<Compile Include="Helpers\StringHelper.cs" />
|
||||
<Compile Include="Helpers\TvSender.cs" />
|
||||
<Compile Include="Helpers\ValidationHelper.cs" />
|
||||
<Compile Include="Models\DatatablesModel.cs" />
|
||||
<Compile Include="Models\QualityModel.cs" />
|
||||
<Compile Include="Models\SearchMusicViewModel.cs" />
|
||||
<Compile Include="Validators\PushoverSettingsValidator.cs" />
|
||||
<Compile Include="Validators\PushbulletSettingsValidator.cs" />
|
||||
<Compile Include="Validators\EmailNotificationSettingsValidator.cs" />
|
||||
|
@ -248,6 +248,15 @@
|
|||
<Content Include="Content\jquery.mixitup.js">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</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">
|
||||
<DependentUpon>pace.scss</DependentUpon>
|
||||
</Content>
|
||||
|
@ -371,6 +380,9 @@
|
|||
<Content Include="Views\Admin\PushoverNotifications.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Views\Admin\Headphones.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="Web.Debug.config">
|
||||
<DependentUpon>web.config</DependentUpon>
|
||||
</None>
|
||||
|
|
|
@ -64,8 +64,10 @@ namespace PlexRequests.UI
|
|||
|
||||
var s = new Setup();
|
||||
var cn = s.SetupDb();
|
||||
s.CacheQualityProfiles();
|
||||
ConfigureTargets(cn);
|
||||
|
||||
|
||||
if (port == -1)
|
||||
port = GetStartupPort();
|
||||
|
||||
|
|
|
@ -88,6 +88,11 @@
|
|||
</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>
|
||||
|
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
|
152
PlexRequests.UI/Views/Admin/Headphones.cshtml
Normal file
152
PlexRequests.UI/Views/Admin/Headphones.cshtml
Normal 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>
|
|
@ -36,6 +36,12 @@
|
|||
</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>
|
||||
<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>
|
|
@ -36,6 +36,12 @@
|
|||
</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>
|
||||
<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>
|
|
@ -52,6 +52,20 @@
|
|||
</label>
|
||||
</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="checkbox">
|
||||
<label>
|
||||
|
@ -82,6 +96,45 @@
|
|||
|
||||
</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">
|
||||
<label for="WeeklyRequestLimit" class="control-label">Weekly Request Limit</label>
|
||||
|
@ -102,4 +155,3 @@
|
|||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -75,15 +75,10 @@
|
|||
<label for="profiles" class="control-label ">Quality Profiles</label>
|
||||
<div id="profiles">
|
||||
<select class="form-control form-control-custom" value="selected">
|
||||
<option id="default" value="default">Use Deafult</option>
|
||||
<option id="sdtv" value="sdtv">SD TV</option>
|
||||
<option id="sddvd" value="sddvd">SD DVD</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>
|
||||
@foreach (var quality in Model.Qualities)
|
||||
{
|
||||
<option id="@quality.Key" value="@quality.Key">@quality.Value</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -131,6 +131,9 @@
|
|||
{
|
||||
<text>
|
||||
var qualitySelected = @Model.QualityProfile;
|
||||
if (!qualitySelected) {
|
||||
return;
|
||||
}
|
||||
var $form = $("#mainForm");
|
||||
$.ajax({
|
||||
type: $form.prop("method"),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue