frontend and tv episodes api work for #254

This commit is contained in:
tidusjar 2016-07-15 14:29:58 +01:00
parent d7c40164cb
commit 33ba1db20b
12 changed files with 433 additions and 210 deletions

View file

@ -84,6 +84,7 @@
<Compile Include="Sonarr\SonarrProfile.cs" /> <Compile Include="Sonarr\SonarrProfile.cs" />
<Compile Include="Sonarr\SystemStatus.cs" /> <Compile Include="Sonarr\SystemStatus.cs" />
<Compile Include="Tv\Authentication.cs" /> <Compile Include="Tv\Authentication.cs" />
<Compile Include="Tv\TvMazeEpisodes.cs" />
<Compile Include="Tv\TvMazeSearch.cs" /> <Compile Include="Tv\TvMazeSearch.cs" />
<Compile Include="Tv\TvMazeSeasons.cs" /> <Compile Include="Tv\TvMazeSeasons.cs" />
<Compile Include="Tv\TVMazeShow.cs" /> <Compile Include="Tv\TVMazeShow.cs" />

View file

@ -0,0 +1,44 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2016 Jamie Rees
// File: TvMazeEpisodes.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.Tv
{
public class TvMazeEpisodes
{
public int id { get; set; }
public string url { get; set; }
public string name { get; set; }
public int season { get; set; }
public int number { get; set; }
public string airdate { get; set; }
public string airtime { get; set; }
public string airstamp { get; set; }
public int runtime { get; set; }
public Image image { get; set; }
public string summary { get; set; }
public Links _links { get; set; }
}
}

View file

@ -1,97 +1,119 @@
using System; #region Copyright
using System.Collections.Generic; // /************************************************************************
using System.Linq; // Copyright (c) 2016 Jamie Rees
using System.Text; // File: TvMazeSearch.cs
using System.Threading.Tasks; // Created By: Jamie Rees
//
namespace PlexRequests.Api.Models.Tv // Permission is hereby granted, free of charge, to any person obtaining
{ // a copy of this software and associated documentation files (the
public class Schedule // "Software"), to deal in the Software without restriction, including
{ // without limitation the rights to use, copy, modify, merge, publish,
public string time { get; set; } // distribute, sublicense, and/or sell copies of the Software, and to
public List<object> days { get; set; } // permit persons to whom the Software is furnished to do so, subject to
} // the following conditions:
//
public class Rating // The above copyright notice and this permission notice shall be
{ // included in all copies or substantial portions of the Software.
public double? average { get; set; } //
} // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
public class Country // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
{ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
public string name { get; set; } // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
public string code { get; set; } // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
public string timezone { get; set; } // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
} // ************************************************************************/
#endregion
public class Network using System.Collections.Generic;
{
public int id { get; set; } namespace PlexRequests.Api.Models.Tv
public string name { get; set; } {
public Country country { get; set; } public class Schedule
} {
public List<object> days { get; set; }
public class Externals public string time { get; set; }
{ }
public int? tvrage { get; set; }
public int? thetvdb { get; set; } public class Rating
public string imdb { get; set; } {
} public double? average { get; set; }
}
public class Image
{ public class Country
public string medium { get; set; } {
public string original { get; set; } public string code { get; set; }
} public string name { get; set; }
public string timezone { get; set; }
public class Self }
{
public string href { get; set; } public class Network
} {
public Country country { get; set; }
public class Previousepisode public int id { get; set; }
{ public string name { get; set; }
public string href { get; set; } }
}
public class Externals
public class Nextepisode {
{ public string imdb { get; set; }
public string href { get; set; } public int? thetvdb { get; set; }
} public int? tvrage { get; set; }
}
public class Links
{ public class Image
public Self self { get; set; } {
public Previousepisode previousepisode { get; set; } public string medium { get; set; }
public Nextepisode nextepisode { get; set; } public string original { get; set; }
} }
public class Show public class Self
{ {
public int id { get; set; } public string href { get; set; }
public string url { get; set; } }
public string name { get; set; }
public string type { get; set; } public class Previousepisode
public string language { get; set; } {
public List<object> genres { get; set; } public string href { get; set; }
public string status { get; set; } }
public int? runtime { get; set; }
public string premiered { get; set; } public class Nextepisode
public Schedule schedule { get; set; } {
public Rating rating { get; set; } public string href { get; set; }
public int weight { get; set; } }
public Network network { get; set; }
public object webChannel { get; set; } public class Links
public Externals externals { get; set; } {
public Image image { get; set; } public Nextepisode nextepisode { get; set; }
public string summary { get; set; } public Previousepisode previousepisode { get; set; }
public int updated { get; set; } public Self self { get; set; }
public Links _links { get; set; } }
}
public class Show
public class TvMazeSearch {
{ public Links _links { get; set; }
public double score { get; set; } public Externals externals { get; set; }
public Show show { get; set; } public List<object> genres { get; set; }
} public int id { get; set; }
} public Image image { get; set; }
public string language { get; set; }
public string name { get; set; }
public Network network { get; set; }
public string premiered { get; set; }
public Rating rating { get; set; }
public int? runtime { get; set; }
public Schedule schedule { get; set; }
public string status { get; set; }
public string summary { get; set; }
public string type { get; set; }
public int updated { get; set; }
public string url { get; set; }
public object webChannel { get; set; }
public int weight { get; set; }
}
public class TvMazeSearch
{
public double score { get; set; }
public Show show { get; set; }
}
}

