mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-11 03:52:33 +02:00
First implementation of completely rewriting the way Radarr handles movies. Searching for new movies is now mostly feature complete.
This commit is contained in:
parent
0b278c7db8
commit
40d7590f80
@ -19,13 +19,15 @@ gulp.task('less', function() {
|
||||
paths.src.root + 'Series/series.less',
|
||||
paths.src.root + 'Activity/activity.less',
|
||||
paths.src.root + 'AddSeries/addSeries.less',
|
||||
paths.src.root + 'AddMovies/addMovies.less',
|
||||
paths.src.root + 'Calendar/calendar.less',
|
||||
paths.src.root + 'Cells/cells.less',
|
||||
paths.src.root + 'ManualImport/manualimport.less',
|
||||
paths.src.root + 'Settings/settings.less',
|
||||
paths.src.root + 'System/Logs/logs.less',
|
||||
paths.src.root + 'System/Update/update.less',
|
||||
paths.src.root + 'System/Info/info.less'
|
||||
paths.src.root + 'System/Info/info.less',
|
||||
|
||||
];
|
||||
|
||||
return gulp.src(src)
|
||||
|
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
@ -231,8 +231,10 @@
|
||||
<Compile Include="Series\SeasonResource.cs" />
|
||||
<Compile Include="SeasonPass\SeasonPassModule.cs" />
|
||||
<Compile Include="Series\SeriesEditorModule.cs" />
|
||||
<Compile Include="Series\MovieLookupModule.cs" />
|
||||
<Compile Include="Series\SeriesLookupModule.cs" />
|
||||
<Compile Include="Series\SeriesModule.cs" />
|
||||
<Compile Include="Series\MovieResource.cs" />
|
||||
<Compile Include="Series\SeriesResource.cs" />
|
||||
<Compile Include="Series\SeasonStatisticsResource.cs" />
|
||||
<Compile Include="System\Backup\BackupModule.cs" />
|
||||
|
44
src/NzbDrone.Api/Series/MovieLookupModule.cs
Normal file
44
src/NzbDrone.Api/Series/MovieLookupModule.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using Nancy;
|
||||
using NzbDrone.Api.Extensions;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class MovieLookupModule : NzbDroneRestModule<MovieResource>
|
||||
{
|
||||
private readonly ISearchForNewMovie _searchProxy;
|
||||
|
||||
public MovieLookupModule(ISearchForNewMovie searchProxy)
|
||||
: base("/movies/lookup")
|
||||
{
|
||||
_searchProxy = searchProxy;
|
||||
Get["/"] = x => Search();
|
||||
}
|
||||
|
||||
|
||||
private Response Search()
|
||||
{
|
||||
var imdbResults = _searchProxy.SearchForNewMovie((string)Request.Query.term);
|
||||
return MapToResource(imdbResults).AsResponse();
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<MovieResource> MapToResource(IEnumerable<Core.Tv.Movie> movies)
|
||||
{
|
||||
foreach (var currentSeries in movies)
|
||||
{
|
||||
var resource = currentSeries.ToResource();
|
||||
var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster);
|
||||
if (poster != null)
|
||||
{
|
||||
resource.RemotePoster = poster.Url;
|
||||
}
|
||||
|
||||
yield return resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
178
src/NzbDrone.Api/Series/MovieResource.cs
Normal file
178
src/NzbDrone.Api/Series/MovieResource.cs
Normal file
@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Api.REST;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
public class MovieResource : RestResource
|
||||
{
|
||||
public MovieResource()
|
||||
{
|
||||
Monitored = true;
|
||||
}
|
||||
|
||||
//Todo: Sorters should be done completely on the client
|
||||
//Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
|
||||
//Todo: We should get the entire Profile instead of ID and Name separately
|
||||
|
||||
//View Only
|
||||
public string Title { get; set; }
|
||||
public List<AlternateTitleResource> AlternateTitles { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
public long? SizeOnDisk { get; set; }
|
||||
public MovieStatusType Status { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public DateTime? InCinemas { get; set; }
|
||||
public List<MediaCover> Images { get; set; }
|
||||
|
||||
public string RemotePoster { get; set; }
|
||||
public int Year { get; set; }
|
||||
|
||||
//View & Edit
|
||||
public string Path { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
|
||||
//Editing Only
|
||||
public bool Monitored { get; set; }
|
||||
public int Runtime { get; set; }
|
||||
public DateTime? LastInfoSync { get; set; }
|
||||
public string CleanTitle { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public string Certification { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
|
||||
//TODO: Add series statistics as a property of the series (instead of individual properties)
|
||||
|
||||
//Used to support legacy consumers
|
||||
public int QualityProfileId
|
||||
{
|
||||
get
|
||||
{
|
||||
return ProfileId;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value > 0 && ProfileId == 0)
|
||||
{
|
||||
ProfileId = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MovieResourceMapper
|
||||
{
|
||||
public static MovieResource ToResource(this Core.Tv.Movie model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new MovieResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Title = model.Title,
|
||||
//AlternateTitles
|
||||
SortTitle = model.SortTitle,
|
||||
InCinemas = model.InCinemas,
|
||||
//TotalEpisodeCount
|
||||
//EpisodeCount
|
||||
//EpisodeFileCount
|
||||
//SizeOnDisk
|
||||
Status = model.Status,
|
||||
Overview = model.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
Images = model.Images,
|
||||
|
||||
Year = model.Year,
|
||||
|
||||
Path = model.Path,
|
||||
ProfileId = model.ProfileId,
|
||||
|
||||
Monitored = model.Monitored,
|
||||
|
||||
Runtime = model.Runtime,
|
||||
LastInfoSync = model.LastInfoSync,
|
||||
CleanTitle = model.CleanTitle,
|
||||
ImdbId = model.ImdbId,
|
||||
TitleSlug = model.TitleSlug,
|
||||
RootFolderPath = model.RootFolderPath,
|
||||
Certification = model.Certification,
|
||||
Genres = model.Genres,
|
||||
Tags = model.Tags,
|
||||
Added = model.Added,
|
||||
Ratings = model.Ratings
|
||||
};
|
||||
}
|
||||
|
||||
public static Core.Tv.Movie ToModel(this MovieResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new Core.Tv.Movie
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Title = resource.Title,
|
||||
//AlternateTitles
|
||||
SortTitle = resource.SortTitle,
|
||||
InCinemas = resource.InCinemas,
|
||||
//TotalEpisodeCount
|
||||
//EpisodeCount
|
||||
//EpisodeFileCount
|
||||
//SizeOnDisk
|
||||
Overview = resource.Overview,
|
||||
//NextAiring
|
||||
//PreviousAiring
|
||||
Images = resource.Images,
|
||||
|
||||
Year = resource.Year,
|
||||
|
||||
Path = resource.Path,
|
||||
ProfileId = resource.ProfileId,
|
||||
|
||||
Monitored = resource.Monitored,
|
||||
|
||||
Runtime = resource.Runtime,
|
||||
LastInfoSync = resource.LastInfoSync,
|
||||
CleanTitle = resource.CleanTitle,
|
||||
ImdbId = resource.ImdbId,
|
||||
TitleSlug = resource.TitleSlug,
|
||||
RootFolderPath = resource.RootFolderPath,
|
||||
Certification = resource.Certification,
|
||||
Genres = resource.Genres,
|
||||
Tags = resource.Tags,
|
||||
Added = resource.Added,
|
||||
Ratings = resource.Ratings
|
||||
};
|
||||
}
|
||||
|
||||
public static Core.Tv.Movie ToModel(this MovieResource resource, Core.Tv.Movie movie)
|
||||
{
|
||||
movie.ImdbId = resource.ImdbId;
|
||||
|
||||
movie.Path = resource.Path;
|
||||
movie.ProfileId = resource.ProfileId;
|
||||
|
||||
movie.Monitored = resource.Monitored;
|
||||
|
||||
movie.RootFolderPath = resource.RootFolderPath;
|
||||
movie.Tags = resource.Tags;
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
public static List<MovieResource> ToResource(this IEnumerable<Core.Tv.Movie> movies)
|
||||
{
|
||||
return movies.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
27
src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs
Normal file
27
src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
public class MovieNotFoundException : NzbDroneException
|
||||
{
|
||||
public string ImdbId { get; set; }
|
||||
|
||||
public MovieNotFoundException(string imdbid)
|
||||
: base(string.Format("Movie with imdbid {0} was not found, it may have been removed from IMDb.", imdbid))
|
||||
{
|
||||
ImdbId = imdbid;
|
||||
}
|
||||
|
||||
public MovieNotFoundException(string imdbid, string message, params object[] args)
|
||||
: base(message, args)
|
||||
{
|
||||
ImdbId = imdbid;
|
||||
}
|
||||
|
||||
public MovieNotFoundException(string imdbid, string message)
|
||||
: base(message)
|
||||
{
|
||||
ImdbId = imdbid;
|
||||
}
|
||||
}
|
||||
}
|
11
src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs
Normal file
11
src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource
|
||||
{
|
||||
public interface IProvideMovieInfo
|
||||
{
|
||||
Movie GetMovieInfo(string ImdbId);
|
||||
}
|
||||
}
|
10
src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs
Normal file
10
src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource
|
||||
{
|
||||
public interface ISearchForNewMovie
|
||||
{
|
||||
List<Movie> SearchForNewMovie(string title);
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries
|
||||
public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries, IProvideMovieInfo, ISearchForNewMovie
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
@ -38,11 +38,7 @@ public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId)
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
string imdbId = string.Format("tt{0:D7}", tvdbSeriesId);
|
||||
|
||||
var imdbRequest = new HttpRequest("http://www.omdbapi.com/?i="+ imdbId + "&plot=full&r=json");
|
||||
|
||||
var httpResponse = _httpClient.Get(imdbRequest);
|
||||
var httpResponse = _httpClient.Get<ShowResource>(httpRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
@ -56,49 +52,140 @@ public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId)
|
||||
}
|
||||
}
|
||||
|
||||
var episodes = httpResponse.Resource.Episodes.Select(MapEpisode);
|
||||
var series = MapSeries(httpResponse.Resource);
|
||||
|
||||
return new Tuple<Series, List<Episode>>(series, episodes.ToList());
|
||||
}
|
||||
|
||||
public Movie GetMovieInfo(string ImdbId)
|
||||
{
|
||||
var imdbRequest = new HttpRequest("http://www.omdbapi.com/?i=" + ImdbId + "&plot=full&r=json");
|
||||
|
||||
var httpResponse = _httpClient.Get(imdbRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new MovieNotFoundException(ImdbId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(imdbRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
var response = httpResponse.Content;
|
||||
|
||||
dynamic json = JsonConvert.DeserializeObject(response);
|
||||
|
||||
var series = new Series();
|
||||
var movie = new Movie();
|
||||
|
||||
series.Title = json.Title;
|
||||
series.TitleSlug = series.Title.ToLower().Replace(" ", "-");
|
||||
series.Overview = json.Plot;
|
||||
series.CleanTitle = Parser.Parser.CleanSeriesTitle(series.Title);
|
||||
series.TvdbId = tvdbSeriesId;
|
||||
movie.Title = json.Title;
|
||||
movie.TitleSlug = movie.Title.ToLower().Replace(" ", "-");
|
||||
movie.Overview = json.Plot;
|
||||
movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title);
|
||||
string airDateStr = json.Released;
|
||||
DateTime airDate = DateTime.Parse(airDateStr);
|
||||
series.FirstAired = airDate;
|
||||
series.Year = airDate.Year;
|
||||
series.ImdbId = imdbId;
|
||||
series.Images = new List<MediaCover.MediaCover>();
|
||||
movie.InCinemas = airDate;
|
||||
movie.Year = airDate.Year;
|
||||
movie.ImdbId = ImdbId;
|
||||
string imdbRating = json.imdbVotes;
|
||||
if (imdbRating == "N/A")
|
||||
{
|
||||
movie.Status = MovieStatusType.Announced;
|
||||
}
|
||||
else
|
||||
{
|
||||
movie.Status = MovieStatusType.Released;
|
||||
}
|
||||
string url = json.Poster;
|
||||
var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url);
|
||||
series.Images.Add(imdbPoster);
|
||||
movie.Images.Add(imdbPoster);
|
||||
string runtime = json.Runtime;
|
||||
int runtimeNum = 0;
|
||||
int.TryParse(runtime.Replace("min", "").Trim(), out runtimeNum);
|
||||
series.Runtime = runtimeNum;
|
||||
movie.Runtime = runtimeNum;
|
||||
|
||||
var season = new Season();
|
||||
season.SeasonNumber = 1;
|
||||
season.Monitored = true;
|
||||
series.Seasons.Add(season);
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
var episode = new Episode();
|
||||
public List<Movie> SearchForNewMovie(string title)
|
||||
{
|
||||
var lowerTitle = title.ToLower();
|
||||
|
||||
episode.AirDate = airDate.ToBestDateString();
|
||||
episode.Title = json.Title;
|
||||
episode.SeasonNumber = 1;
|
||||
episode.EpisodeNumber = 1;
|
||||
episode.Overview = series.Overview;
|
||||
episode.AirDate = airDate.ToShortDateString();
|
||||
if (lowerTitle.StartsWith("imdb:") || lowerTitle.StartsWith("imdbid:"))
|
||||
{
|
||||
var slug = lowerTitle.Split(':')[1].Trim();
|
||||
|
||||
var episodes = new List<Episode> { episode };
|
||||
string imdbid = slug;
|
||||
|
||||
return new Tuple<Series, List<Episode>>(series, episodes.ToList());
|
||||
if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace))
|
||||
{
|
||||
return new List<Movie>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new List<Movie> { GetMovieInfo(imdbid) };
|
||||
}
|
||||
catch (SeriesNotFoundException)
|
||||
{
|
||||
return new List<Movie>();
|
||||
}
|
||||
}
|
||||
|
||||
var searchTerm = lowerTitle.Replace("+", "_").Replace(" ", "_");
|
||||
|
||||
var firstChar = searchTerm.First();
|
||||
|
||||
var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/" + firstChar + "/" + searchTerm + ".json");
|
||||
|
||||
var response = _httpClient.Get(imdbRequest);
|
||||
|
||||
var imdbCallback = "imdb$" + searchTerm + "(";
|
||||
|
||||
var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")");
|
||||
|
||||
dynamic json = JsonConvert.DeserializeObject(responseCleaned);
|
||||
|
||||
var imdbMovies = new List<Movie>();
|
||||
|
||||
foreach (dynamic entry in json.d)
|
||||
{
|
||||
var imdbMovie = new Movie();
|
||||
imdbMovie.ImdbId = entry.id;
|
||||
try
|
||||
{
|
||||
imdbMovie.SortTitle = entry.l;
|
||||
imdbMovie.Title = entry.l;
|
||||
string titleSlug = entry.l;
|
||||
imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-");
|
||||
imdbMovie.Year = entry.y;
|
||||
imdbMovie.Images = new List<MediaCover.MediaCover>();
|
||||
try
|
||||
{
|
||||
string url = entry.i[0];
|
||||
var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url);
|
||||
imdbMovie.Images.Add(imdbPoster);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
imdbMovies.Add(imdbMovie);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return imdbMovies;
|
||||
}
|
||||
|
||||
public List<Series> SearchForNewSeries(string title)
|
||||
@ -128,70 +215,14 @@ public List<Series> SearchForNewSeries(string title)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", "search")
|
||||
.AddQueryParam("term", title.ToLower().Trim())
|
||||
.Build();
|
||||
|
||||
var searchTerm = lowerTitle.Replace("+", "_").Replace(" ", "_");
|
||||
|
||||
var firstChar = searchTerm.First();
|
||||
|
||||
var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/"+firstChar+"/" + searchTerm + ".json");
|
||||
|
||||
var response = _httpClient.Get(imdbRequest);
|
||||
|
||||
var imdbCallback = "imdb$" + searchTerm + "(";
|
||||
|
||||
var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")");
|
||||
|
||||
dynamic json = JsonConvert.DeserializeObject(responseCleaned);
|
||||
|
||||
var imdbMovies = new List<Series>();
|
||||
|
||||
foreach (dynamic entry in json.d)
|
||||
{
|
||||
var imdbMovie = new Series();
|
||||
imdbMovie.ImdbId = entry.id;
|
||||
string noTT = imdbMovie.ImdbId.Replace("tt", "");
|
||||
try
|
||||
{
|
||||
imdbMovie.TvdbId = (int)Double.Parse(noTT);
|
||||
}
|
||||
catch
|
||||
{
|
||||
imdbMovie.TvdbId = 0;
|
||||
}
|
||||
try
|
||||
{
|
||||
imdbMovie.SortTitle = entry.l;
|
||||
imdbMovie.Title = entry.l;
|
||||
string titleSlug = entry.l;
|
||||
imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-");
|
||||
imdbMovie.Year = entry.y;
|
||||
imdbMovie.Images = new List<MediaCover.MediaCover>();
|
||||
try
|
||||
{
|
||||
string url = entry.i[0];
|
||||
var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url);
|
||||
imdbMovie.Images.Add(imdbPoster);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
imdbMovies.Add(imdbMovie);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return imdbMovies;
|
||||
|
||||
|
||||
var httpResponse = _httpClient.Get<List<ShowResource>>(httpRequest);
|
||||
|
||||
|
@ -488,6 +488,7 @@
|
||||
<Compile Include="Exceptions\BadRequestException.cs" />
|
||||
<Compile Include="Exceptions\DownstreamException.cs" />
|
||||
<Compile Include="Exceptions\NzbDroneClientException.cs" />
|
||||
<Compile Include="Exceptions\MovieNotFoundExceptions.cs" />
|
||||
<Compile Include="Exceptions\SeriesNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\ReleaseDownloadException.cs" />
|
||||
<Compile Include="Exceptions\StatusCodeToExceptions.cs" />
|
||||
@ -778,6 +779,8 @@
|
||||
<Compile Include="Messaging\Events\IEventAggregator.cs" />
|
||||
<Compile Include="Messaging\Events\IHandle.cs" />
|
||||
<Compile Include="Messaging\IProcessMessage.cs" />
|
||||
<Compile Include="MetadataSource\IProvideMovieInfo.cs" />
|
||||
<Compile Include="MetadataSource\ISearchForNewMovie.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ActorResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\EpisodeResource.cs" />
|
||||
<Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" />
|
||||
@ -1067,6 +1070,7 @@
|
||||
<Compile Include="Tv\RefreshEpisodeService.cs" />
|
||||
<Compile Include="Tv\RefreshSeriesService.cs" />
|
||||
<Compile Include="Tv\Season.cs" />
|
||||
<Compile Include="Tv\Movie.cs" />
|
||||
<Compile Include="Tv\Series.cs" />
|
||||
<Compile Include="Tv\SeriesAddedHandler.cs" />
|
||||
<Compile Include="Tv\SeriesScannedHandler.cs" />
|
||||
@ -1075,6 +1079,7 @@
|
||||
<Compile Include="Tv\SeriesService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Tv\MovieStatusType.cs" />
|
||||
<Compile Include="Tv\SeriesStatusType.cs" />
|
||||
<Compile Include="Tv\SeriesTitleNormalizer.cs" />
|
||||
<Compile Include="Tv\SeriesTypes.cs" />
|
||||
|
50
src/NzbDrone.Core/Tv/Movie.cs
Normal file
50
src/NzbDrone.Core/Tv/Movie.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Marr.Data;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class Movie : ModelBase
|
||||
{
|
||||
public Movie()
|
||||
{
|
||||
Images = new List<MediaCover.MediaCover>();
|
||||
Genres = new List<string>();
|
||||
Actors = new List<Actor>();
|
||||
Tags = new HashSet<int>();
|
||||
}
|
||||
|
||||
public string ImdbId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string CleanTitle { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
public MovieStatusType Status { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public bool Monitored { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
public DateTime? LastInfoSync { get; set; }
|
||||
public int Runtime { get; set; }
|
||||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Path { get; set; }
|
||||
public int Year { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public List<Actor> Actors { get; set; }
|
||||
public string Certification { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public DateTime? InCinemas { get; set; }
|
||||
public LazyLoaded<Profile> Profile { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
// public AddMovieOptions AddOptions { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe());
|
||||
}
|
||||
}
|
||||
}
|
9
src/NzbDrone.Core/Tv/MovieStatusType.cs
Normal file
9
src/NzbDrone.Core/Tv/MovieStatusType.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public enum MovieStatusType
|
||||
{
|
||||
TBA = 0, //Nothing yet announced, only rumors, but still IMDb page
|
||||
Announced = 1, //AirDate is announced
|
||||
Released = 2 //Has at least one PreDB release
|
||||
}
|
||||
}
|
BIN
src/UI/.DS_Store
vendored
Normal file
BIN
src/UI/.DS_Store
vendored
Normal file
Binary file not shown.
22
src/UI/AddMovies/AddMoviesCollection.js
Normal file
22
src/UI/AddMovies/AddMoviesCollection.js
Normal file
@ -0,0 +1,22 @@
|
||||
var Backbone = require('backbone');
|
||||
var MovieModel = require('../Movies/MovieModel');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/movies/lookup',
|
||||
model : MovieModel,
|
||||
|
||||
parse : function(response) {
|
||||
var self = this;
|
||||
|
||||
_.each(response, function(model) {
|
||||
model.id = undefined;
|
||||
|
||||
if (self.unmappedFolderModel) {
|
||||
model.path = self.unmappedFolderModel.get('folder').path;
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
53
src/UI/AddMovies/AddMoviesLayout.js
Normal file
53
src/UI/AddMovies/AddMoviesLayout.js
Normal file
@ -0,0 +1,53 @@
|
||||
var vent = require('vent');
|
||||
var AppLayout = require('../AppLayout');
|
||||
var Marionette = require('marionette');
|
||||
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
|
||||
//var ExistingMoviesCollectionView = require('./Existing/AddExistingSeriesCollectionView');
|
||||
var AddMoviesView = require('./AddMoviesView');
|
||||
var ProfileCollection = require('../Profile/ProfileCollection');
|
||||
var RootFolderCollection = require('./RootFolders/RootFolderCollection');
|
||||
require('../Movies/MoviesCollection');
|
||||
|
||||
module.exports = Marionette.Layout.extend({
|
||||
template : 'AddMovies/AddMoviesLayoutTemplate',
|
||||
|
||||
regions : {
|
||||
workspace : '#add-movies-workspace'
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-import' : '_importMovies',
|
||||
'click .x-add-new' : '_addMovies'
|
||||
},
|
||||
|
||||
attributes : {
|
||||
id : 'add-movies-screen'
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
ProfileCollection.fetch();
|
||||
RootFolderCollection.fetch().done(function() {
|
||||
RootFolderCollection.synced = true;
|
||||
});
|
||||
},
|
||||
|
||||
onShow : function() {
|
||||
this.workspace.show(new AddMoviesView());
|
||||
},
|
||||
|
||||
_folderSelected : function(options) {
|
||||
//vent.trigger(vent.Commands.CloseModalCommand);
|
||||
//TODO: Fix this shit.
|
||||
//this.workspace.show(new ExistingMoviesCollectionView({ model : options.model }));
|
||||
},
|
||||
|
||||
_importMovies : function() {
|
||||
this.rootFolderLayout = new RootFolderLayout();
|
||||
this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected);
|
||||
AppLayout.modalRegion.show(this.rootFolderLayout);
|
||||
},
|
||||
|
||||
_addMovies : function() {
|
||||
this.workspace.show(new AddMoviesView());
|
||||
}
|
||||
});
|
16
src/UI/AddMovies/AddMoviesLayoutTemplate.hbs
Normal file
16
src/UI/AddMovies/AddMoviesLayoutTemplate.hbs
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="btn-group add-movies-btn-group btn-group-lg btn-block">
|
||||
<button type="button" class="btn btn-default col-md-10 col-xs-8 add-movies-import-btn x-import">
|
||||
<i class="icon-sonarr-hdd"/>
|
||||
Import existing movies on disk
|
||||
</button>
|
||||
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Movie</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="add-movies-workspace"></div>
|
||||
</div>
|
||||
</div>
|
183
src/UI/AddMovies/AddMoviesView.js
Normal file
183
src/UI/AddMovies/AddMoviesView.js
Normal file
@ -0,0 +1,183 @@
|
||||
var _ = require('underscore');
|
||||
var vent = require('vent');
|
||||
var Marionette = require('marionette');
|
||||
var AddMoviesCollection = require('./AddMoviesCollection');
|
||||
var SearchResultCollectionView = require('./SearchResultCollectionView');
|
||||
var EmptyView = require('./EmptyView');
|
||||
var NotFoundView = require('./NotFoundView');
|
||||
var ErrorView = require('./ErrorView');
|
||||
var LoadingView = require('../Shared/LoadingView');
|
||||
|
||||
module.exports = Marionette.Layout.extend({
|
||||
template : 'AddMovies/AddMoviesViewTemplate',
|
||||
|
||||
regions : {
|
||||
searchResult : '#search-result'
|
||||
},
|
||||
|
||||
ui : {
|
||||
moviesSearch : '.x-movies-search',
|
||||
searchBar : '.x-search-bar',
|
||||
loadMore : '.x-load-more'
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-load-more' : '_onLoadMore'
|
||||
},
|
||||
|
||||
initialize : function(options) {
|
||||
console.log(options)
|
||||
this.isExisting = options.isExisting;
|
||||
this.collection = new AddMoviesCollection();
|
||||
|
||||
if (this.isExisting) {
|
||||
this.collection.unmappedFolderModel = this.model;
|
||||
}
|
||||
|
||||
if (this.isExisting) {
|
||||
this.className = 'existing-movies';
|
||||
} else {
|
||||
this.className = 'new-movies';
|
||||
}
|
||||
|
||||
this.listenTo(vent, vent.Events.MoviesAdded, this._onMoviesAdded);
|
||||
this.listenTo(this.collection, 'sync', this._showResults);
|
||||
|
||||
this.resultCollectionView = new SearchResultCollectionView({
|
||||
collection : this.collection,
|
||||
isExisting : this.isExisting
|
||||
});
|
||||
|
||||
this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this);
|
||||
},
|
||||
|
||||
onRender : function() {
|
||||
var self = this;
|
||||
|
||||
this.$el.addClass(this.className);
|
||||
|
||||
this.ui.moviesSearch.keyup(function(e) {
|
||||
|
||||
if (_.contains([
|
||||
9,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
37,
|
||||
38,
|
||||
39,
|
||||
40,
|
||||
91,
|
||||
92,
|
||||
93
|
||||
], e.keyCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._abortExistingSearch();
|
||||
self.throttledSearch({
|
||||
term : self.ui.moviesSearch.val()
|
||||
});
|
||||
});
|
||||
|
||||
this._clearResults();
|
||||
|
||||
if (this.isExisting) {
|
||||
this.ui.searchBar.hide();
|
||||
}
|
||||
},
|
||||
|
||||
onShow : function() {
|
||||
this.ui.moviesSearch.focus();
|
||||
},
|
||||
|
||||
search : function(options) {
|
||||
var self = this;
|
||||
|
||||
this.collection.reset();
|
||||
|
||||
if (!options.term || options.term === this.collection.term) {
|
||||
return Marionette.$.Deferred().resolve();
|
||||
}
|
||||
|
||||
this.searchResult.show(new LoadingView());
|
||||
this.collection.term = options.term;
|
||||
this.currentSearchPromise = this.collection.fetch({
|
||||
data : { term : options.term }
|
||||
});
|
||||
|
||||
this.currentSearchPromise.fail(function() {
|
||||
self._showError();
|
||||
});
|
||||
|
||||
return this.currentSearchPromise;
|
||||
},
|
||||
|
||||
_onMoviesAdded : function(options) {
|
||||
if (this.isExisting && options.movies.get('path') === this.model.get('folder').path) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
else if (!this.isExisting) {
|
||||
this.collection.term = '';
|
||||
this.collection.reset();
|
||||
this._clearResults();
|
||||
this.ui.moviesSearch.val('');
|
||||
this.ui.moviesSearch.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onLoadMore : function() {
|
||||
var showingAll = this.resultCollectionView.showMore();
|
||||
this.ui.searchBar.show();
|
||||
|
||||
if (showingAll) {
|
||||
this.ui.loadMore.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_clearResults : function() {
|
||||
if (!this.isExisting) {
|
||||
this.searchResult.show(new EmptyView());
|
||||
} else {
|
||||
this.searchResult.close();
|
||||
}
|
||||
},
|
||||
|
||||
_showResults : function() {
|
||||
if (!this.isClosed) {
|
||||
if (this.collection.length === 0) {
|
||||
this.ui.searchBar.show();
|
||||
this.searchResult.show(new NotFoundView({ term : this.collection.term }));
|
||||
} else {
|
||||
this.searchResult.show(this.resultCollectionView);
|
||||
if (!this.showingAll && this.isExisting) {
|
||||
this.ui.loadMore.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_abortExistingSearch : function() {
|
||||
if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) {
|
||||
console.log('aborting previous pending search request.');
|
||||
this.currentSearchPromise.abort();
|
||||
} else {
|
||||
this._clearResults();
|
||||
}
|
||||
},
|
||||
|
||||
_showError : function() {
|
||||
if (!this.isClosed) {
|
||||
this.ui.searchBar.show();
|
||||
this.searchResult.show(new ErrorView({ term : this.collection.term }));
|
||||
this.collection.term = '';
|
||||
}
|
||||
}
|
||||
});
|
24
src/UI/AddMovies/AddMoviesViewTemplate.hbs
Normal file
24
src/UI/AddMovies/AddMoviesViewTemplate.hbs
Normal file
@ -0,0 +1,24 @@
|
||||
{{#if folder.path}}
|
||||
<div class="unmapped-folder-path">
|
||||
<div class="col-md-12">
|
||||
{{folder.path}}
|
||||
</div>
|
||||
</div>{{/if}}
|
||||
<div class="x-search-bar">
|
||||
<div class="input-group input-group-lg add-movies-search">
|
||||
<span class="input-group-addon"><i class="icon-sonarr-search"/></span>
|
||||
|
||||
{{#if folder}}
|
||||
<input type="text" class="form-control x-movies-search" value="{{folder.name}}">
|
||||
{{else}}
|
||||
<input type="text" class="form-control x-movies-search" placeholder="Start typing the name of the movie you want to add ...">
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="search-result" class="result-list col-md-12"/>
|
||||
</div>
|
||||
<div class="btn btn-block text-center new-movies-loadmore x-load-more" style="display: none;">
|
||||
<i class="icon-sonarr-load-more"/>
|
||||
more
|
||||
</div>
|
5
src/UI/AddMovies/EmptyView.js
Normal file
5
src/UI/AddMovies/EmptyView.js
Normal file
@ -0,0 +1,5 @@
|
||||
var Marionette = require('marionette');
|
||||
|
||||
module.exports = Marionette.CompositeView.extend({
|
||||
template : 'AddMovies/EmptyViewTemplate'
|
||||
});
|
3
src/UI/AddMovies/EmptyViewTemplate.hbs
Normal file
3
src/UI/AddMovies/EmptyViewTemplate.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="text-center hint col-md-12">
|
||||
<span>You can also search by imdbid using the imdb: prefixes.</span>
|
||||
</div>
|
13
src/UI/AddMovies/ErrorView.js
Normal file
13
src/UI/AddMovies/ErrorView.js
Normal file
@ -0,0 +1,13 @@
|
||||
var Marionette = require('marionette');
|
||||
|
||||
module.exports = Marionette.CompositeView.extend({
|
||||
template : 'AddMovies/ErrorViewTemplate',
|
||||
|
||||
initialize : function(options) {
|
||||
this.options = options;
|
||||
},
|
||||
|
||||
templateHelpers : function() {
|
||||
return this.options;
|
||||
}
|
||||
});
|
7
src/UI/AddMovies/ErrorViewTemplate.hbs
Normal file
7
src/UI/AddMovies/ErrorViewTemplate.hbs
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="text-center col-md-12">
|
||||
<h3>
|
||||
There was an error searching for '{{term}}'.
|
||||
</h3>
|
||||
|
||||
If the movie title contains non-alphanumeric characters try removing them, otherwise try your search again later.
|
||||
</div>
|
51
src/UI/AddMovies/Existing/AddExistingSeriesCollectionView.js
Normal file
51
src/UI/AddMovies/Existing/AddExistingSeriesCollectionView.js
Normal file
@ -0,0 +1,51 @@
|
||||
var Marionette = require('marionette');
|
||||
var AddSeriesView = require('../AddSeriesView');
|
||||
var UnmappedFolderCollection = require('./UnmappedFolderCollection');
|
||||
|
||||
module.exports = Marionette.CompositeView.extend({
|
||||
itemView : AddSeriesView,
|
||||
itemViewContainer : '.x-loading-folders',
|
||||
template : 'AddSeries/Existing/AddExistingSeriesCollectionViewTemplate',
|
||||
|
||||
ui : {
|
||||
loadingFolders : '.x-loading-folders'
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
this.collection = new UnmappedFolderCollection();
|
||||
this.collection.importItems(this.model);
|
||||
},
|
||||
|
||||
showCollection : function() {
|
||||
this._showAndSearch(0);
|
||||
},
|
||||
|
||||
appendHtml : function(collectionView, itemView, index) {
|
||||
collectionView.ui.loadingFolders.before(itemView.el);
|
||||
},
|
||||
|
||||
_showAndSearch : function(index) {
|
||||
var self = this;
|
||||
var model = this.collection.at(index);
|
||||
|
||||
if (model) {
|
||||
var currentIndex = index;
|
||||
var folderName = model.get('folder').name;
|
||||
this.addItemView(model, this.getItemView(), index);
|
||||
this.children.findByModel(model).search({ term : folderName }).always(function() {
|
||||
if (!self.isClosed) {
|
||||
self._showAndSearch(currentIndex + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.loadingFolders.hide();
|
||||
}
|
||||
},
|
||||
|
||||
itemViewOptions : {
|
||||
isExisting : true
|
||||
}
|
||||
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
<div class="x-existing-folders">
|
||||
<div class="loading-folders x-loading-folders">
|
||||
Loading search results from TheTVDB for your series, this may take a few minutes.
|
||||
</div>
|
||||
</div>
|
20
src/UI/AddMovies/Existing/UnmappedFolderCollection.js
Normal file
20
src/UI/AddMovies/Existing/UnmappedFolderCollection.js
Normal file
@ -0,0 +1,20 @@
|
||||
var Backbone = require('backbone');
|
||||
var UnmappedFolderModel = require('./UnmappedFolderModel');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
model : UnmappedFolderModel,
|
||||
|
||||
importItems : function(rootFolderModel) {
|
||||
|
||||
this.reset();
|
||||
var rootFolder = rootFolderModel;
|
||||
|
||||
_.each(rootFolderModel.get('unmappedFolders'), function(folder) {
|
||||
this.push(new UnmappedFolderModel({
|
||||
rootFolder : rootFolder,
|
||||
folder : folder
|
||||
}));
|
||||
}, this);
|
||||
}
|
||||
});
|
3
src/UI/AddMovies/Existing/UnmappedFolderModel.js
Normal file
3
src/UI/AddMovies/Existing/UnmappedFolderModel.js
Normal file
@ -0,0 +1,3 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({});
|
18
src/UI/AddMovies/MonitoringTooltipTemplate.hbs
Normal file
18
src/UI/AddMovies/MonitoringTooltipTemplate.hbs
Normal file
@ -0,0 +1,18 @@
|
||||
<dl class="monitor-tooltip-contents">
|
||||
<dt>All</dt>
|
||||
<dd>Monitor all episodes except specials</dd>
|
||||
<dt>Future</dt>
|
||||
<dd>Monitor episodes that have not aired yet</dd>
|
||||
<dt>Missing</dt>
|
||||
<dd>Monitor episodes that do not have files or have not aired yet</dd>
|
||||
<dt>Existing</dt>
|
||||
<dd>Monitor episodes that have files or have not aired yet</dd>
|
||||
<dt>First Season</dt>
|
||||
<dd>Monitor all episodes of the first season. All other seasons will be ignored</dd>
|
||||
<dt>Latest Season</dt>
|
||||
<dd>Monitor all episodes of the latest season and future seasons</dd>
|
||||
<dt>None</dt>
|
||||
<dd>No episodes will be monitored.</dd>
|
||||
<!--<dt>Latest Season</dt>-->
|
||||
<!--<dd>Monitor all episodes the latest season only, previous seasons will be ignored</dd>-->
|
||||
</dl>
|
3
src/UI/AddMovies/MoviesTypeSelectionPartial.hbs
Normal file
3
src/UI/AddMovies/MoviesTypeSelectionPartial.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
<select class="form-control col-md-2 x-movie-type" name="movieType">
|
||||
<option value="standard">Standard</option>
|
||||
</select>
|
13
src/UI/AddMovies/NotFoundView.js
Normal file
13
src/UI/AddMovies/NotFoundView.js
Normal file
@ -0,0 +1,13 @@
|
||||
var Marionette = require('marionette');
|
||||
|
||||
module.exports = Marionette.CompositeView.extend({
|
||||
template : 'AddMovies/NotFoundViewTemplate',
|
||||
|
||||
initialize : function(options) {
|
||||
this.options = options;
|
||||
},
|
||||
|
||||
templateHelpers : function() {
|
||||
return this.options;
|
||||
}
|
||||
});
|
7
src/UI/AddMovies/NotFoundViewTemplate.hbs
Normal file
7
src/UI/AddMovies/NotFoundViewTemplate.hbs
Normal file
@ -0,0 +1,7 @@
|
||||
<div class="text-center col-md-12">
|
||||
<h3>
|
||||
Sorry. We couldn't find any movies matching '{{term}}'
|
||||
</h3>
|
||||
<a href="https://github.com/NzbDrone/NzbDrone/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">Why can't I find my show?</a>
|
||||
|
||||
</div>
|
10
src/UI/AddMovies/RootFolders/RootFolderCollection.js
Normal file
10
src/UI/AddMovies/RootFolders/RootFolderCollection.js
Normal file
@ -0,0 +1,10 @@
|
||||
var Backbone = require('backbone');
|
||||
var RootFolderModel = require('./RootFolderModel');
|
||||
require('../../Mixins/backbone.signalr.mixin');
|
||||
|
||||
var RootFolderCollection = Backbone.Collection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/rootfolder',
|
||||
model : RootFolderModel
|
||||
});
|
||||
|
||||
module.exports = new RootFolderCollection();
|
8
src/UI/AddMovies/RootFolders/RootFolderCollectionView.js
Normal file
8
src/UI/AddMovies/RootFolders/RootFolderCollectionView.js
Normal file
@ -0,0 +1,8 @@
|
||||
var Marionette = require('marionette');
|
||||
var RootFolderItemView = require('./RootFolderItemView');
|
||||
|
||||
module.exports = Marionette.CompositeView.extend({
|
||||
template : 'AddSeries/RootFolders/RootFolderCollectionViewTemplate',
|
||||
itemViewContainer : '.x-root-folders',
|
||||
itemView : RootFolderItemView
|
||||
});
|
@ -0,0 +1,13 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-10 ">
|
||||
Path
|
||||
</th>
|
||||
<th class="col-md-3">
|
||||
Free Space
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="x-root-folders"></tbody>
|
||||
</table>
|
28
src/UI/AddMovies/RootFolders/RootFolderItemView.js
Normal file
28
src/UI/AddMovies/RootFolders/RootFolderItemView.js
Normal file
@ -0,0 +1,28 @@
|
||||
var Marionette = require('marionette');
|
||||
|
||||
module.exports = Marionette.ItemView.extend({
|
||||
template : 'AddSeries/RootFolders/RootFolderItemViewTemplate',
|
||||
className : 'recent-folder',
|
||||
tagName : 'tr',
|
||||
|
||||
initialize : function() {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-delete' : 'removeFolder',
|
||||
'click .x-folder' : 'folderSelected'
|
||||
},
|
||||
|
||||
removeFolder : function() {
|
||||
var self = this;
|
||||
|
||||
this.model.destroy().success(function() {
|
||||
self.close();
|
||||
});
|
||||
},
|
||||
|
||||
folderSelected : function() {
|
||||
this.trigger('folderSelected', this.model);
|
||||
}
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
<td class="col-md-10 x-folder folder-path">
|
||||
{{path}}
|
||||
</td>
|
||||
<td class="col-md-3 x-folder folder-free-space">
|
||||
<span>{{Bytes freeSpace}}</span>
|
||||
</td>
|
||||
<td class="col-md-1">
|
||||
<i class="icon-sonarr-delete x-delete"></i>
|
||||
</td>
|
77
src/UI/AddMovies/RootFolders/RootFolderLayout.js
Normal file
77
src/UI/AddMovies/RootFolders/RootFolderLayout.js
Normal file
@ -0,0 +1,77 @@
|
||||
var Marionette = require('marionette');
|
||||
var RootFolderCollectionView = require('./RootFolderCollectionView');
|
||||
var RootFolderCollection = require('./RootFolderCollection');
|
||||
var RootFolderModel = require('./RootFolderModel');
|
||||
var LoadingView = require('../../Shared/LoadingView');
|
||||
var AsValidatedView = require('../../Mixins/AsValidatedView');
|
||||
require('../../Mixins/FileBrowser');
|
||||
|
||||
var Layout = Marionette.Layout.extend({
|
||||
template : 'AddSeries/RootFolders/RootFolderLayoutTemplate',
|
||||
|
||||
ui : {
|
||||
pathInput : '.x-path'
|
||||
},
|
||||
|
||||
regions : {
|
||||
currentDirs : '#current-dirs'
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-add' : '_addFolder',
|
||||
'keydown .x-path input' : '_keydown'
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
this.collection = RootFolderCollection;
|
||||
this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection });
|
||||
|
||||
this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected);
|
||||
},
|
||||
|
||||
onShow : function() {
|
||||
this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs);
|
||||
this.currentDirs.show(new LoadingView());
|
||||
|
||||
if (RootFolderCollection.synced) {
|
||||
this._showCurrentDirs();
|
||||
}
|
||||
|
||||
this.ui.pathInput.fileBrowser();
|
||||
},
|
||||
|
||||
_onFolderSelected : function(options) {
|
||||
this.trigger('folderSelected', options);
|
||||
},
|
||||
|
||||
_addFolder : function() {
|
||||
var self = this;
|
||||
|
||||
var newDir = new RootFolderModel({
|
||||
Path : this.ui.pathInput.val()
|
||||
});
|
||||
|
||||
this.bindToModelValidation(newDir);
|
||||
|
||||
newDir.save().done(function() {
|
||||
RootFolderCollection.add(newDir);
|
||||
self.trigger('folderSelected', { model : newDir });
|
||||
});
|
||||
},
|
||||
|
||||
_showCurrentDirs : function() {
|
||||
this.currentDirs.show(this.rootfolderListView);
|
||||
},
|
||||
|
||||
_keydown : function(e) {
|
||||
if (e.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._addFolder();
|
||||
}
|
||||
});
|
||||
|
||||
var Layout = AsValidatedView.apply(Layout);
|
||||
|
||||
module.exports = Layout;
|
36
src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs
Normal file
36
src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs
Normal file
@ -0,0 +1,36 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>Select Folder</h3>
|
||||
</div>
|
||||
<div class="modal-body root-folders-modal">
|
||||
<div class="validation-errors"></div>
|
||||
<div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
|
||||
<div class="col-md-12">
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"> <i class="icon-sonarr-folder-open"></i></span>
|
||||
<input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows">
|
||||
<span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-sonarr-ok"/></button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row root-folders">
|
||||
<div class="col-md-12">
|
||||
{{#if items}}
|
||||
<h4>Recent Folders</h4>
|
||||
{{/if}}
|
||||
<div id="current-dirs" class="root-folders-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
8
src/UI/AddMovies/RootFolders/RootFolderModel.js
Normal file
8
src/UI/AddMovies/RootFolders/RootFolderModel.js
Normal file
@ -0,0 +1,8 @@
|
||||
var Backbone = require('backbone');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
urlRoot : window.NzbDrone.ApiRoot + '/rootfolder',
|
||||
defaults : {
|
||||
freeSpace : 0
|
||||
}
|
||||
});
|
11
src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs
Normal file
11
src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs
Normal file
@ -0,0 +1,11 @@
|
||||
<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath">
|
||||
{{#if this}}
|
||||
{{#each this}}
|
||||
<option value="{{id}}">{{path}}</option>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<option value="">Select Path</option>
|
||||
{{/if}}
|
||||
<option value="addNew">Add a different path</option>
|
||||
</select>
|
||||
|
29
src/UI/AddMovies/SearchResultCollectionView.js
Normal file
29
src/UI/AddMovies/SearchResultCollectionView.js
Normal file
@ -0,0 +1,29 @@
|
||||
var Marionette = require('marionette');
|
||||
var SearchResultView = require('./SearchResultView');
|
||||
|
||||
module.exports = Marionette.CollectionView.extend({
|
||||
itemView : SearchResultView,
|
||||
|
||||
initialize : function(options) {
|
||||
this.isExisting = options.isExisting;
|
||||
this.showing = 1;
|
||||
},
|
||||
|
||||
showAll : function() {
|
||||
this.showingAll = true;
|
||||
this.render();
|
||||
},
|
||||
|
||||
showMore : function() {
|
||||
this.showing += 5;
|
||||
this.render();
|
||||
|
||||
return this.showing >= this.collection.length;
|
||||
},
|
||||
|
||||
appendHtml : function(collectionView, itemView, index) {
|
||||
if (!this.isExisting || index < this.showing || index === 0) {
|
||||
collectionView.$el.append(itemView.el);
|
||||
}
|
||||
}
|
||||
});
|
272
src/UI/AddMovies/SearchResultView.js
Normal file
272
src/UI/AddMovies/SearchResultView.js
Normal file
@ -0,0 +1,272 @@
|
||||
var _ = require('underscore');
|
||||
var vent = require('vent');
|
||||
var AppLayout = require('../AppLayout');
|
||||
var Backbone = require('backbone');
|
||||
var Marionette = require('marionette');
|
||||
var Profiles = require('../Profile/ProfileCollection');
|
||||
var RootFolders = require('./RootFolders/RootFolderCollection');
|
||||
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
|
||||
var MoviesCollection = require('../Movies/MoviesCollection');
|
||||
var Config = require('../Config');
|
||||
var Messenger = require('../Shared/Messenger');
|
||||
var AsValidatedView = require('../Mixins/AsValidatedView');
|
||||
|
||||
require('jquery.dotdotdot');
|
||||
|
||||
var view = Marionette.ItemView.extend({
|
||||
|
||||
template : 'AddMovies/SearchResultViewTemplate',
|
||||
|
||||
ui : {
|
||||
profile : '.x-profile',
|
||||
rootFolder : '.x-root-folder',
|
||||
seasonFolder : '.x-season-folder',
|
||||
monitor : '.x-monitor',
|
||||
monitorTooltip : '.x-monitor-tooltip',
|
||||
addButton : '.x-add',
|
||||
addSearchButton : '.x-add-search',
|
||||
overview : '.x-overview'
|
||||
},
|
||||
|
||||
events : {
|
||||
'click .x-add' : '_addWithoutSearch',
|
||||
'click .x-add-search' : '_addAndSearch',
|
||||
'change .x-profile' : '_profileChanged',
|
||||
'change .x-root-folder' : '_rootFolderChanged',
|
||||
'change .x-season-folder' : '_seasonFolderChanged',
|
||||
'change .x-monitor' : '_monitorChanged'
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
|
||||
if (!this.model) {
|
||||
throw 'model is required';
|
||||
}
|
||||
|
||||
this.templateHelpers = {};
|
||||
this._configureTemplateHelpers();
|
||||
|
||||
this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated);
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(RootFolders, 'all', this._rootFoldersUpdated);
|
||||
},
|
||||
|
||||
onRender : function() {
|
||||
|
||||
var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId);
|
||||
var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId);
|
||||
var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true);
|
||||
var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing');
|
||||
|
||||
if (Profiles.get(defaultProfile)) {
|
||||
this.ui.profile.val(defaultProfile);
|
||||
}
|
||||
|
||||
if (RootFolders.get(defaultRoot)) {
|
||||
this.ui.rootFolder.val(defaultRoot);
|
||||
}
|
||||
|
||||
this.ui.seasonFolder.prop('checked', useSeasonFolder);
|
||||
this.ui.monitor.val(defaultMonitorEpisodes);
|
||||
|
||||
//TODO: make this work via onRender, FM?
|
||||
//works with onShow, but stops working after the first render
|
||||
this.ui.overview.dotdotdot({
|
||||
height : 120
|
||||
});
|
||||
|
||||
this.templateFunction = Marionette.TemplateCache.get('AddMovies/MonitoringTooltipTemplate');
|
||||
var content = this.templateFunction();
|
||||
|
||||
this.ui.monitorTooltip.popover({
|
||||
content : content,
|
||||
html : true,
|
||||
trigger : 'hover',
|
||||
title : 'Episode Monitoring Options',
|
||||
placement : 'right',
|
||||
container : this.$el
|
||||
});
|
||||
},
|
||||
|
||||
_configureTemplateHelpers : function() {
|
||||
var existingMovies = MoviesCollection.where({ imdbId : this.model.get('imdbId') });
|
||||
console.log(existingMovies)
|
||||
if (existingMovies.length > 0) {
|
||||
this.templateHelpers.existing = existingMovies[0].toJSON();
|
||||
}
|
||||
|
||||
this.templateHelpers.profiles = Profiles.toJSON();
|
||||
console.log(this.model)
|
||||
console.log(this.templateHelpers.existing)
|
||||
if (!this.model.get('isExisting')) {
|
||||
this.templateHelpers.rootFolders = RootFolders.toJSON();
|
||||
}
|
||||
},
|
||||
|
||||
_onConfigUpdated : function(options) {
|
||||
if (options.key === Config.Keys.DefaultProfileId) {
|
||||
this.ui.profile.val(options.value);
|
||||
}
|
||||
|
||||
else if (options.key === Config.Keys.DefaultRootFolderId) {
|
||||
this.ui.rootFolder.val(options.value);
|
||||
}
|
||||
|
||||
else if (options.key === Config.Keys.UseSeasonFolder) {
|
||||
this.ui.seasonFolder.prop('checked', options.value);
|
||||
}
|
||||
|
||||
else if (options.key === Config.Keys.MonitorEpisodes) {
|
||||
this.ui.monitor.val(options.value);
|
||||
}
|
||||
},
|
||||
|
||||
_profileChanged : function() {
|
||||
Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val());
|
||||
},
|
||||
|
||||
_seasonFolderChanged : function() {
|
||||
Config.setValue(Config.Keys.UseSeasonFolder, this.ui.seasonFolder.prop('checked'));
|
||||
},
|
||||
|
||||
_rootFolderChanged : function() {
|
||||
var rootFolderValue = this.ui.rootFolder.val();
|
||||
if (rootFolderValue === 'addNew') {
|
||||
var rootFolderLayout = new RootFolderLayout();
|
||||
this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder);
|
||||
AppLayout.modalRegion.show(rootFolderLayout);
|
||||
} else {
|
||||
Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue);
|
||||
}
|
||||
},
|
||||
|
||||
_monitorChanged : function() {
|
||||
Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val());
|
||||
},
|
||||
|
||||
_setRootFolder : function(options) {
|
||||
vent.trigger(vent.Commands.CloseModalCommand);
|
||||
this.ui.rootFolder.val(options.model.id);
|
||||
this._rootFolderChanged();
|
||||
},
|
||||
|
||||
_addWithoutSearch : function() {
|
||||
this._addMovies(true);
|
||||
},
|
||||
|
||||
_addAndSearch : function() {
|
||||
this._addMovies(true);
|
||||
},
|
||||
|
||||
_addMovies : function(searchForMissingEpisodes) {
|
||||
var addButton = this.ui.addButton;
|
||||
var addSearchButton = this.ui.addSearchButton;
|
||||
|
||||
addButton.addClass('disabled');
|
||||
addSearchButton.addClass('disabled');
|
||||
|
||||
var profile = this.ui.profile.val();
|
||||
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
|
||||
|
||||
var options = this._getAddMoviesOptions();
|
||||
options.searchForMissingEpisodes = searchForMissingEpisodes;
|
||||
|
||||
this.model.set({
|
||||
profileId : profile,
|
||||
rootFolderPath : rootFolderPath,
|
||||
addOptions : options,
|
||||
monitored : true
|
||||
}, { silent : true });
|
||||
|
||||
var self = this;
|
||||
var promise = this.model.save();
|
||||
|
||||
console.log(this.model.save);
|
||||
console.log(promise);
|
||||
|
||||
if (searchForMissingEpisodes) {
|
||||
this.ui.addSearchButton.spinForPromise(promise);
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.addButton.spinForPromise(promise);
|
||||
}
|
||||
|
||||
promise.always(function() {
|
||||
addButton.removeClass('disabled');
|
||||
addSearchButton.removeClass('disabled');
|
||||
});
|
||||
|
||||
promise.done(function() {
|
||||
MoviesCollection.add(self.model);
|
||||
|
||||
self.close();
|
||||
|
||||
Messenger.show({
|
||||
message : 'Added: ' + self.model.get('title'),
|
||||
actions : {
|
||||
goToSeries : {
|
||||
label : 'Go to Movie',
|
||||
action : function() {
|
||||
Backbone.history.navigate('/movies/' + self.model.get('titleSlug'), { trigger : true });
|
||||
}
|
||||
}
|
||||
},
|
||||
hideAfter : 8,
|
||||
hideOnNavigate : true
|
||||
});
|
||||
|
||||
vent.trigger(vent.Events.MoviesAdded, { movie : self.model });
|
||||
});
|
||||
},
|
||||
|
||||
_rootFoldersUpdated : function() {
|
||||
this._configureTemplateHelpers();
|
||||
this.render();
|
||||
},
|
||||
|
||||
_getAddMoviesOptions : function() {
|
||||
var monitor = this.ui.monitor.val();
|
||||
|
||||
var options = {
|
||||
ignoreEpisodesWithFiles : false,
|
||||
ignoreEpisodesWithoutFiles : false
|
||||
};
|
||||
|
||||
if (monitor === 'all') {
|
||||
return options;
|
||||
}
|
||||
|
||||
else if (monitor === 'future') {
|
||||
options.ignoreEpisodesWithFiles = true;
|
||||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'latest') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber);
|
||||
}
|
||||
|
||||
else if (monitor === 'first') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
|
||||
this.model.setSeasonMonitored(firstSeason.seasonNumber);
|
||||
}
|
||||
|
||||
else if (monitor === 'missing') {
|
||||
options.ignoreEpisodesWithFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'existing') {
|
||||
options.ignoreEpisodesWithoutFiles = true;
|
||||
}
|
||||
|
||||
else if (monitor === 'none') {
|
||||
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
});
|
||||
|
||||
AsValidatedView.apply(view);
|
||||
|
||||
module.exports = view;
|
101
src/UI/AddMovies/SearchResultViewTemplate.hbs
Normal file
101
src/UI/AddMovies/SearchResultViewTemplate.hbs
Normal file
@ -0,0 +1,101 @@
|
||||
<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<a href="{{imdbUrl}}" target="_blank">
|
||||
{{poster}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 class="movies-title">
|
||||
{{titleWithYear}}
|
||||
|
||||
<span class="labels">
|
||||
<span class="label label-default">{{network}}</span>
|
||||
{{#unless_eq status compare="announced"}}
|
||||
<span class="label label-danger">Released</span> <!-- TODO: Better handling of cases here! -->
|
||||
{{/unless_eq}}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row new-movies-overview x-overview">
|
||||
<div class="col-md-12 overview-internal">
|
||||
{{overview}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{{#unless existing}}
|
||||
{{#unless path}}
|
||||
<div class="form-group col-md-4">
|
||||
<label>Path</label>
|
||||
{{> RootFolderSelectionPartial rootFolders}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label>Monitor <i class="icon-sonarr-form-info monitor-tooltip x-monitor-tooltip"></i></label>
|
||||
<select class="form-control col-md-2 x-monitor">
|
||||
<option value="all">All</option>
|
||||
<option value="missing">Missing</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label>Profile</label>
|
||||
{{> ProfileSelectionPartial profiles}}
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label>Season Folders</label>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="checkbox toggle well">
|
||||
<input type="checkbox" class="x-season-folder"/>
|
||||
<p>
|
||||
<span>Yes</span>
|
||||
<span>No</span>
|
||||
</p>
|
||||
<div class="btn btn-primary slide-button"/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#unless existing}}
|
||||
{{#if title}}
|
||||
<div class="form-group col-md-2">
|
||||
<!--Uncomment if we need to add even more controls to add Movies-->
|
||||
<label style="visibility: hidden">Add</label>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success add x-add" title="Add">
|
||||
<i class="icon-sonarr-add"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success add x-add-search" title="Add and Search for missing episodes">
|
||||
<i class="icon-sonarr-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<label style="visibility: hidden">Add</label>
|
||||
<div class="col-md-2" title="Movies require an English title">
|
||||
<button class="btn add-movies disabled">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<label style="visibility: hidden">Add</label>
|
||||
<div class="col-md-2 col-md-offset-10">
|
||||
<a class="btn btn-default" href="{{route}}">
|
||||
Already Exists
|
||||
</a>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
src/UI/AddMovies/StartingSeasonSelectionPartial.hbs
Normal file
13
src/UI/AddMovies/StartingSeasonSelectionPartial.hbs
Normal file
@ -0,0 +1,13 @@
|
||||
<select class="form-control col-md-2 starting-season x-starting-season">
|
||||
|
||||
|
||||
{{#each this}}
|
||||
{{#if_eq seasonNumber compare="0"}}
|
||||
<option value="{{seasonNumber}}">Specials</option>
|
||||
{{else}}
|
||||
<option value="{{seasonNumber}}">Season {{seasonNumber}}</option>
|
||||
{{/if_eq}}
|
||||
{{/each}}
|
||||
|
||||
<option value="5000000">None</option>
|
||||
</select>
|
177
src/UI/AddMovies/addMovies.less
Normal file
177
src/UI/AddMovies/addMovies.less
Normal file
@ -0,0 +1,177 @@
|
||||
@import "../Shared/Styles/card.less";
|
||||
@import "../Shared/Styles/clickable.less";
|
||||
|
||||
#add-movies-screen {
|
||||
.existing-movies {
|
||||
|
||||
.card();
|
||||
margin : 30px 0px;
|
||||
|
||||
.unmapped-folder-path {
|
||||
padding: 20px;
|
||||
margin-left : 0px;
|
||||
font-weight : 100;
|
||||
font-size : 25px;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
.new-movies-loadmore {
|
||||
font-size : 30px;
|
||||
font-weight : 300;
|
||||
padding-top : 10px;
|
||||
padding-bottom : 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.new-movies {
|
||||
.search-item {
|
||||
.card();
|
||||
margin : 40px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-movies-search {
|
||||
margin-top : 20px;
|
||||
margin-bottom : 20px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
|
||||
padding-bottom : 20px;
|
||||
|
||||
.btn-group{
|
||||
display: table;
|
||||
}
|
||||
|
||||
.movies-title {
|
||||
margin-top : 5px;
|
||||
|
||||
.labels {
|
||||
margin-left : 10px;
|
||||
|
||||
.label {
|
||||
font-size : 12px;
|
||||
vertical-align : middle;
|
||||
}
|
||||
}
|
||||
|
||||
.year {
|
||||
font-style : italic;
|
||||
color : #aaaaaa;
|
||||
}
|
||||
}
|
||||
|
||||
.new-movies-overview {
|
||||
overflow : hidden;
|
||||
height : 103px;
|
||||
|
||||
.overview-internal {
|
||||
overflow : hidden;
|
||||
height : 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.movies-poster {
|
||||
min-width : 138px;
|
||||
min-height : 203px;
|
||||
max-width : 138px;
|
||||
max-height : 203px;
|
||||
margin : 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color : #343434;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration : none;
|
||||
}
|
||||
|
||||
select {
|
||||
font-size : 14px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-top : 0px;
|
||||
}
|
||||
|
||||
.add {
|
||||
i {
|
||||
&:before {
|
||||
color : #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-tooltip {
|
||||
margin-left : 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-folders {
|
||||
margin : 30px 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color : #999999;
|
||||
font-style : italic;
|
||||
}
|
||||
|
||||
.monitor-tooltip-contents {
|
||||
padding-bottom : 0px;
|
||||
|
||||
dd {
|
||||
padding-bottom : 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li.add-new {
|
||||
.clickable;
|
||||
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 20px;
|
||||
color: rgb(51, 51, 51);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
li.add-new:hover {
|
||||
text-decoration: none;
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(0, 129, 194);
|
||||
}
|
||||
|
||||
.root-folders-modal {
|
||||
overflow : visible;
|
||||
|
||||
.root-folders-list {
|
||||
overflow-y : auto;
|
||||
max-height : 300px;
|
||||
|
||||
i {
|
||||
.clickable();
|
||||
}
|
||||
}
|
||||
|
||||
.validation-errors {
|
||||
display : none;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.form-control {
|
||||
background-color : white;
|
||||
}
|
||||
}
|
||||
|
||||
.root-folders {
|
||||
margin-top : 20px;
|
||||
}
|
||||
|
||||
.recent-folder {
|
||||
.clickable();
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ var Marionette = require('marionette');
|
||||
var ActivityLayout = require('./Activity/ActivityLayout');
|
||||
var SettingsLayout = require('./Settings/SettingsLayout');
|
||||
var AddSeriesLayout = require('./AddSeries/AddSeriesLayout');
|
||||
var AddMoviesLayout = require('./AddMovies/AddMoviesLayout');
|
||||
var WantedLayout = require('./Wanted/WantedLayout');
|
||||
var CalendarLayout = require('./Calendar/CalendarLayout');
|
||||
var ReleaseLayout = require('./Release/ReleaseLayout');
|
||||
@ -17,6 +18,11 @@ module.exports = NzbDroneController.extend({
|
||||
this.showMainRegion(new AddSeriesLayout({ action : action }));
|
||||
},
|
||||
|
||||
addMovies : function(action) {
|
||||
this.setTitle("Add Movie");
|
||||
this.showMainRegion(new AddMoviesLayout({action : action}));
|
||||
},
|
||||
|
||||
calendar : function() {
|
||||
this.setTitle('Calendar');
|
||||
this.showMainRegion(new CalendarLayout());
|
||||
|
@ -28,7 +28,7 @@ Handlebars.registerHelper('imdbUrl', function() {
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('tvdbUrl', function() {
|
||||
return 'http://imdb.com/title/tt' + this.tvdbId;
|
||||
return 'http://imdb.com/title/tt' + this.imdbId;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('tvRageUrl', function() {
|
||||
|
13
src/UI/Movies/MovieModel.js
Normal file
13
src/UI/Movies/MovieModel.js
Normal file
@ -0,0 +1,13 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
urlRoot : window.NzbDrone.ApiRoot + '/movies',
|
||||
|
||||
defaults : {
|
||||
episodeFileCount : 0,
|
||||
episodeCount : 0,
|
||||
isExisting : false,
|
||||
status : 0
|
||||
}
|
||||
});
|
120
src/UI/Movies/MoviesCollection.js
Normal file
120
src/UI/Movies/MoviesCollection.js
Normal file
@ -0,0 +1,120 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var PageableCollection = require('backbone.pageable');
|
||||
var MovieModel = require('./MovieModel');
|
||||
var ApiData = require('../Shared/ApiData');
|
||||
var AsFilteredCollection = require('../Mixins/AsFilteredCollection');
|
||||
var AsSortedCollection = require('../Mixins/AsSortedCollection');
|
||||
var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection');
|
||||
var moment = require('moment');
|
||||
require('../Mixins/backbone.signalr.mixin');
|
||||
|
||||
var Collection = PageableCollection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/movies',
|
||||
model : MovieModel,
|
||||
tableName : 'movies',
|
||||
|
||||
state : {
|
||||
sortKey : 'sortTitle',
|
||||
order : -1,
|
||||
pageSize : 100000,
|
||||
secondarySortKey : 'sortTitle',
|
||||
secondarySortOrder : -1
|
||||
},
|
||||
|
||||
mode : 'client',
|
||||
|
||||
save : function() {
|
||||
var self = this;
|
||||
|
||||
var proxy = _.extend(new Backbone.Model(), {
|
||||
id : '',
|
||||
|
||||
url : self.url + '/editor',
|
||||
|
||||
toJSON : function() {
|
||||
return self.filter(function(model) {
|
||||
return model.edited;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.listenTo(proxy, 'sync', function(proxyModel, models) {
|
||||
this.add(models, { merge : true });
|
||||
this.trigger('save', this);
|
||||
});
|
||||
|
||||
return proxy.save();
|
||||
},
|
||||
|
||||
filterModes : {
|
||||
'all' : [
|
||||
null,
|
||||
null
|
||||
],
|
||||
'continuing' : [
|
||||
'status',
|
||||
'continuing'
|
||||
],
|
||||
'ended' : [
|
||||
'status',
|
||||
'ended'
|
||||
],
|
||||
'monitored' : [
|
||||
'monitored',
|
||||
true
|
||||
],
|
||||
'missing' : [
|
||||
null,
|
||||
null,
|
||||
function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); }
|
||||
]
|
||||
},
|
||||
|
||||
sortMappings : {
|
||||
title : {
|
||||
sortKey : 'sortTitle'
|
||||
},
|
||||
|
||||
nextAiring : {
|
||||
sortValue : function(model, attr, order) {
|
||||
var nextAiring = model.get(attr);
|
||||
|
||||
if (nextAiring) {
|
||||
return moment(nextAiring).unix();
|
||||
}
|
||||
|
||||
if (order === 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
},
|
||||
|
||||
percentOfEpisodes : {
|
||||
sortValue : function(model, attr) {
|
||||
var percentOfEpisodes = model.get(attr);
|
||||
var episodeCount = model.get('episodeCount');
|
||||
|
||||
return percentOfEpisodes + episodeCount / 1000000;
|
||||
}
|
||||
},
|
||||
|
||||
path : {
|
||||
sortValue : function(model) {
|
||||
var path = model.get('path');
|
||||
|
||||
return path.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Collection = AsFilteredCollection.call(Collection);
|
||||
Collection = AsSortedCollection.call(Collection);
|
||||
Collection = AsPersistedStateCollection.call(Collection);
|
||||
|
||||
var data = ApiData.get('series');
|
||||
|
||||
module.exports = new Collection(data, { full : true }).bindSignalR();
|
@ -6,6 +6,8 @@ module.exports = Marionette.AppRouter.extend({
|
||||
appRoutes : {
|
||||
'addseries' : 'addSeries',
|
||||
'addseries/:action(/:query)' : 'addSeries',
|
||||
'addmovies' : 'addMovies',
|
||||
'addmovies/:action(/:query)' : 'addMovies',
|
||||
'calendar' : 'calendar',
|
||||
'settings' : 'settings',
|
||||
'settings/:action(/:query)' : 'settings',
|
||||
@ -22,4 +24,4 @@ module.exports = Marionette.AppRouter.extend({
|
||||
'serieseditor' : 'seriesEditor',
|
||||
':whatever' : 'showNotFound'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<a href="/addseries" class='btn btn-lg btn-block btn-success x-add-series'>
|
||||
<a href="/addmovies" class='btn btn-lg btn-block btn-success x-add-series'>
|
||||
<i class='icon-sonarr-add'></i>
|
||||
Add Movie
|
||||
</a>
|
||||
|
@ -82,7 +82,7 @@ module.exports = Marionette.Layout.extend({
|
||||
{
|
||||
title : 'Add Movie',
|
||||
icon : 'icon-sonarr-add',
|
||||
route : 'addseries'
|
||||
route : 'addmovies'
|
||||
},
|
||||
{
|
||||
title : 'Season Pass',
|
||||
|
@ -26,6 +26,7 @@
|
||||
<link href="/Content/logs.css" rel='stylesheet' type='text/css'/>
|
||||
<link href="/Content/settings.css" rel='stylesheet' type='text/css'/>
|
||||
<link href="/Content/addSeries.css" rel='stylesheet' type='text/css'/>
|
||||
<link href="/Content/addMovies.css" rel='stylesheet' type='text/css'/>
|
||||
<link href="/Content/calendar.css" rel='stylesheet' type='text/css'/>
|
||||
<link href="/Content/update.css" rel='stylesheet' type='text/css'/>
|
||||
<link href="/Content/overrides.css" rel='stylesheet' type='text/css'/>
|
||||
|
Loading…
Reference in New Issue
Block a user