View file

@ -1,108 +1,121 @@
#region Copyright #region Copyright
// /************************************************************************ // /************************************************************************
// Copyright (c) 2016 Jamie Rees // Copyright (c) 2016 Jamie Rees
// File: TvMazeApi.cs // File: TvMazeApi.cs
// Created By: Jamie Rees // Created By: Jamie Rees
// //
// Permission is hereby granted, free of charge, to any person obtaining // Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the // a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including // "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish, // without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to // distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to // permit persons to whom the Software is furnished to do so, subject to
// the following conditions: // the following conditions:
// //
// The above copyright notice and this permission notice shall be // The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software. // included in all copies or substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using PlexRequests.Api.Models.Tv; using PlexRequests.Api.Models.Tv;
using RestSharp; using RestSharp;
namespace PlexRequests.Api namespace PlexRequests.Api
{ {
public class TvMazeApi : TvMazeBase public class TvMazeApi : TvMazeBase
{ {
public TvMazeApi() public TvMazeApi()
{ {
Api = new ApiRequest(); Api = new ApiRequest();
} }
private ApiRequest Api { get; } private ApiRequest Api { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
public List<TvMazeSearch> Search(string searchTerm) public List<TvMazeSearch> Search(string searchTerm)
{ {
var request = new RestRequest var request = new RestRequest
{ {
Method = Method.GET, Method = Method.GET,
Resource = "search/shows?q={searchTerm}" Resource = "search/shows?q={searchTerm}"
}; };
request.AddUrlSegment("searchTerm", searchTerm); request.AddUrlSegment("searchTerm", searchTerm);
request.AddHeader("Content-Type", "application/json"); request.AddHeader("Content-Type", "application/json");
return Api.Execute<List<TvMazeSearch>>(request, new Uri(Uri)); return Api.Execute<List<TvMazeSearch>>(request, new Uri(Uri));
} }
public TvMazeShow ShowLookup(int showId) public TvMazeShow ShowLookup(int showId)
{ {
var request = new RestRequest var request = new RestRequest
{ {
Method = Method.GET, Method = Method.GET,
Resource = "shows/{id}" Resource = "shows/{id}"
}; };
request.AddUrlSegment("id", showId.ToString()); request.AddUrlSegment("id", showId.ToString());
request.AddHeader("Content-Type", "application/json"); request.AddHeader("Content-Type", "application/json");
return Api.Execute<TvMazeShow>(request, new Uri(Uri)); return Api.Execute<TvMazeShow>(request, new Uri(Uri));
} }
public TvMazeShow ShowLookupByTheTvDbId(int theTvDbId) public IEnumerable<TvMazeEpisodes> EpisodeLookup(int showId)
{ {
var request = new RestRequest var request = new RestRequest
{ {
Method = Method.GET, Method = Method.GET,
Resource = "lookup/shows?thetvdb={id}" Resource = "shows/{id}/episodes"
}; };
request.AddUrlSegment("id", theTvDbId.ToString()); request.AddUrlSegment("id", showId.ToString());
request.AddHeader("Content-Type", "application/json"); request.AddHeader("Content-Type", "application/json");
var obj = Api.Execute<TvMazeShow>(request, new Uri(Uri)); return Api.Execute<List<TvMazeEpisodes>>(request, new Uri(Uri));
obj.seasonCount = GetSeasonCount(obj.id); }
return obj; public TvMazeShow ShowLookupByTheTvDbId(int theTvDbId)
} {
var request = new RestRequest
public List<TvMazeSeasons> GetSeasons(int id) {
{ Method = Method.GET,
var request = new RestRequest Resource = "lookup/shows?thetvdb={id}"
{ };
Method = Method.GET, request.AddUrlSegment("id", theTvDbId.ToString());
Resource = "shows/{id}/seasons" request.AddHeader("Content-Type", "application/json");
};
request.AddUrlSegment("id", id.ToString()); var obj = Api.Execute<TvMazeShow>(request, new Uri(Uri));
request.AddHeader("Content-Type", "application/json"); obj.seasonCount = GetSeasonCount(obj.id);
return Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri)); return obj;
} }
public int GetSeasonCount(int id)
{ public List<TvMazeSeasons> GetSeasons(int id)
var obj = GetSeasons(id); {
var seasons = obj.Select(x => x.number > 0); var request = new RestRequest
return seasons.Count(); {
} Method = Method.GET,
Resource = "shows/{id}/seasons"
} };
request.AddUrlSegment("id", id.ToString());
request.AddHeader("Content-Type", "application/json");
return Api.Execute<List<TvMazeSeasons>>(request, new Uri(Uri));
}
public int GetSeasonCount(int id)
{
var obj = GetSeasons(id);
var seasons = obj.Select(x => x.number > 0);
return seasons.Count();
}
}
} }

View file

@ -329,3 +329,7 @@ label {
.landing-title { .landing-title {
font-weight: bold; } font-weight: bold; }
.checkbox-custom {
margin-top: 0 !important;
margin-bottom: 0 !important; }

File diff suppressed because one or more lines are too long

View file

@ -384,7 +384,7 @@ $border-radius: 10px;
.bootstrap-datetimepicker-widget table td.active, .bootstrap-datetimepicker-widget table td.active,
.bootstrap-datetimepicker-widget table td.active:hover { .bootstrap-datetimepicker-widget table td.active:hover {
color: #fff !important; color: #fff $i;
} }
.landing-header { .landing-header {
@ -393,7 +393,7 @@ $border-radius: 10px;
} }
.landing-block { .landing-block {
background: #2f2f2f !important; background: #2f2f2f $i;
padding: 5px; padding: 5px;
} }
@ -415,3 +415,8 @@ $border-radius: 10px;
.landing-title { .landing-title {
font-weight: bold; font-weight: bold;
} }
.checkbox-custom{
margin-top:0 $i;
margin-bottom:0 $i;
}

View file

@ -12,9 +12,14 @@ $(function () {
var searchSource = $("#search-template").html(); var searchSource = $("#search-template").html();
var seasonsSource = $("#seasons-template").html(); var seasonsSource = $("#seasons-template").html();
var musicSource = $("#music-template").html(); var musicSource = $("#music-template").html();
var seasonsNumberSource = $("#seasonNumber-template").html();
var episodeSource = $("#episode-template").html();
var searchTemplate = Handlebars.compile(searchSource); var searchTemplate = Handlebars.compile(searchSource);
var musicTemplate = Handlebars.compile(musicSource); var musicTemplate = Handlebars.compile(musicSource);
var seasonsTemplate = Handlebars.compile(seasonsSource); var seasonsTemplate = Handlebars.compile(seasonsSource);
var seasonsNumberTemplate = Handlebars.compile(seasonsNumberSource);
var episodesTemplate = Handlebars.compile(episodeSource);
var base = $('#baseUrl').text(); var base = $('#baseUrl').text();
@ -256,6 +261,7 @@ $(function () {
$('#typeModal').val(type); $('#typeModal').val(type);
}); });
function focusSearch($content) { function focusSearch($content) {
if ($content.length > 0) { if ($content.length > 0) {
$('input[type=text].form-control', $content).first().focus(); $('input[type=text].form-control', $content).first().focus();
@ -531,4 +537,71 @@ $(function () {
}); });
$('#episodesModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var id = button.data('identifier'); // Extract info from data-* attributes
var url = createBaseUrl(base, '/search/episodes/');
var seenSeasons = [];
$.ajax({
type: "get",
url: url,
data: { tvId: id },
dataType: "json",
success: function (results) {
var $content = $("#episodesBody");
$content.html("");
$('#selectedEpisodeId').val(id);
results.forEach(function (result) {
var episodes = buildEpisodesView(result);
if (!seenSeasons.find(x => x === episodes.season)) {
// Create the seasons heading
seenSeasons.push(episodes.season);
var context = buildSeasonsCount(result);
$content.append(seasonsNumberTemplate(context));
}
var episodesResult = episodesTemplate(episodes);
$content.append(episodesResult);
});
},
error: function (e) {
console.log(e);
generateNotify("Something went wrong!", "danger");
}
});
function buildSeasonsContext(result) {
var context = {
id: result
};
return context;
};
function buildSeasonsCount(result) {
return {
seasonNumber: result.season
}
}
function buildEpisodesView(result) {
return {
id: result.id,
url: result.url,
name: result.name,
season: result.season,
number: result.number,
airdate: result.airdate,
airtime: result.airtime,
airstamp: result.airstamp,
runtime: result.runtime,
image: result.image,
summary: result.summary,
links : result._links
}
}
});
}); });

View file

@ -100,6 +100,7 @@ namespace PlexRequests.UI.Modules
IssueService = issue; IssueService = issue;
Analytics = a; Analytics = a;
RequestLimitRepo = rl; RequestLimitRepo = rl;
TvApi = new TvMazeApi();
Get["SearchIndex","/", true] = async (x, ct) => await RequestLoad(); Get["SearchIndex","/", true] = async (x, ct) => await RequestLoad();
@ -120,7 +121,9 @@ namespace PlexRequests.UI.Modules
Get["/notifyuser", true] = async (x, ct) => await GetUserNotificationSettings(); Get["/notifyuser", true] = async (x, ct) => await GetUserNotificationSettings();
Get["/seasons"] = x => GetSeasons(); Get["/seasons"] = x => GetSeasons();
Get["/episodes"] = x => GetEpisodes();
} }
private TvMazeApi TvApi { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private TheMovieDbApi MovieApi { get; } private TheMovieDbApi MovieApi { get; }
private INotificationService NotificationService { get; } private INotificationService NotificationService { get; }
@ -854,14 +857,21 @@ namespace PlexRequests.UI.Modules
private Response GetSeasons() private Response GetSeasons()
{ {
var tv = new TvMazeApi();
var seriesId = (int)Request.Query.tvId; var seriesId = (int)Request.Query.tvId;
var show = tv.ShowLookupByTheTvDbId(seriesId); var show = TvApi.ShowLookupByTheTvDbId(seriesId);
var seasons = tv.GetSeasons(show.id); var seasons = TvApi.GetSeasons(show.id);
var model = seasons.Select(x => x.number); var model = seasons.Select(x => x.number);
return Response.AsJson(model); return Response.AsJson(model);
} }
private Response GetEpisodes()
{
var seriesId = (int)Request.Query.tvId;
var show = TvApi.ShowLookupByTheTvDbId(seriesId);
var seasons = TvApi.EpisodeLookup(show.id);
return Response.AsJson(seasons);
}
private async Task<bool> CheckRequestLimit(PlexRequestSettings s, RequestType type) private async Task<bool> CheckRequestLimit(PlexRequestSettings s, RequestType type)
{ {
if (IsAdmin) if (IsAdmin)

View file

@ -888,6 +888,15 @@ namespace PlexRequests.UI.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Select Episode.
/// </summary>
public static string Search_SelectEpisode {
get {
return ResourceManager.GetString("Search_SelectEpisode", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Select . /// Looks up a localized string similar to Select .
/// </summary> /// </summary>

View file

@ -431,4 +431,7 @@
<data name="Layout_French" xml:space="preserve"> <data name="Layout_French" xml:space="preserve">
<value>French</value> <value>French</value>
</data> </data>
<data name="Search_SelectEpisode" xml:space="preserve">
<value>Select Episode</value>
</data>
</root> </root>

View file

@ -192,6 +192,7 @@
<li><a id="{{id}}" season-select="1" class="dropdownTv" href="#">@UI.Search_FirstSeason</a></li> <li><a id="{{id}}" season-select="1" class="dropdownTv" href="#">@UI.Search_FirstSeason</a></li>
<li><a id="{{id}}" season-select="2" class="dropdownTv" href="#">@UI.Search_LatestSeason</a></li> <li><a id="{{id}}" season-select="2" class="dropdownTv" href="#">@UI.Search_LatestSeason</a></li>
<li><a id="SeasonSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#seasonsModal" href="#">@UI.Search_SelectSeason...</a></li> <li><a id="SeasonSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#seasonsModal" href="#">@UI.Search_SelectSeason...</a></li>
<li><a id="EpisodeSelect" data-identifier="{{id}}" data-toggle="modal" data-target="#episodesModal" href="#">@UI.Search_SelectEpisode...</a></li>
</ul> </ul>
</div> </div>
{{/if_eq}} {{/if_eq}}
@ -291,6 +292,26 @@
</div> </div>
</div> </div>
<div class="modal fade" id="episodesModal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">@UI.Search_Modal_SeasonsTitle</h4>
</div>
<div class="modal-body" id="episodesBody">
</div>
<div hidden="hidden" id="selectedEpisodeId"></div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">@UI.Common_Close</button>
<button type="button" id="episodesRequest" class="btn btn-primary">@UI.Search_Request</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="issuesModal"> <div class="modal fade" id="issuesModal">
<div class="modal-dialog"> <div class="modal-dialog">
@ -321,6 +342,24 @@
<input type="checkbox" class="selectedSeasons" id="{{id}}" name="{{id}}"><label for="{{id}}">@UI.Search_Season {{id}}</label> <input type="checkbox" class="selectedSeasons" id="{{id}}" name="{{id}}"><label for="{{id}}">@UI.Search_Season {{id}}</label>
</div> </div>
</div> </div>
</script>
<script id="seasonNumber-template" type="text/x-handlebars-template">
<div class="row"></div>
<div id="seasonNumber{{seasonNumber}}">
<strong>@UI.Search_Season {{seasonNumber}}</strong>
</div>
</script>
<script id="episode-template" type="text/x-handlebars-template">
<div class="form-group col-md-6">
<div class="checkbox" style="margin-bottom:0px; margin-top:0px;">
<input type="checkbox" class="selectedEpisodes" id="{{id}}" name="{{id}}"><label for="{{id}}">{{number}}. {{name}}</label>
</div>
</div>
</script> </script>