Merge remote-tracking branch 'origin/develop' into develop

pull/2/head
Leonardo Galli 7 years ago
commit b1e75ffc57

@ -24,7 +24,7 @@ The project was inspired by other Usenet/BitTorrent movie downloaders such as Co
[![AppVeyor Builds](https://img.shields.io/badge/downloads-continuous-green.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/radarr-usby1/build/artifacts)
[![Docker release](https://img.shields.io/badge/docker-release-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/linuxserver/radarr)
[![Docker nightly](https://img.shields.io/badge/docker-nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/hotio/radarr)
[![Docker nightly](https://img.shields.io/badge/docker-nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/hotio/suitarr)
[![Docker armhf](https://img.shields.io/badge/docker-armhf-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/lsioarmhf/radarr)
[![Docker aarch64](https://img.shields.io/badge/docker-aarch64-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://store.docker.com/community/images/lsioarmhf/radarr-aarch64)

@ -16,14 +16,14 @@ install:
build_script:
- ps: ./build-appveyor.ps1
# test: off
test:
assemblies:
- '_tests\*Test.dll'
categories:
except:
- IntegrationTest
- AutomationTest
test: off
#test:
# assemblies:
# - '_tests\*Test.dll'
# categories:
# except:
# - IntegrationTest
# - AutomationTest
artifacts:
- path: '_artifacts\*.zip'

4190
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{
"name": "Sonarr",
"name": "Radarr",
"version": "2.0.0",
"description": "Sonarr",
"description": "Radarr",
"main": "main.js",
"scripts": {
"build": "gulp build",
@ -9,7 +9,7 @@
},
"repository": {
"type": "git",
"url": "git://github.com/Sonarr/Sonarr.git"
"url": "git://github.com/Radarr/Radarr.git"
},
"author": "",
"license": "GPL-3.0",

@ -1,4 +1,5 @@
using System.Text;
using System.Linq;
using System.Text;
using Marr.Data.Mapping;
using Marr.Data.QGen.Dialects;
@ -129,7 +130,16 @@ namespace Marr.Data.QGen
public void BuildOrderClause(StringBuilder sql)
{
sql.Append(OrderBy.ToString());
}
}
public void BuildGroupBy(StringBuilder sql)
{
var baseTable = this.Tables.First();
var primaryKeyColumn = baseTable.Columns.Single(c => c.ColumnInfo.IsPrimaryKey);
string token = this.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", primaryKeyColumn.ColumnInfo.Name));
sql.AppendFormat(" GROUP BY {0}", token);
}
private string TranslateJoin(JoinType join)
{

@ -14,8 +14,21 @@ namespace Marr.Data.QGen
public string Generate()
{
StringBuilder sql = new StringBuilder();
BuildSelectCountClause(sql);
if (_innerQuery.IsJoin)
{
sql.Append(" FROM (");
_innerQuery.BuildSelectClause(sql);
_innerQuery.BuildFromClause(sql);
_innerQuery.BuildJoinClauses(sql);
_innerQuery.BuildGroupBy(sql);
sql.Append(") ");
return sql.ToString();
}
_innerQuery.BuildFromClause(sql);
_innerQuery.BuildJoinClauses(sql);
_innerQuery.BuildWhereClause(sql);

@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text.RegularExpressions;
using Nancy;
@ -17,7 +17,7 @@ namespace NzbDrone.Api.Frontend.Mappers
private readonly IAnalyticsService _analyticsService;
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory;
private readonly string _indexPath;
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src|content)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg|json|xml))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static string API_KEY;
private static string URL_BASE;

@ -46,7 +46,7 @@ namespace NzbDrone.Api.Indexers
GetResourceAll = GetReleases;
Post["/"] = x => DownloadRelease(this.Bind<ReleaseResource>());
PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true);
//PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true);
PostValidator.RuleFor(s => s.Guid).NotEmpty();
_remoteEpisodeCache = cacheManager.GetCache<RemoteEpisode>(GetType(), "remoteEpisodes");
@ -70,7 +70,7 @@ namespace NzbDrone.Api.Indexers
try
{
_downloadService.DownloadReport(remoteMovie);
_downloadService.DownloadReport(remoteMovie, false);
}
catch (ReleaseDownloadException ex)
{

@ -8,6 +8,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.DecisionEngine;
using System.Linq;
using NzbDrone.Core.Datastore.Migration;
namespace NzbDrone.Api.Indexers
{
@ -29,8 +30,8 @@ namespace NzbDrone.Api.Indexers
public bool FullSeason { get; set; }
public int SeasonNumber { get; set; }
public Language Language { get; set; }
public string AirDate { get; set; }
public string SeriesTitle { get; set; }
public int Year { get; set; }
public string MovieTitle { get; set; }
public int[] EpisodeNumbers { get; set; }
public int[] AbsoluteEpisodeNumbers { get; set; }
public bool Approved { get; set; }
@ -43,8 +44,9 @@ namespace NzbDrone.Api.Indexers
public string CommentUrl { get; set; }
public string DownloadUrl { get; set; }
public string InfoUrl { get; set; }
public bool DownloadAllowed { get; set; }
public MappingResultType MappingResult { get; set; }
public int ReleaseWeight { get; set; }
public int SuspectedMovieId { get; set; }
public IEnumerable<string> IndexerFlags { get; set; }
@ -88,11 +90,12 @@ namespace NzbDrone.Api.Indexers
var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo;
var remoteEpisode = model.RemoteEpisode;
var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo();
var downloadAllowed = model.RemoteEpisode.DownloadAllowed;
var mappingResult = MappingResultType.Success;
if (model.IsForMovie)
{
downloadAllowed = model.RemoteMovie.DownloadAllowed;
mappingResult = model.RemoteMovie.MappingResult;
var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo;
var movieId = model.RemoteMovie.Movie?.Id ?? 0;
return new ReleaseResource
{
@ -111,8 +114,8 @@ namespace NzbDrone.Api.Indexers
//FullSeason = parsedMovieInfo.FullSeason,
//SeasonNumber = parsedMovieInfo.SeasonNumber,
Language = parsedMovieInfo.Language,
AirDate = "",
SeriesTitle = parsedMovieInfo.MovieTitle,
Year = parsedMovieInfo.Year,
MovieTitle = parsedMovieInfo.MovieTitle,
EpisodeNumbers = new int[0],
AbsoluteEpisodeNumbers = new int[0],
Approved = model.Approved,
@ -125,8 +128,10 @@ namespace NzbDrone.Api.Indexers
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
DownloadAllowed = downloadAllowed,
MappingResult = mappingResult,
//ReleaseWeight
SuspectedMovieId = movieId,
MagnetUrl = torrentInfo.MagnetUrl,
InfoHash = torrentInfo.InfoHash,
@ -161,8 +166,8 @@ namespace NzbDrone.Api.Indexers
FullSeason = parsedEpisodeInfo.FullSeason,
SeasonNumber = parsedEpisodeInfo.SeasonNumber,
Language = parsedEpisodeInfo.Language,
AirDate = parsedEpisodeInfo.AirDate,
SeriesTitle = parsedEpisodeInfo.SeriesTitle,
//AirDate = parsedEpisodeInfo.AirDate,
//SeriesTitle = parsedEpisodeInfo.SeriesTitle,
EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers,
AbsoluteEpisodeNumbers = parsedEpisodeInfo.AbsoluteEpisodeNumbers,
Approved = model.Approved,
@ -175,7 +180,7 @@ namespace NzbDrone.Api.Indexers
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
DownloadAllowed = downloadAllowed,
//DownloadAllowed = downloadAllowed,
//ReleaseWeight
MagnetUrl = torrentInfo.MagnetUrl,

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Marr.Data;
using Nancy;
using NzbDrone.Api;
using NzbDrone.Api.Movie;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.RadarrAPI;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Api.Movie
{
public class AlternativeTitleModule : NzbDroneRestModule<AlternativeTitleResource>
{
private readonly IAlternativeTitleService _altTitleService;
private readonly IMovieService _movieService;
private readonly IRadarrAPIClient _radarrApi;
private readonly IEventAggregator _eventAggregator;
public AlternativeTitleModule(IAlternativeTitleService altTitleService, IMovieService movieService, IRadarrAPIClient radarrApi, IEventAggregator eventAggregator)
: base("/alttitle")
{
_altTitleService = altTitleService;
_movieService = movieService;
_radarrApi = radarrApi;
CreateResource = AddTitle;
GetResourceById = GetTitle;
_eventAggregator = eventAggregator;
}
private int AddTitle(AlternativeTitleResource altTitle)
{
var title = altTitle.ToModel();
var movie = _movieService.GetMovie(altTitle.MovieId);
var newTitle = _radarrApi.AddNewAlternativeTitle(title, movie.TmdbId);
var addedTitle = _altTitleService.AddAltTitle(newTitle, movie);
_eventAggregator.PublishEvent(new MovieUpdatedEvent(movie));
return addedTitle.Id;
}
private AlternativeTitleResource GetTitle(int id)
{
return _altTitleService.GetById(id).ToResource();
}
}
}

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Marr.Data;
using Nancy;
using NzbDrone.Api;
using NzbDrone.Api.Movie;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.RadarrAPI;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Api.Movie
{
public class AlternativeYearModule : NzbDroneRestModule<AlternativeYearResource>
{
private readonly IMovieService _movieService;
private readonly IRadarrAPIClient _radarrApi;
private readonly ICached<int> _yearCache;
private readonly IEventAggregator _eventAggregator;
public AlternativeYearModule(IMovieService movieService, IRadarrAPIClient radarrApi, ICacheManager cacheManager, IEventAggregator eventAggregator)
: base("/altyear")
{
_movieService = movieService;
_radarrApi = radarrApi;
CreateResource = AddYear;
GetResourceById = GetYear;
_yearCache = cacheManager.GetCache<int>(GetType(), "altYears");
_eventAggregator = eventAggregator;
}
private int AddYear(AlternativeYearResource altYear)
{
var id = new Random().Next();
_yearCache.Set(id.ToString(), altYear.Year, TimeSpan.FromMinutes(1));
var movie = _movieService.GetMovie(altYear.MovieId);
var newYear = _radarrApi.AddNewAlternativeYear(altYear.Year, movie.TmdbId);
movie.SecondaryYear = newYear.Year;
movie.SecondaryYearSourceId = newYear.SourceId;
_movieService.UpdateMovie(movie);
_eventAggregator.PublishEvent(new MovieUpdatedEvent(movie));
return id;
}
private AlternativeYearResource GetYear(int id)
{
return new AlternativeYearResource
{
Year = _yearCache.Find(id.ToString())
};
}
}
}

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Api.REST;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Qualities;
using NzbDrone.Api.Series;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser;
namespace NzbDrone.Api.Movie
{
public class AlternativeYearResource : RestResource
{
public AlternativeYearResource()
{
}
//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
public int MovieId { get; set; }
public int Year { get; set; }
//TODO: Add series statistics as a property of the series (instead of individual properties)
}
/*public static class AlternativeYearResourceMapper
{
/*public static AlternativeYearResource ToResource(this AlternativeTitle model)
{
if (model == null) return null;
AlternativeTitleResource resource = null;
return new AlternativeTitleResource
{
Id = model.Id,
SourceType = model.SourceType,
MovieId = model.MovieId,
Title = model.Title,
SourceId = model.SourceId,
Votes = model.Votes,
VoteCount = model.VoteCount,
Language = model.Language
};
}
public static AlternativeTitle ToModel(this AlternativeTitleResource resource)
{
if (resource == null) return null;
return new AlternativeTitle
{
Id = resource.Id,
SourceType = resource.SourceType,
MovieId = resource.MovieId,
Title = resource.Title,
SourceId = resource.SourceId,
Votes = resource.Votes,
VoteCount = resource.VoteCount,
Language = resource.Language
};
}
public static List<AlternativeTitleResource> ToResource(this IEnumerable<AlternativeTitle> movies)
{
return movies.Select(ToResource).ToList();
}
}*/
}

@ -119,6 +119,9 @@
<Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" />
<Compile Include="Indexers\ReleaseModuleBase.cs" />
<Compile Include="Indexers\ReleasePushModule.cs" />
<Compile Include="Movies\AlternativeTitleModule.cs" />
<Compile Include="Movies\AlternativeYearResource.cs" />
<Compile Include="Movies\AlternativeYearModule.cs" />
<Compile Include="Movies\MovieModuleWithSignalR.cs" />
<Compile Include="Movies\MovieBulkImportModule.cs" />
<Compile Include="Movies\MovieFileModule.cs" />
@ -240,7 +243,7 @@
<Compile Include="RootFolders\RootFolderModule.cs" />
<Compile Include="RootFolders\RootFolderResource.cs" />
<Compile Include="SeasonPass\SeasonPassResource.cs" />
<Compile Include="Series\AlternateTitleResource.cs" />
<Compile Include="Series\AlternativeTitleResource.cs" />
<Compile Include="Series\MovieFileResource.cs" />
<Compile Include="Series\FetchMovieListModule.cs" />
<Compile Include="Series\SeasonResource.cs" />

@ -105,7 +105,7 @@ namespace NzbDrone.Api.Queue
throw new NotFoundException();
}
_downloadService.DownloadReport(pendingRelease.RemoteMovie);
_downloadService.DownloadReport(pendingRelease.RemoteMovie, false);
return resource.AsResponse();
}

@ -1,9 +0,0 @@
namespace NzbDrone.Api.Series
{
public class AlternateTitleResource
{
public string Title { get; set; }
public int? SeasonNumber { get; set; }
public int? SceneSeasonNumber { get; set; }
}
}

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Api.REST;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Qualities;
using NzbDrone.Api.Series;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser;
namespace NzbDrone.Api.Movie
{
public class AlternativeTitleResource : RestResource
{
public AlternativeTitleResource()
{
}
//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
public SourceType SourceType { get; set; }
public int MovieId { get; set; }
public string Title { get; set; }
public string CleanTitle { get; set; }
public int SourceId { get; set; }
public int Votes { get; set; }
public int VoteCount { get; set; }
public Language Language { get; set; }
//TODO: Add series statistics as a property of the series (instead of individual properties)
}
public static class AlternativeTitleResourceMapper
{
public static AlternativeTitleResource ToResource(this AlternativeTitle model)
{
if (model == null) return null;
AlternativeTitleResource resource = null;
return new AlternativeTitleResource
{
Id = model.Id,
SourceType = model.SourceType,
MovieId = model.MovieId,
Title = model.Title,
SourceId = model.SourceId,
Votes = model.Votes,
VoteCount = model.VoteCount,
Language = model.Language
};
}
public static AlternativeTitle ToModel(this AlternativeTitleResource resource)
{
if (resource == null) return null;
return new AlternativeTitle
{
Id = resource.Id,
SourceType = resource.SourceType,
MovieId = resource.MovieId,
Title = resource.Title,
SourceId = resource.SourceId,
Votes = resource.Votes,
VoteCount = resource.VoteCount,
Language = resource.Language
};
}
public static List<AlternativeTitleResource> ToResource(this IEnumerable<AlternativeTitle> movies)
{
return movies.Select(ToResource).ToList();
}
}
}

@ -21,7 +21,9 @@ namespace NzbDrone.Api.Movie
//View Only
public string Title { get; set; }
public List<AlternateTitleResource> AlternateTitles { get; set; }
public List<AlternativeTitleResource> AlternativeTitles { get; set; }
public int? SecondaryYear { get; set; }
public int SecondaryYearSourceId { get; set; }
public string SortTitle { get; set; }
public long? SizeOnDisk { get; set; }
public MovieStatusType Status { get; set; }
@ -62,7 +64,7 @@ namespace NzbDrone.Api.Movie
public DateTime Added { get; set; }
public AddMovieOptions AddOptions { get; set; }
public Ratings Ratings { get; set; }
public List<string> AlternativeTitles { get; set; }
//public List<string> AlternativeTitles { get; set; }
public MovieFileResource MovieFile { get; set; }
//TODO: Add series statistics as a property of the series (instead of individual properties)
@ -107,6 +109,8 @@ namespace NzbDrone.Api.Movie
downloaded = true;
movieFile = model.MovieFile.Value.ToResource();
}
//model.AlternativeTitles.LazyLoad();
return new MovieResource
{
@ -131,6 +135,8 @@ namespace NzbDrone.Api.Movie
Images = model.Images,
Year = model.Year,
SecondaryYear = model.SecondaryYear,
SecondaryYearSourceId = model.SecondaryYearSourceId,
Path = model.Path,
ProfileId = model.ProfileId,
@ -156,7 +162,7 @@ namespace NzbDrone.Api.Movie
Tags = model.Tags,
Added = model.Added,
AddOptions = model.AddOptions,
AlternativeTitles = model.AlternativeTitles,
AlternativeTitles = model.AlternativeTitles.ToResource(),
Ratings = model.Ratings,
MovieFile = movieFile,
YouTubeTrailerId = model.YouTubeTrailerId,
@ -189,6 +195,8 @@ namespace NzbDrone.Api.Movie
Images = resource.Images,
Year = resource.Year,
SecondaryYear = resource.SecondaryYear,
SecondaryYearSourceId = resource.SecondaryYearSourceId,
Path = resource.Path,
ProfileId = resource.ProfileId,
@ -209,7 +217,7 @@ namespace NzbDrone.Api.Movie
Tags = resource.Tags,
Added = resource.Added,
AddOptions = resource.AddOptions,
AlternativeTitles = resource.AlternativeTitles,
//AlternativeTitles = resource.AlternativeTitles,
Ratings = resource.Ratings,
YouTubeTrailerId = resource.YouTubeTrailerId,
Studio = resource.Studio

@ -199,7 +199,7 @@ namespace NzbDrone.Api.Series
if (mappings == null) return;
resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList();
//resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList();
}
public void Handle(EpisodeImportedEvent message)

@ -20,7 +20,7 @@ namespace NzbDrone.Api.Series
//View Only
public string Title { get; set; }
public List<AlternateTitleResource> AlternateTitles { get; set; }
//public List<AlternativeTitleResource> AlternateTitles { get; set; }
public string SortTitle { get; set; }
public int SeasonCount

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Core.Datastore.Events;
@ -12,7 +12,7 @@ namespace NzbDrone.Api.System.Tasks
{
private readonly ITaskManager _taskManager;
private static readonly Regex NameRegex = new Regex("(?<!^)[A-Z]", RegexOptions.Compiled);
private static readonly Regex NameRegex = new Regex("(?<!^)[A-Z][a-z]", RegexOptions.Compiled);
public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
: base(broadcastSignalRMessage, "system/task")

@ -263,7 +263,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
result.Should().HaveCount(1);
result.First().RemoteMovie.DownloadAllowed.Should().BeFalse();
//result.First().RemoteMovie.DownloadAllowed.Should().BeFalse();
}
[Test]
@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
result.Should().HaveCount(1);
result.First().RemoteMovie.DownloadAllowed.Should().BeFalse();
//result.First().RemoteMovie.DownloadAllowed.Should().BeFalse();
}
[Test]

@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteMovie));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Once());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), false), Times.Once());
}
[Test]
@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteMovie));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Once());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), false), Times.Once());
}
[Test]
@ -157,7 +157,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteMovie));
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>())).Throws(new Exception());
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>(), false)).Throws(new Exception());
Subject.ProcessDecisions(decisions).Grabbed.Should().BeEmpty();
ExceptionVerification.ExpectedWarns(1);
}
@ -183,7 +183,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteMovie));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Never());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), false), Times.Never());
}
[Test]

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Test.HealthCheck
[TestFixture]
public class HealthCheckFixture : CoreTest
{
private const string WikiRoot = "https://github.com/Sonarr/Sonarr/wiki/";
private const string WikiRoot = "https://github.com/Radarr/Radarr/wiki/";
[TestCase("I blew up because of some weird user mistake", null, WikiRoot + "Health-checks#i-blew-up-because-of-some-weird-user-mistake")]
[TestCase("I blew up because of some weird user mistake", "#my-health-check", WikiRoot + "Health-checks#my-health-check")]

@ -54,6 +54,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Der.Soldat.James.German.Bluray.FuckYou.Pso.Why.cant.you.follow.scene.rules.1998", Language.German)]
[TestCase("Passengers.German.DL.AC3.Dubbed..BluRay.x264-PsO", Language.German)]
[TestCase("Valana la Legende FRENCH BluRay 720p 2016 kjhlj", Language.French)]
[TestCase("Smurfs.The.Lost.Village.2017.1080p.BluRay.HebDub.x264-iSrael",Language.Hebrew)]
public void should_parse_language(string postTitle, Language language)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);

@ -6,7 +6,9 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
@ -43,7 +45,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
.With(m => m.Title = "Fack Ju Göthe 2")
.With(m => m.CleanTitle = "fackjugoethe2")
.With(m => m.Year = 2015)
.With(m => m.AlternativeTitles = new List<string> { "Fack Ju Göthe 2: Same same" })
.With(m => m.AlternativeTitles = new LazyList<AlternativeTitle>( new List<AlternativeTitle> {new AlternativeTitle("Fack Ju Göthe 2: Same same")}))
.Build();
_episodes = Builder<Episode>.CreateListOfSize(1)
@ -80,7 +82,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
_alternativeTitleInfo = new ParsedMovieInfo
{
MovieTitle = _movie.AlternativeTitles.First(),
MovieTitle = _movie.AlternativeTitles.First().Title,
Year = _movie.Year,
};

@ -307,6 +307,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "korsub")]
[TestCase("Movie.Title.2016.1080p.KORSUBS.WEBRip.x264.AAC2.0-RADARR", "korsubs")]
[TestCase("Wonder Woman 2017 HC 720p HDRiP DD5 1 x264-LEGi0N", "Generic Hardcoded Subs")]
public void should_parse_hardcoded_subs(string postTitle, string sub)
{
QualityParser.ParseQuality(postTitle).HardcodedSubs.Should().Be(sub);

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
@ -39,15 +39,15 @@ namespace NzbDrone.Core.Test.Profiles
[Test]
public void should_not_be_able_to_delete_profile_if_assigned_to_series()
public void should_not_be_able_to_delete_profile_if_assigned_to_movie()
{
var seriesList = Builder<Series>.CreateListOfSize(3)
var movieList = Builder<Movie>.CreateListOfSize(3)
.Random(1)
.With(c => c.ProfileId = 2)
.Build().ToList();
Mocker.GetMock<ISeriesService>().Setup(c => c.GetAllSeries()).Returns(seriesList);
Mocker.GetMock<IMovieService>().Setup(c => c.GetAllMovies()).Returns(movieList);
Assert.Throws<ProfileInUseException>(() => Subject.Delete(2));
@ -57,15 +57,15 @@ namespace NzbDrone.Core.Test.Profiles
[Test]
public void should_delete_profile_if_not_assigned_to_series()
public void should_delete_profile_if_not_assigned_to_movie()
{
var seriesList = Builder<Series>.CreateListOfSize(3)
var movieList = Builder<Movie>.CreateListOfSize(3)
.All()
.With(c => c.ProfileId = 2)
.Build().ToList();
Mocker.GetMock<ISeriesService>().Setup(c => c.GetAllSeries()).Returns(seriesList);
Mocker.GetMock<IMovieService>().Setup(c => c.GetAllMovies()).Returns(movieList);
Subject.Delete(1);

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Web.Hosting;
using Marr.Data;
using Marr.Data.QGen;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Datastore.Extensions;
@ -48,7 +50,7 @@ namespace NzbDrone.Core.Datastore
_eventAggregator = eventAggregator;
}
protected QueryBuilder<TModel> Query => DataMapper.Query<TModel>();
protected QueryBuilder<TModel> Query => AddJoinQueries(DataMapper.Query<TModel>());
protected void Delete(Expression<Func<TModel, bool>> filter)
{
@ -246,18 +248,23 @@ namespace NzbDrone.Core.Datastore
public virtual PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec)
{
pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList();
pagingSpec.Records = GetPagedQuery(Query, pagingSpec).Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize).ToList();
pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount();
var queryStr = GetPagedQuery(Query, pagingSpec).BuildQuery();
var beforeQuery = Query.BuildQuery();
pagingSpec.SortKey = beforeQuery;
pagingSpec.SortKey = queryStr;
return pagingSpec;
}
protected virtual SortBuilder<TModel> GetPagedQuery(QueryBuilder<TModel> query, PagingSpec<TModel> pagingSpec)
{
return query.Where(pagingSpec.FilterExpression)
.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
.Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize);
.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection());
}
protected void ModelCreated(TModel model)
@ -283,6 +290,11 @@ namespace NzbDrone.Core.Datastore
}
}
protected virtual QueryBuilder<TModel> AddJoinQueries(QueryBuilder<TModel> baseQuery)
{
return baseQuery;
}
protected virtual bool PublishModelEvents => false;
}
}

@ -110,10 +110,10 @@ namespace NzbDrone.Core.Datastore
{
if (OsInfo.IsOsx)
{
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-use-sonarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", ex, fileName);
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Radarr/Radarr/wiki/FAQ#i-use-radarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", ex, fileName);
}
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", ex, fileName);
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Radarr/Radarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", ex, fileName);
}
}

@ -28,12 +28,12 @@ namespace NzbDrone.Core.Datastore.Extensions
return mapBuilder.Relationships.AutoMapComplexTypeProperties<ILazyLoaded>();
}
public static RelationshipBuilder<TParent> HasMany<TParent, TChild>(this RelationshipBuilder<TParent> relationshipBuilder, Expression<Func<TParent, LazyList<TChild>>> portalExpression, Func<TParent, int> childIdSelector)
public static RelationshipBuilder<TParent> HasMany<TParent, TChild>(this RelationshipBuilder<TParent> relationshipBuilder, Expression<Func<TParent, LazyList<TChild>>> portalExpression, Func<TChild, int> parentIdSelector)
where TParent : ModelBase
where TChild : ModelBase
{
return relationshipBuilder.For(portalExpression.GetMemberName())
.LazyLoad((db, parent) => db.Query<TChild>().Where(c => c.Id == childIdSelector(parent)).ToList());
.LazyLoad((db, parent) => db.Query<TChild>().Where(c => parentIdSelector(c) == parent.Id).ToList());
}
private static string GetMemberName<T, TMember>(this Expression<Func<T, TMember>> member)

@ -0,0 +1,39 @@
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
using System.Text;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text.RegularExpressions;
using System.Globalization;
using Marr.Data.QGen;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(140)]
public class add_alternative_titles_table : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
if (!this.Schema.Schema("dbo").Table("alternative_titles").Exists())
{
Create.TableForModel("AlternativeTitles")
.WithColumn("MovieId").AsInt64().NotNullable()
.WithColumn("Title").AsString().NotNullable()
.WithColumn("CleanTitle").AsString().NotNullable()
.WithColumn("SourceType").AsInt64().WithDefault(0)
.WithColumn("SourceId").AsInt64().WithDefault(0)
.WithColumn("Votes").AsInt64().WithDefault(0)
.WithColumn("VoteCount").AsInt64().WithDefault(0)
.WithColumn("Language").AsInt64().WithDefault(0);
Delete.Column("AlternativeTitles").FromTable("Movies");
}
Alter.Table("Movies").AddColumn("SecondaryYear").AsInt32().Nullable();
Alter.Table("Movies").AddColumn("SecondaryYearSourceId").AsInt64().Nullable().WithDefault(0);
}
}
}

@ -36,6 +36,45 @@ using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.NetImport;
using NzbDrone.Core.NetImport.ImportExclusions;
using System;
using System.Collections.Generic;
using Marr.Data;
using Marr.Data.Mapping;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Restrictions;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.SeriesStats;
using NzbDrone.Core.Tags;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.NetImport;
using NzbDrone.Core.NetImport.ImportExclusions;
namespace NzbDrone.Core.Datastore
{
@ -101,12 +140,19 @@ namespace NzbDrone.Core.Datastore
query: (db, parent) => db.Query<Movie>().Where(c => c.MovieFileId == parent.Id).ToList())
.HasOne(file => file.Movie, file => file.MovieId);
Mapper.Entity<Movie>().RegisterModel("Movies")
Mapper.Entity<Movie>().RegisterModel("Movies")
.Ignore(s => s.RootFolderPath)
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId)
.HasOne(m => m.MovieFile, m => m.MovieFileId);
Mapper.Entity<AlternativeTitle>().RegisterModel("AlternativeTitles")
.For(t => t.Id)
.SetAltName("AltTitle_Id")
.Relationship()
.HasOne(t => t.Movie, t => t.MovieId);
Mapper.Entity<ImportExclusion>().RegisterModel("ImportExclusions");

@ -113,11 +113,11 @@ namespace NzbDrone.Core.DecisionEngine
var remoteMovie = result.RemoteMovie;
remoteMovie.Release = report;
remoteMovie.MappingResult = result.MappingResultType;
if (result.MappingResultType != MappingResultType.Success && result.MappingResultType != MappingResultType.SuccessLenientMapping)
{
var rejection = result.ToRejection();
remoteMovie.Movie = null; // HACK: For now!
decision = new DownloadDecision(remoteMovie, rejection);
}
@ -125,7 +125,7 @@ namespace NzbDrone.Core.DecisionEngine
{
if (parsedMovieInfo.Quality.HardcodedSubs.IsNotNullOrWhiteSpace())
{
remoteMovie.DownloadAllowed = true;
//remoteMovie.DownloadAllowed = true;
if (_configService.AllowHardcodedSubs)
{
decision = GetDecisionForReport(remoteMovie, searchCriteria);
@ -146,7 +146,7 @@ namespace NzbDrone.Core.DecisionEngine
}
else
{
remoteMovie.DownloadAllowed = true;
//remoteMovie.DownloadAllowed = true;
decision = GetDecisionForReport(remoteMovie, searchCriteria);
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.DecisionEngine
{
@ -36,13 +37,13 @@ namespace NzbDrone.Core.DecisionEngine
public List<DownloadDecision> PrioritizeDecisionsForMovies(List<DownloadDecision> decisions)
{
return decisions.Where(c => c.RemoteMovie.Movie != null)
return decisions.Where(c => c.RemoteMovie.MappingResult == MappingResultType.Success || c.RemoteMovie.MappingResult == MappingResultType.SuccessLenientMapping)
.GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) =>
{
return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService, _configService));
})
.SelectMany(c => c)
.Union(decisions.Where(c => c.RemoteMovie.Movie == null))
.Union(decisions.Where(c => c.RemoteMovie.MappingResult != MappingResultType.Success || c.RemoteMovie.MappingResult != MappingResultType.SuccessLenientMapping))
.ToList();
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common.Disk;
@ -111,7 +111,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
case "error": // some error occurred, applies to paused torrents
item.Status = DownloadItemStatus.Failed;
item.Message = "QBittorrent is reporting an error";
item.Message = "qBittorrent is reporting an error";
break;
case "pausedDL": // torrent is paused and has NOT finished downloading
@ -212,7 +212,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var config = _proxy.GetConfig(Settings);
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
{
return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
{
DetailedDescription = "Radarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'."
};

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Net;
using NLog;
@ -72,7 +72,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormParameter("urls", torrentUrl);
ProcessRequest<object>(request, settings);
var result = ProcessRequest(request, settings);
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here.
if (result == "Fails.")
{
throw new DownloadClientException("Download client failed to add torrent by url");
}
}
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
@ -81,7 +87,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormUpload("torrents", fileName, fileContent);
ProcessRequest<object>(request, settings);
var result = ProcessRequest(request, settings);
// Note: Current qbit versions return nothing, so we can't do != "Ok." here.
if (result == "Fails.")
{
throw new DownloadClientException("Download client failed to add torrent");
}
}
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
@ -90,7 +102,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest<object>(request, settings);
ProcessRequest(request, settings);
}
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
@ -101,7 +113,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.AddFormParameter("category", label);
try
{
ProcessRequest<object>(setCategoryRequest, settings);
ProcessRequest(setCategoryRequest, settings);
}
catch(DownloadClientException ex)
{
@ -112,7 +124,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("label", label);
ProcessRequest<object>(setLabelRequest, settings);
ProcessRequest(setLabelRequest, settings);
}
}
}
@ -125,7 +138,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
try
{
var response = ProcessRequest<object>(request, settings);
ProcessRequest(request, settings);
}
catch (DownloadClientException ex)
{
@ -152,10 +165,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
where TResult : new()
{
var responseContent = ProcessRequest(requestBuilder, settings);
return Json.Deserialize<TResult>(responseContent);
}
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
{
AuthenticateClient(requestBuilder, settings);
var request = requestBuilder.Build();
request.LogResponseContent = true;
HttpResponse response;
try
@ -176,15 +197,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
else
{
throw new DownloadClientException("Failed to connect to qBitTorrent, check your settings.", ex);
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex);
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
return Json.Deserialize<TResult>(response.Content);
return response.Content;
}
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
@ -218,23 +239,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_logger.Debug("qbitTorrent authentication failed.");
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
throw new DownloadClientAuthenticationException("Failed to authenticate with qbitTorrent.", ex);
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
}
throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex);
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex);
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
if (response.Content != "Ok.") // returns "Fails." on bad login
{
_logger.Debug("qbitTorrent authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qbitTorrent.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}
_logger.Debug("qbitTorrent authentication succeeded.");
_logger.Debug("qBittorrent authentication succeeded.");
cookies = response.GetCookies();

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download
public interface IDownloadService
{
void DownloadReport(RemoteEpisode remoteEpisode);
void DownloadReport(RemoteMovie remoteMovie);
void DownloadReport(RemoteMovie remoteMovie, bool forceDownload);
}
@ -92,7 +92,7 @@ namespace NzbDrone.Core.Download
_eventAggregator.PublishEvent(episodeGrabbedEvent);
}
public void DownloadReport(RemoteMovie remoteMovie)
public void DownloadReport(RemoteMovie remoteMovie, bool foceDownload = false)
{
//Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull();
//Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit

@ -88,7 +88,7 @@ namespace NzbDrone.Core.Download
try
{
_downloadService.DownloadReport(remoteMovie);
_downloadService.DownloadReport(remoteMovie, false);
grabbed.Add(report);
}
catch (Exception e)

@ -39,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck
private static HttpUri MakeWikiUrl(string fragment)
{
return new HttpUri("https://github.com/Sonarr/Sonarr/wiki/Health-checks") + new HttpUri(fragment);
return new HttpUri("https://github.com/Radarr/Radarr/wiki/Health-checks") + new HttpUri(fragment);
}
}

@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Newznab
else
{
var searchTitle = System.Web.HttpUtility.UrlPathEncode(Parser.Parser.ReplaceGermanUmlauts(Parser.Parser.NormalizeTitle(searchCriteria.Movie.Title)));
var altTitles = searchCriteria.Movie.AlternativeTitles.DistinctBy(t => Parser.Parser.CleanSeriesTitle(t)).Take(5).ToList();
var altTitles = searchCriteria.Movie.AlternativeTitles.Take(5).Select(t => t.Title).ToList();
var realMaxPages = (int)MaxPages / (altTitles.Count() + 1);

@ -3,7 +3,10 @@ using NzbDrone.Core.Configuration;
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.MetadataSource.RadarrAPI
{
@ -11,6 +14,10 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI
{
IHttpRequestBuilderFactory RadarrAPI { get; }
List<MovieResult> DiscoverMovies(string action, Func<HttpRequest, HttpRequest> enhanceRequest);
List<AlternativeTitle> AlternativeTitlesForMovie(int TmdbId);
Tuple<List<AlternativeTitle>, AlternativeYear> AlternativeTitlesAndYearForMovie(int tmdbId);
AlternativeTitle AddNewAlternativeTitle(AlternativeTitle title, int TmdbId);
AlternativeYear AddNewAlternativeYear(int year, int tmdbId);
string APIURL { get; }
}
@ -65,7 +72,7 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI
{
var error = JsonConvert.DeserializeObject<RadarrError>(response.Content);
if (error != null && error.Errors.Count != 0)
if (error != null && error.Errors != null && error.Errors.Count != 0)
{
throw new RadarrAPIException(error);
}
@ -96,6 +103,83 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI
return Execute<List<MovieResult>>(request);
}
public List<AlternativeTitle> AlternativeTitlesForMovie(int TmdbId)
{
var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "find").AddQueryParam("tmdbid", TmdbId).Build();
var mappings = Execute<Mapping>(request);
var titles = new List<NzbDrone.Core.Movies.AlternativeTitles.AlternativeTitle>();
foreach (var altTitle in mappings.Mappings.Titles)
{
titles.Add(new NzbDrone.Core.Movies.AlternativeTitles.AlternativeTitle(altTitle.Info.AkaTitle, SourceType.Mappings, altTitle.Id));
}
return titles;
}
public Tuple<List<AlternativeTitle>, AlternativeYear> AlternativeTitlesAndYearForMovie(int tmdbId)
{
var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "find").AddQueryParam("tmdbid", tmdbId).Build();
var mappings = Execute<Mapping>(request);
var titles = new List<NzbDrone.Core.Movies.AlternativeTitles.AlternativeTitle>();
foreach (var altTitle in mappings.Mappings.Titles)
{
titles.Add(new NzbDrone.Core.Movies.AlternativeTitles.AlternativeTitle(altTitle.Info.AkaTitle, SourceType.Mappings, altTitle.Id));
}
var year = mappings.Mappings.Years.Where(y => y.Votes >= 3).OrderBy(y => y.Votes).FirstOrDefault();
AlternativeYear newYear = null;
if (year != null)
{
newYear = new AlternativeYear
{
Year = year.Info.AkaYear,
SourceId = year.Id
};
}
return new Tuple<List<AlternativeTitle>, AlternativeYear>(titles, newYear);
}
public AlternativeTitle AddNewAlternativeTitle(AlternativeTitle title, int TmdbId)
{
var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "add")
.AddQueryParam("tmdbid", TmdbId).AddQueryParam("type", "title")
.AddQueryParam("language", IsoLanguages.Get(title.Language).TwoLetterCode)
.AddQueryParam("aka_title", title.Title).Build();
var newMapping = Execute<AddTitleMapping>(request);
var newTitle = new AlternativeTitle(newMapping.Info.AkaTitle, SourceType.Mappings, newMapping.Id, title.Language);
newTitle.VoteCount = newMapping.VoteCount;
newTitle.Votes = newMapping.Votes;
return newTitle;
}
public AlternativeYear AddNewAlternativeYear(int year, int tmdbId)
{
var request = RadarrAPI.Create().SetSegment("route", "mappings").SetSegment("action", "add")
.AddQueryParam("tmdbid", tmdbId).AddQueryParam("type", "year")
.AddQueryParam("aka_year", year).Build();
var newYear = Execute<AddYearMapping>(request);
return new AlternativeYear
{
Year = newYear.Info.AkaYear,
SourceId = newYear.Id
};
}
public IHttpRequestBuilderFactory RadarrAPI { get; private set; }
}
}

@ -27,21 +27,183 @@ namespace NzbDrone.Core.MetadataSource.RadarrAPI
public class RadarrAPIException : Exception
{
RadarrError APIErrors;
public RadarrError APIErrors;
public RadarrAPIException(RadarrError apiError) : base(HumanReadable(apiError))
{
APIErrors = apiError;
}
private static string HumanReadable(RadarrError APIErrors)
private static string HumanReadable(RadarrError apiErrors)
{
var firstError = APIErrors.Errors.First();
var details = string.Join("\n", APIErrors.Errors.Select(error =>
var firstError = apiErrors.Errors.First();
var details = string.Join("\n", apiErrors.Errors.Select(error =>
{
return $"{error.Title} ({error.Status}, RayId: {error.RayId}), Details: {error.Detail}";
}));
return $"Error while calling api: {firstError.Title}\nFull error(s): {details}";
}
}
public class TitleInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("aka_title")]
public string AkaTitle { get; set; }
[JsonProperty("aka_clean_title")]
public string AkaCleanTitle { get; set; }
}
public class YearInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("aka_year")]
public int AkaYear { get; set; }
}
public class Title
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("tmdbid")]
public int Tmdbid { get; set; }
[JsonProperty("votes")]
public int Votes { get; set; }
[JsonProperty("vote_count")]
public int VoteCount { get; set; }
[JsonProperty("locked")]
public bool Locked { get; set; }
[JsonProperty("info_type")]
public string InfoType { get; set; }
[JsonProperty("info_id")]
public int InfoId { get; set; }
[JsonProperty("info")]
public TitleInfo Info { get; set; }
}
public class Year
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("tmdbid")]
public int Tmdbid { get; set; }
[JsonProperty("votes")]
public int Votes { get; set; }
[JsonProperty("vote_count")]
public int VoteCount { get; set; }
[JsonProperty("locked")]
public bool Locked { get; set; }
[JsonProperty("info_type")]
public string InfoType { get; set; }
[JsonProperty("info_id")]
public int InfoId { get; set; }
[JsonProperty("info")]
public YearInfo Info { get; set; }
}
public class Mappings
{
[JsonProperty("titles")]
public IList<Title> Titles { get; set; }
[JsonProperty("years")]
public IList<Year> Years { get; set; }
}
public class Mapping
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("imdb_id")]
public string ImdbId { get; set; }
[JsonProperty("mappings")]
public Mappings Mappings { get; set; }
}
public class AddTitleMapping
{
[JsonProperty("tmdbid")]
public string Tmdbid { get; set; }
[JsonProperty("info_type")]
public string InfoType { get; set; }
[JsonProperty("info_id")]
public int InfoId { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("info")]
public TitleInfo Info { get; set; }
[JsonProperty("votes")]
public int Votes { get; set; }
[JsonProperty("vote_count")]
public int VoteCount { get; set; }
[JsonProperty("locked")]
public bool Locked { get; set; }
}
public class AddYearMapping
{
[JsonProperty("tmdbid")]
public string Tmdbid { get; set; }
[JsonProperty("info_type")]
public string InfoType { get; set; }
[JsonProperty("info_id")]
public int InfoId { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("info")]
public YearInfo Info { get; set; }
[JsonProperty("votes")]
public int Votes { get; set; }
[JsonProperty("vote_count")]
public int VoteCount { get; set; }
[JsonProperty("locked")]
public bool Locked { get; set; }
}
}

@ -2,7 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using System.ServiceModel;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
@ -19,6 +20,7 @@ using NzbDrone.Common.Serializer;
using NzbDrone.Core.NetImport.ImportExclusions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource.RadarrAPI;
using NzbDrone.Core.Movies.AlternativeTitles;
namespace NzbDrone.Core.MetadataSource.SkyHook
{
@ -33,12 +35,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private readonly IMovieService _movieService;
private readonly IPreDBService _predbService;
private readonly IImportExclusionsService _exclusionService;
private readonly IAlternativeTitleService _altTitleService;
private readonly IRadarrAPIClient _radarrAPI;
private readonly IHttpRequestBuilderFactory _apiBuilder;
public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, IMovieService movieService,
IPreDBService predbService, IImportExclusionsService exclusionService, IRadarrAPIClient radarrAPI, Logger logger)
IPreDBService predbService, IImportExclusionsService exclusionService, IAlternativeTitleService altTitleService, IRadarrAPIClient radarrAPI, Logger logger)
{
_httpClient = httpClient;
_requestBuilder = requestBuilder.SkyHookTvdb;
@ -47,6 +50,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
_movieService = movieService;
_predbService = predbService;
_exclusionService = exclusionService;
_altTitleService = altTitleService;
_radarrAPI = radarrAPI;
_logger = logger;
@ -133,21 +137,28 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
}
var movie = new Movie();
var altTitles = new List<AlternativeTitle>();
if (langCode != "us")
if (langCode != "en")
{
movie.AlternativeTitles.Add(resource.original_title);
var iso = IsoLanguages.Find(resource.original_language);
if (iso != null)
{
altTitles.Add(new AlternativeTitle(resource.original_title, SourceType.TMDB, TmdbId, iso.Language));
}
//movie.AlternativeTitles.Add(resource.original_title);
}
foreach (var alternativeTitle in resource.alternative_titles.titles)
{
if (alternativeTitle.iso_3166_1.ToLower() == langCode)
{
movie.AlternativeTitles.Add(alternativeTitle.title);
altTitles.Add(new AlternativeTitle(alternativeTitle.title, SourceType.TMDB, TmdbId, IsoLanguages.Find(alternativeTitle.iso_3166_1.ToLower()).Language));
}
else if (alternativeTitle.iso_3166_1.ToLower() == "us")
{
movie.AlternativeTitles.Add(alternativeTitle.title);
altTitles.Add(new AlternativeTitle(alternativeTitle.title, SourceType.TMDB, TmdbId, Language.English));
}
}
@ -321,6 +332,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
}
}
movie.AlternativeTitles.AddRange(altTitles);
return movie;
}

@ -0,0 +1,77 @@
using System;
using Marr.Data;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Movies.AlternativeTitles
{
public class AlternativeTitle : ModelBase
{
public SourceType SourceType { get; set; }
public int MovieId { get; set; }
public string Title { get; set; }
public string CleanTitle { get; set; }
public int SourceId { get; set; }
public int Votes { get; set; }
public int VoteCount { get; set; }
public Language Language { get; set; }
public LazyLoaded<Movie> Movie { get; set; }
public AlternativeTitle()
{
}
public AlternativeTitle(string title, SourceType sourceType = SourceType.TMDB, int sourceId = 0, Language language = Language.English)
{
Title = title;
CleanTitle = title.CleanSeriesTitle();
SourceType = sourceType;
SourceId = sourceId;
Language = language;
}
public bool IsTrusted(int minVotes = 3)
{
switch (SourceType)
{
case SourceType.TMDB:
return Votes >= minVotes;
default:
return true;
}
}
public override bool Equals(object obj)
{
var item = obj as AlternativeTitle;
if (item == null)
{
return false;
}
return item.CleanTitle == this.CleanTitle;
}
public override String ToString()
{
return Title;
}
}
public enum SourceType
{
TMDB = 0,
Mappings = 1,
User = 2,
Indexer = 3
}
public class AlternativeYear
{
public int Year { get; set; }
public int SourceId { get; set; }
}
}

@ -0,0 +1,21 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Movies.AlternativeTitles
{
public interface IAlternativeTitleRepository : IBasicRepository<AlternativeTitle>
{
}
public class AlternativeTitleRepository : BasicRepository<AlternativeTitle>, IAlternativeTitleRepository
{
protected IMainDatabase _database;
public AlternativeTitleRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
_database = database;
}
}
}

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Movies.AlternativeTitles
{
public interface IAlternativeTitleService
{
List<AlternativeTitle> GetAllTitlesForMovie(Movie movie);
AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie);
List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie);
AlternativeTitle GetById(int id);
}
public class AlternativeTitleService : IAlternativeTitleService
{
private readonly IAlternativeTitleRepository _titleRepo;
private readonly IConfigService _configService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public AlternativeTitleService(IAlternativeTitleRepository titleRepo,
IEventAggregator eventAggregator,
IConfigService configService,
Logger logger)
{
_titleRepo = titleRepo;
_eventAggregator = eventAggregator;
_configService = configService;
_logger = logger;
}
public List<AlternativeTitle> GetAllTitlesForMovie(Movie movie)
{
return _titleRepo.All().ToList();
}
public AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie)
{
title.MovieId = movie.Id;
return _titleRepo.Insert(title);
}
public List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie)
{
titles.ForEach(t => t.MovieId = movie.Id);
_titleRepo.InsertMany(titles);
return titles;
}
public AlternativeTitle GetById(int id)
{
return _titleRepo.Get(id);
}
}
}

@ -12,6 +12,9 @@ namespace NzbDrone.Core.Notifications.Slack.Payloads
[JsonProperty("icon_emoji")]
public string IconEmoji { get; set; }
[JsonProperty("icon_url")]
public string IconUrl { get; set; }
public List<Attachment> Attachments { get; set; }
}
}

@ -3,6 +3,8 @@ using System.Collections.Generic;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Notifications.Slack.Payloads;
using NzbDrone.Core.Rest;
using NzbDrone.Core.Tv;
@ -14,10 +16,12 @@ namespace NzbDrone.Core.Notifications.Slack
{
public class Slack : NotificationBase<SlackSettings>
{
private readonly ISlackProxy _proxy;
private readonly Logger _logger;
public Slack(Logger logger)
public Slack(ISlackProxy proxy, Logger logger)
{
_proxy = proxy;
_logger = logger;
}
@ -27,86 +31,68 @@ namespace NzbDrone.Core.Notifications.Slack
public override void OnGrab(GrabMessage message)
{
var payload = new SlackPayload
{
IconEmoji = Settings.Icon,
Username = Settings.Username,
Text = $"Grabbed: {message.Message}",
Attachments = new List<Attachment>
{
new Attachment
{
Fallback = message.Message,
Title = message.Movie.Title,
Text = message.Message,
Color = "warning"
}
}
};
NotifySlack(payload);
var attachments = new List<Attachment>
{
new Attachment
{
Fallback = message.Message,
Title = message.Movie.Title,
Text = message.Message,
Color = "warning"
}
};
var payload = CreatePayload($"Grabbed: {message.Message}", attachments);
_proxy.SendPayload(payload, Settings);
}
public override void OnDownload(DownloadMessage message)
{
var payload = new SlackPayload
{
IconEmoji = Settings.Icon,
Username = Settings.Username,
Text = $"Imported: {message.Message}",
Attachments = new List<Attachment>
{
new Attachment
{
Fallback = message.Message,
Title = message.Movie.Title,
Text = message.Message,
Color = "good"
}
}
};
NotifySlack(payload);
var attachments = new List<Attachment>
{
new Attachment
{
Fallback = message.Message,
Title = message.Movie.Title,
Text = message.Message,
Color = "good"
}
};
var payload = CreatePayload($"Imported: {message.Message}", attachments);
_proxy.SendPayload(payload, Settings);
}
public override void OnMovieRename(Movie movie)
{
var payload = new SlackPayload
{
IconEmoji = Settings.Icon,
Username = Settings.Username,
Text = "Renamed",
Attachments = new List<Attachment>
{
new Attachment
{
Title = movie.Title,
}
}
};
NotifySlack(payload);
var attachments = new List<Attachment>
{
new Attachment
{
Title = movie.Title,
}
};
var payload = CreatePayload("Renamed", attachments);
_proxy.SendPayload(payload, Settings);
}
public override void OnRename(Series series)
{
var payload = new SlackPayload
{
IconEmoji = Settings.Icon,
Username = Settings.Username,
Text = "Renamed",
Attachments = new List<Attachment>
{
new Attachment
{
Title = series.Title,
}
}
};
var attachments = new List<Attachment>
{
new Attachment
{
Title = series.Title,
}
};
NotifySlack(payload);
}
var payload = CreatePayload("Renamed", attachments);
_proxy.SendPayload(payload, Settings);
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
@ -121,14 +107,10 @@ namespace NzbDrone.Core.Notifications.Slack
try
{
var message = $"Test message from Radarr posted at {DateTime.Now}";
var payload = new SlackPayload
{
IconEmoji = Settings.Icon,
Username = Settings.Username,
Text = message
};
NotifySlack(payload);
var payload = CreatePayload(message);
_proxy.SendPayload(payload, Settings);
}
catch (SlackExeption ex)
@ -139,24 +121,31 @@ namespace NzbDrone.Core.Notifications.Slack
return null;
}
private void NotifySlack(SlackPayload payload)
private SlackPayload CreatePayload(string message, List<Attachment> attachments = null)
{
try
var icon = Settings.Icon;
var payload = new SlackPayload
{
var client = RestClientFactory.BuildClient(Settings.WebHookUrl);
var request = new RestRequest(Method.POST)
{
RequestFormat = DataFormat.Json,
JsonSerializer = new JsonNetSerializer()
};
request.AddBody(payload);
client.ExecuteAndValidate(request);
}
catch (RestException ex)
Username = Settings.Username,
Text = message,
Attachments = attachments
};
if (icon.IsNotNullOrWhiteSpace())
{
_logger.Error(ex, "Unable to post payload {0}", payload);
throw new SlackExeption("Unable to post payload", ex);
// Set the correct icon based on the value
if (icon.StartsWith(":") && icon.EndsWith(":"))
{
payload.IconEmoji = icon;
}
else
{
payload.IconUrl = icon;
}
}
return payload;
}
}
}

@ -0,0 +1,46 @@
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Notifications.Slack.Payloads;
using NzbDrone.Core.Rest;
namespace NzbDrone.Core.Notifications.Slack
{
public interface ISlackProxy
{
void SendPayload(SlackPayload payload, SlackSettings settings);
}
public class SlackProxy : ISlackProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public SlackProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public void SendPayload(SlackPayload payload, SlackSettings settings)
{
try
{
var request = new HttpRequestBuilder(settings.WebHookUrl)
.Accept(HttpAccept.Json)
.Build();
request.Method = HttpMethod.POST;
request.Headers.ContentType = "application/json";
request.SetContent(payload.ToJson());
_httpClient.Execute(request);
}
catch (RestException ex)
{
_logger.Error(ex, "Unable to post payload {0}", payload);
throw new SlackExeption("Unable to post payload", ex);
}
}
}
}

@ -24,7 +24,7 @@ namespace NzbDrone.Core.Notifications.Slack
[FieldDefinition(1, Label = "Username", HelpText = "Choose the username that this integration will post as", Type = FieldType.Textbox)]
public string Username { get; set; }
[FieldDefinition(2, Label = "Icon", HelpText = "Change the icon that is used for messages from this integration", Type = FieldType.Textbox, HelpLink = "http://www.emoji-cheat-sheet.com/")]
[FieldDefinition(2, Label = "Icon", HelpText = "Change the icon that is used for messages from this integration (Emoji or URL)", Type = FieldType.Textbox, HelpLink = "http://www.emoji-cheat-sheet.com/")]
public string Icon { get; set; }
public NzbDroneValidationResult Validate()

@ -37,10 +37,10 @@ namespace NzbDrone.Core.Notifications.Twitter
AuthorizeNotification = "step1";
}
[FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")]
[FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Radarr/Radarr/wiki/Twitter-Notifications")]
public string ConsumerKey { get; set; }
[FieldDefinition(1, Label = "Consumer Secret", HelpText = "Consumer secret from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")]
[FieldDefinition(1, Label = "Consumer Secret", HelpText = "Consumer secret from a Twitter application", HelpLink = "https://github.com/Radarr/Radarr/wiki/Twitter-Notifications")]
public string ConsumerSecret { get; set; }
[FieldDefinition(2, Label = "Access Token", Advanced = true)]

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Notifications.Webhook
_service = service;
}
public override string Link => "https://github.com/Sonarr/Sonarr/wiki/Webhook";
public override string Link => "https://github.com/Radarr/Radarr/wiki/Webhook";
public override void OnGrab(GrabMessage message)
{

@ -125,6 +125,7 @@
<Compile Include="Authentication\UserRepository.cs" />
<Compile Include="Authentication\UserService.cs" />
<Compile Include="Datastore\Migration\123_create_netimport_table.cs" />
<Compile Include="Datastore\Migration\140_add_alternative_titles_table.cs" />
<Compile Include="MediaFiles\Events\MovieFileUpdatedEvent.cs" />
<Compile Include="Datastore\Migration\134_add_remux_qualities_for_the_wankers.cs" />
<Compile Include="Datastore\Migration\129_add_parsed_movie_info_to_pending_release.cs" />
@ -134,6 +135,9 @@
<Compile Include="Datastore\Migration\133_add_minimumavailability.cs" />
<Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" />
<Compile Include="Indexers\HDBits\HDBitsInfo.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" />
<Compile Include="NetImport\NetImportListLevels.cs" />
<Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" />
<Compile Include="NetImport\TMDb\TMDbSettings.cs" />
@ -966,6 +970,7 @@
<Compile Include="Notifications\Slack\Payloads\SlackPayload.cs" />
<Compile Include="Notifications\Slack\Slack.cs" />
<Compile Include="Notifications\Slack\SlackExeption.cs" />
<Compile Include="Notifications\Slack\SlackProxy.cs" />
<Compile Include="Notifications\Slack\SlackSettings.cs" />
<Compile Include="Notifications\Synology\SynologyException.cs" />
<Compile Include="Notifications\Synology\SynologyIndexer.cs" />

@ -28,7 +28,8 @@ namespace NzbDrone.Core.Parser
// new IsoLanguage("nl", "nld", Language.Flemish),
new IsoLanguage("el", "ell", Language.Greek),
new IsoLanguage("ko", "kor", Language.Korean),
new IsoLanguage("hu", "hun", Language.Hungarian)
new IsoLanguage("hu", "hun", Language.Hungarian)//,
//new IsoLanguage("he", "heb", Language.Hebrew)
};
public static IsoLanguage Find(string isoCode)

@ -24,6 +24,7 @@
Flemish = 19,
Greek = 20,
Korean = 21,
Hungarian = 22
Hungarian = 22,
Hebrew = 23
}
}

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Parser
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser));
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR|VO|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)",
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR|VO|VFF|VFQ|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -77,6 +77,9 @@ namespace NzbDrone.Core.Parser
if (lowerTitle.Contains("hungarian"))
return Language.Hungarian;
if (lowerTitle.Contains("hebrew"))
return Language.Hebrew;
var match = LanguageRegex.Match(title);
if (match.Groups["italian"].Captures.Cast<Capture>().Any())
@ -103,6 +106,9 @@ namespace NzbDrone.Core.Parser
if (match.Groups["hungarian"].Success)
return Language.Hungarian;
if (match.Groups["hebrew"].Success)
return Language.Hebrew;
return Language.English;
}

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Parser.Model
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } //TODO: Change to ParsedMovieInfo, for now though ParsedEpisodeInfo will do.
public ParsedMovieInfo ParsedMovieInfo { get; set; }
public Movie Movie { get; set; }
public bool DownloadAllowed { get; set; }
public MappingResultType MappingResult { get; set; }
public override string ToString()
{

@ -9,6 +9,7 @@ using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Parser.RomanNumerals;
using NzbDrone.Core.Tv;
@ -381,7 +382,7 @@ namespace NzbDrone.Core.Parser
{
var movie = _movieService.FindByImdbId(imdbId);
//Should fix practically all problems, where indexer is shite at adding correct imdbids to movies.
if (movie != null && parsedMovieInfo.Year > 1800 && parsedMovieInfo.Year != movie.Year)
if (movie != null && parsedMovieInfo.Year > 1800 && (parsedMovieInfo.Year != movie.Year && movie.SecondaryYear != parsedMovieInfo.Year))
{
result = new MappingResult { Movie = movie, MappingResultType = MappingResultType.WrongYear};
return false;
@ -458,9 +459,9 @@ namespace NzbDrone.Core.Parser
possibleTitles.Add(searchCriteria.Movie.CleanTitle);
foreach (string altTitle in searchCriteria.Movie.AlternativeTitles)
foreach (AlternativeTitle altTitle in searchCriteria.Movie.AlternativeTitles)
{
possibleTitles.Add(altTitle.CleanSeriesTitle());
possibleTitles.Add(altTitle.CleanTitle);
}
string cleanTitle = parsedMovieInfo.MovieTitle.CleanSeriesTitle();
@ -494,7 +495,7 @@ namespace NzbDrone.Core.Parser
if (possibleMovie != null)
{
if (parsedMovieInfo.Year < 1800 || possibleMovie.Year == parsedMovieInfo.Year)
if (parsedMovieInfo.Year < 1800 || possibleMovie.Year == parsedMovieInfo.Year || possibleMovie.SecondaryYear == parsedMovieInfo.Year)
{
result = new MappingResult { Movie = possibleMovie, MappingResultType = MappingResultType.Success };
return true;
@ -509,7 +510,7 @@ namespace NzbDrone.Core.Parser
cleanTitle.Contains(searchCriteria.Movie.CleanTitle))
{
possibleMovie = searchCriteria.Movie;
if (parsedMovieInfo.Year > 1800 && parsedMovieInfo.Year == possibleMovie.Year)
if (parsedMovieInfo.Year > 1800 && parsedMovieInfo.Year == possibleMovie.Year || possibleMovie.SecondaryYear == parsedMovieInfo.Year)
{
result = new MappingResult {Movie = possibleMovie, MappingResultType = MappingResultType.SuccessLenientMapping};
return true;

@ -22,13 +22,13 @@ namespace NzbDrone.Core.Profiles
public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent>
{
private readonly IProfileRepository _profileRepository;
private readonly ISeriesService _seriesService;
private readonly IMovieService _movieService;
private readonly Logger _logger;
public ProfileService(IProfileRepository profileRepository, ISeriesService seriesService, Logger logger)
public ProfileService(IProfileRepository profileRepository, IMovieService movieService, Logger logger)
{
_profileRepository = profileRepository;
_seriesService = seriesService;
_movieService = movieService;
_logger = logger;
}
@ -44,7 +44,7 @@ namespace NzbDrone.Core.Profiles
public void Delete(int id)
{
if (_seriesService.GetAllSeries().Any(c => c.ProfileId == id))
if (_movieService.GetAllMovies().Any(c => c.ProfileId == id))
{
throw new ProfileInUseException(id);
}

@ -6,6 +6,8 @@ using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.MediaFiles;
using System.IO;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
namespace NzbDrone.Core.Tv
{
@ -17,7 +19,7 @@ namespace NzbDrone.Core.Tv
Genres = new List<string>();
Actors = new List<Actor>();
Tags = new HashSet<int>();
AlternativeTitles = new List<string>();
AlternativeTitles = new List<AlternativeTitle>();
}
public int TmdbId { get; set; }
public string ImdbId { get; set; }
@ -52,7 +54,10 @@ namespace NzbDrone.Core.Tv
public LazyLoaded<MovieFile> MovieFile { get; set; }
public bool HasPreDBEntry { get; set; }
public int MovieFileId { get; set; }
public List<string> AlternativeTitles { get; set; }
//Get Loaded via a Join Query
public List<AlternativeTitle> AlternativeTitles { get; set; }
public int? SecondaryYear { get; set; }
public int SecondaryYearSourceId { get; set; }
public string YouTubeTrailerId{ get; set; }
public string Studio { get; set; }

@ -6,6 +6,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Datastore.Extensions;
using Marr.Data.QGen;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser.RomanNumerals;
using NzbDrone.Core.Qualities;
using CoreParser = NzbDrone.Core.Parser.Parser;
@ -103,7 +104,7 @@ namespace NzbDrone.Core.Tv
public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec)
{
if (pagingSpec.SortKey == "downloadedQuality")
/*if (pagingSpec.SortKey == "downloadedQuality")
{
var mapper = _database.GetDataMapper();
var offset = pagingSpec.PagingOffset();
@ -113,7 +114,7 @@ namespace NzbDrone.Core.Tv
{
direction = "DESC";
}
var q = mapper.Query<Movie>($"SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title {direction} LIMIT {offset},{limit};");
var q = Query.Select($"SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title {direction} LIMIT {offset},{limit};");
var q2 = mapper.Query<Movie>("SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title ASC;");
//var ok = q.BuildQuery();
@ -122,9 +123,11 @@ namespace NzbDrone.Core.Tv
pagingSpec.TotalRecords = q2.Count();
}
else
else*/
{
pagingSpec = base.GetPaged(pagingSpec);
//pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList();
//pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount();
}
if (pagingSpec.Records.Count == 0 && pagingSpec.Page != 1)
@ -136,6 +139,22 @@ namespace NzbDrone.Core.Tv
return pagingSpec;
}
/*protected override SortBuilder<Movie> GetPagedQuery(QueryBuilder<Movie> query, PagingSpec<Movie> pagingSpec)
{
return DataMapper.Query<Movie>().Join<Movie, AlternativeTitle>(JoinType.Left, m => m.AlternativeTitles,
(m, t) => m.Id == t.MovieId).Where(pagingSpec.FilterExpression)
.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
.Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize);
}*/
/*protected override SortBuilder<Movie> GetPagedQuery(QueryBuilder<Movie> query, PagingSpec<Movie> pagingSpec)
{
var newQuery = base.GetPagedQuery(query.Join<Movie, AlternativeTitle>(JoinType.Left, m => m.JoinAlternativeTitles, (movie, title) => title.MovieId == movie.Id), pagingSpec);
System.Console.WriteLine(newQuery.ToString());
return newQuery;
}*/
public SortBuilder<Movie> GetMoviesWithoutFilesQuery(PagingSpec<Movie> pagingSpec)
{
@ -247,22 +266,39 @@ namespace NzbDrone.Core.Tv
if (result == null)
{
IEnumerable<Movie> movies = All();
/*IEnumerable<Movie> movies = All();
Func<string, string> titleCleaner = title => CoreParser.CleanSeriesTitle(title.ToLower());
Func<IEnumerable<string>, string, bool> altTitleComparer =
Func<IEnumerable<AlternativeTitle>, string, bool> altTitleComparer =
(alternativeTitles, atitle) =>
alternativeTitles.Any(altTitle => titleCleaner(altTitle) == atitle);
alternativeTitles.Any(altTitle => altTitle.CleanTitle == atitle);*/
result = movies.Where(m => altTitleComparer(m.AlternativeTitles, cleanTitle) ||
/*result = movies.Where(m => altTitleComparer(m.AlternativeTitles, cleanTitle) ||
altTitleComparer(m.AlternativeTitles, cleanTitleWithRomanNumbers) ||
altTitleComparer(m.AlternativeTitles, cleanTitleWithArabicNumbers)).FirstWithYear(year);
altTitleComparer(m.AlternativeTitles, cleanTitleWithArabicNumbers)).FirstWithYear(year);*/
//result = Query.Join<Movie, AlternativeTitle>(JoinType.Inner, m => m._newAltTitles,
//(m, t) => m.Id == t.MovieId && (t.CleanTitle == cleanTitle)).FirstWithYear(year);
result = Query.Where<AlternativeTitle>(t =>
t.CleanTitle == cleanTitle || t.CleanTitle == cleanTitleWithArabicNumbers
|| t.CleanTitle == cleanTitleWithRomanNumbers).FirstWithYear(year);
}
}
return result;
/*return year.HasValue
? results?.FirstOrDefault(movie => movie.Year == year.Value)
: results?.FirstOrDefault();*/
: results?.FirstOrDefault();*/
}
protected override QueryBuilder<Movie> AddJoinQueries(QueryBuilder<Movie> baseQuery)
{
baseQuery = base.AddJoinQueries(baseQuery);
baseQuery = baseQuery.Join<Movie, AlternativeTitle>(JoinType.Left, m => m.AlternativeTitles,
(m, t) => m.Id == t.MovieId);
return baseQuery;
}
public Movie FindByTmdbId(int tmdbid)

@ -16,7 +16,7 @@ namespace NzbDrone.Core
{
public static Movie FirstWithYear(this SortBuilder<Movie> query, int? year)
{
return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year) : query.FirstOrDefault();
return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year || movie.SecondaryYear == year) : query.FirstOrDefault();
}
}
@ -24,7 +24,7 @@ namespace NzbDrone.Core
{
public static Movie FirstWithYear(this IEnumerable<Movie> query, int? year)
{
return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year) : query.FirstOrDefault();
return year.HasValue ? query.FirstOrDefault(movie => movie.Year == year || movie.SecondaryYear == year) : query.FirstOrDefault();
}
}
}

@ -14,6 +14,8 @@ using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Tv.Commands;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MetadataSource.RadarrAPI;
using NzbDrone.Core.Movies.AlternativeTitles;
namespace NzbDrone.Core.Tv
{
@ -21,27 +23,34 @@ namespace NzbDrone.Core.Tv
{
private readonly IProvideMovieInfo _movieInfo;
private readonly IMovieService _movieService;
private readonly IAlternativeTitleService _titleService;
private readonly IRefreshEpisodeService _refreshEpisodeService;
private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IDiskScanService _diskScanService;
private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed;
private readonly IRadarrAPIClient _apiClient;
private readonly Logger _logger;
public RefreshMovieService(IProvideMovieInfo movieInfo,
IMovieService movieService,
IAlternativeTitleService titleService,
IRefreshEpisodeService refreshEpisodeService,
IEventAggregator eventAggregator,
IDiskScanService diskScanService,
IRadarrAPIClient apiClient,
ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed,
IManageCommandQueue commandQueue,
Logger logger)
{
_movieInfo = movieInfo;
_movieService = movieService;
_titleService = titleService;
_refreshEpisodeService = refreshEpisodeService;
_eventAggregator = eventAggregator;
_commandQueueManager = commandQueue;
_apiClient = apiClient;
_commandQueueManager = commandQueue;
_diskScanService = diskScanService;
_checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed;
_logger = logger;
@ -85,7 +94,7 @@ namespace NzbDrone.Core.Tv
movie.Certification = movieInfo.Certification;
movie.InCinemas = movieInfo.InCinemas;
movie.Website = movieInfo.Website;
movie.AlternativeTitles = movieInfo.AlternativeTitles;
//movie.AlternativeTitles = movieInfo.AlternativeTitles;
movie.Year = movieInfo.Year;
movie.PhysicalRelease = movieInfo.PhysicalRelease;
movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId;
@ -102,8 +111,47 @@ namespace NzbDrone.Core.Tv
_logger.Warn(e, "Couldn't update movie path for " + movie.Path);
}
movieInfo.AlternativeTitles = movieInfo.AlternativeTitles.Where(t => t.CleanTitle != movie.CleanTitle)
.DistinctBy(t => t.CleanTitle)
.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles, t => t.CleanTitle, EqualityComparer<string>.Default).ToList();
try
{
var mappings = _apiClient.AlternativeTitlesAndYearForMovie(movieInfo.TmdbId);
var mappingsTitles = mappings.Item1;
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(movieInfo.AlternativeTitles, movie));
mappingsTitles = mappingsTitles.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles,
t => t.CleanTitle, EqualityComparer<string>.Default).ToList();
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(mappingsTitles, movie));
if (mappings.Item2 != null)
{
movie.SecondaryYear = mappings.Item2.Year;
movie.SecondaryYearSourceId = mappings.Item2.SourceId;
}
}
catch (RadarrAPIException ex)
{
//Not that wild, could just be a 404.
}
_movieService.UpdateMovie(movie);
try
{
var newTitles = movieInfo.AlternativeTitles.Except(movie.AlternativeTitles);
//_titleService.AddAltTitles(newTitles.ToList(), movie);
}
catch (Exception e)
{
_logger.Debug(e, "Failed adding alternative titles.");
throw;
}
_logger.Debug("Finished movie refresh for {0}", movie.Title);
_eventAggregator.PublishEvent(new MovieUpdatedEvent(movie));
}

@ -22,6 +22,7 @@ namespace NzbDrone.Core.Tv
public bool ShouldRefresh(Movie movie)
{
//return false;
if (movie.LastInfoSync < DateTime.UtcNow.AddDays(-30))
{
_logger.Trace("Movie {0} last updated more than 30 days ago, should refresh.", movie.Title);

@ -47,7 +47,7 @@ namespace NzbDrone.Integration.Test.ApiTests
releaseResource.Age.Should().BeGreaterOrEqualTo(-1);
releaseResource.Title.Should().NotBeNullOrWhiteSpace();
releaseResource.DownloadUrl.Should().NotBeNullOrWhiteSpace();
releaseResource.SeriesTitle.Should().NotBeNullOrWhiteSpace();
releaseResource.MovieTitle.Should().NotBeNullOrWhiteSpace();
//TODO: uncomment these after moving to restsharp for rss
//releaseResource.NzbInfoUrl.Should().NotBeNullOrWhiteSpace();
//releaseResource.Size.Should().BeGreaterThan(0);

@ -14,6 +14,7 @@
"define": true,
"window": true,
"document": true,
"console": true
"console": true,
"_": true
}
}

@ -79,7 +79,7 @@ module.exports = Marionette.Layout.extend({
if (options.action === "search") {
this.search({term: options.query});
} else if (options.action == "discover") {
} else if (options.action === "discover") {
this.isDiscover = true;
}
@ -254,7 +254,7 @@ module.exports = Marionette.Layout.extend({
_discover : function(action) {
if (this.collection.action === action) {
return
return;
}
if (this.collection.specialProperty === "special") {

@ -39,14 +39,14 @@ module.exports = Backgrid.Cell.extend({
break;
case "PTP_Approved":
addon = "✔";
title = "Approved by PTP"
title = "Approved by PTP";
break;
case "HDB_Internal":
addon = "⭐️";
title = "HDBits Internal";
break;
}
if (addon != "") {
if (addon !== "") {
html += "<span title='{0}'>{1}</span> ".format(title, addon);
}
});

@ -7,6 +7,15 @@
border-radius: .1em;
}
.@{fa-css-prefix}-pull-left { float: left; }
.@{fa-css-prefix}-pull-right { float: right; }
.@{fa-css-prefix} {
&.@{fa-css-prefix}-pull-left { margin-right: .3em; }
&.@{fa-css-prefix}-pull-right { margin-left: .3em; }
}
/* Deprecated as of 4.4.0 */
.pull-right { float: right; }
.pull-left { float: left; }

@ -3,11 +3,10 @@
.@{fa-css-prefix} {
display: inline-block;
font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform: translate(0, 0); // ensures no half-pixel rendering in firefox
}

@ -1,5 +1,5 @@
/*!
* Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@ -15,3 +15,4 @@
@import "rotated-flipped.less";
@import "stacked.less";
@import "icons.less";
@import "screen-reader.less";

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 434 KiB

@ -163,6 +163,7 @@
.@{fa-css-prefix}-github:before { content: @fa-var-github; }
.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; }
.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; }
.@{fa-css-prefix}-feed:before,
.@{fa-css-prefix}-rss:before { content: @fa-var-rss; }
.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; }
.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; }
@ -437,7 +438,7 @@
.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; }
.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; }
.@{fa-css-prefix}-digg:before { content: @fa-var-digg; }
.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; }
.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; }
.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; }
.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; }
@ -487,11 +488,14 @@
.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; }
.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; }
.@{fa-css-prefix}-ra:before,
.@{fa-css-prefix}-resistance:before,
.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; }
.@{fa-css-prefix}-ge:before,
.@{fa-css-prefix}-empire:before { content: @fa-var-empire; }
.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; }
.@{fa-css-prefix}-git:before { content: @fa-var-git; }
.@{fa-css-prefix}-y-combinator-square:before,
.@{fa-css-prefix}-yc-square:before,
.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; }
.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; }
.@{fa-css-prefix}-qq:before { content: @fa-var-qq; }
@ -502,7 +506,6 @@
.@{fa-css-prefix}-send-o:before,
.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; }
.@{fa-css-prefix}-history:before { content: @fa-var-history; }
.@{fa-css-prefix}-genderless:before,
.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; }
.@{fa-css-prefix}-header:before { content: @fa-var-header; }
.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; }
@ -573,6 +576,7 @@
.@{fa-css-prefix}-venus:before { content: @fa-var-venus; }
.@{fa-css-prefix}-mars:before { content: @fa-var-mars; }
.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; }
.@{fa-css-prefix}-intersex:before,
.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; }
.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; }
.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; }
@ -582,6 +586,7 @@
.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; }
.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; }
.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; }
.@{fa-css-prefix}-genderless:before { content: @fa-var-genderless; }
.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; }
.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; }
.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; }
@ -594,3 +599,191 @@
.@{fa-css-prefix}-train:before { content: @fa-var-train; }
.@{fa-css-prefix}-subway:before { content: @fa-var-subway; }
.@{fa-css-prefix}-medium:before { content: @fa-var-medium; }
.@{fa-css-prefix}-yc:before,
.@{fa-css-prefix}-y-combinator:before { content: @fa-var-y-combinator; }
.@{fa-css-prefix}-optin-monster:before { content: @fa-var-optin-monster; }
.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; }
.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; }
.@{fa-css-prefix}-battery-4:before,
.@{fa-css-prefix}-battery:before,
.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; }
.@{fa-css-prefix}-battery-3:before,
.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; }
.@{fa-css-prefix}-battery-2:before,
.@{fa-css-prefix}-battery-half:before { content: @fa-var-battery-half; }
.@{fa-css-prefix}-battery-1:before,
.@{fa-css-prefix}-battery-quarter:before { content: @fa-var-battery-quarter; }
.@{fa-css-prefix}-battery-0:before,
.@{fa-css-prefix}-battery-empty:before { content: @fa-var-battery-empty; }
.@{fa-css-prefix}-mouse-pointer:before { content: @fa-var-mouse-pointer; }
.@{fa-css-prefix}-i-cursor:before { content: @fa-var-i-cursor; }
.@{fa-css-prefix}-object-group:before { content: @fa-var-object-group; }
.@{fa-css-prefix}-object-ungroup:before { content: @fa-var-object-ungroup; }
.@{fa-css-prefix}-sticky-note:before { content: @fa-var-sticky-note; }
.@{fa-css-prefix}-sticky-note-o:before { content: @fa-var-sticky-note-o; }
.@{fa-css-prefix}-cc-jcb:before { content: @fa-var-cc-jcb; }
.@{fa-css-prefix}-cc-diners-club:before { content: @fa-var-cc-diners-club; }
.@{fa-css-prefix}-clone:before { content: @fa-var-clone; }
.@{fa-css-prefix}-balance-scale:before { content: @fa-var-balance-scale; }
.@{fa-css-prefix}-hourglass-o:before { content: @fa-var-hourglass-o; }
.@{fa-css-prefix}-hourglass-1:before,
.@{fa-css-prefix}-hourglass-start:before { content: @fa-var-hourglass-start; }
.@{fa-css-prefix}-hourglass-2:before,
.@{fa-css-prefix}-hourglass-half:before { content: @fa-var-hourglass-half; }
.@{fa-css-prefix}-hourglass-3:before,
.@{fa-css-prefix}-hourglass-end:before { content: @fa-var-hourglass-end; }
.@{fa-css-prefix}-hourglass:before { content: @fa-var-hourglass; }
.@{fa-css-prefix}-hand-grab-o:before,
.@{fa-css-prefix}-hand-rock-o:before { content: @fa-var-hand-rock-o; }
.@{fa-css-prefix}-hand-stop-o:before,
.@{fa-css-prefix}-hand-paper-o:before { content: @fa-var-hand-paper-o; }
.@{fa-css-prefix}-hand-scissors-o:before { content: @fa-var-hand-scissors-o; }
.@{fa-css-prefix}-hand-lizard-o:before { content: @fa-var-hand-lizard-o; }
.@{fa-css-prefix}-hand-spock-o:before { content: @fa-var-hand-spock-o; }
.@{fa-css-prefix}-hand-pointer-o:before { content: @fa-var-hand-pointer-o; }
.@{fa-css-prefix}-hand-peace-o:before { content: @fa-var-hand-peace-o; }
.@{fa-css-prefix}-trademark:before { content: @fa-var-trademark; }
.@{fa-css-prefix}-registered:before { content: @fa-var-registered; }
.@{fa-css-prefix}-creative-commons:before { content: @fa-var-creative-commons; }
.@{fa-css-prefix}-gg:before { content: @fa-var-gg; }
.@{fa-css-prefix}-gg-circle:before { content: @fa-var-gg-circle; }
.@{fa-css-prefix}-tripadvisor:before { content: @fa-var-tripadvisor; }
.@{fa-css-prefix}-odnoklassniki:before { content: @fa-var-odnoklassniki; }
.@{fa-css-prefix}-odnoklassniki-square:before { content: @fa-var-odnoklassniki-square; }
.@{fa-css-prefix}-get-pocket:before { content: @fa-var-get-pocket; }
.@{fa-css-prefix}-wikipedia-w:before { content: @fa-var-wikipedia-w; }
.@{fa-css-prefix}-safari:before { content: @fa-var-safari; }
.@{fa-css-prefix}-chrome:before { content: @fa-var-chrome; }
.@{fa-css-prefix}-firefox:before { content: @fa-var-firefox; }
.@{fa-css-prefix}-opera:before { content: @fa-var-opera; }
.@{fa-css-prefix}-internet-explorer:before { content: @fa-var-internet-explorer; }
.@{fa-css-prefix}-tv:before,
.@{fa-css-prefix}-television:before { content: @fa-var-television; }
.@{fa-css-prefix}-contao:before { content: @fa-var-contao; }
.@{fa-css-prefix}-500px:before { content: @fa-var-500px; }
.@{fa-css-prefix}-amazon:before { content: @fa-var-amazon; }
.@{fa-css-prefix}-calendar-plus-o:before { content: @fa-var-calendar-plus-o; }
.@{fa-css-prefix}-calendar-minus-o:before { content: @fa-var-calendar-minus-o; }
.@{fa-css-prefix}-calendar-times-o:before { content: @fa-var-calendar-times-o; }
.@{fa-css-prefix}-calendar-check-o:before { content: @fa-var-calendar-check-o; }
.@{fa-css-prefix}-industry:before { content: @fa-var-industry; }
.@{fa-css-prefix}-map-pin:before { content: @fa-var-map-pin; }
.@{fa-css-prefix}-map-signs:before { content: @fa-var-map-signs; }
.@{fa-css-prefix}-map-o:before { content: @fa-var-map-o; }
.@{fa-css-prefix}-map:before { content: @fa-var-map; }
.@{fa-css-prefix}-commenting:before { content: @fa-var-commenting; }
.@{fa-css-prefix}-commenting-o:before { content: @fa-var-commenting-o; }
.@{fa-css-prefix}-houzz:before { content: @fa-var-houzz; }
.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo; }
.@{fa-css-prefix}-black-tie:before { content: @fa-var-black-tie; }
.@{fa-css-prefix}-fonticons:before { content: @fa-var-fonticons; }
.@{fa-css-prefix}-reddit-alien:before { content: @fa-var-reddit-alien; }
.@{fa-css-prefix}-edge:before { content: @fa-var-edge; }
.@{fa-css-prefix}-credit-card-alt:before { content: @fa-var-credit-card-alt; }
.@{fa-css-prefix}-codiepie:before { content: @fa-var-codiepie; }
.@{fa-css-prefix}-modx:before { content: @fa-var-modx; }
.@{fa-css-prefix}-fort-awesome:before { content: @fa-var-fort-awesome; }
.@{fa-css-prefix}-usb:before { content: @fa-var-usb; }
.@{fa-css-prefix}-product-hunt:before { content: @fa-var-product-hunt; }
.@{fa-css-prefix}-mixcloud:before { content: @fa-var-mixcloud; }
.@{fa-css-prefix}-scribd:before { content: @fa-var-scribd; }
.@{fa-css-prefix}-pause-circle:before { content: @fa-var-pause-circle; }
.@{fa-css-prefix}-pause-circle-o:before { content: @fa-var-pause-circle-o; }
.@{fa-css-prefix}-stop-circle:before { content: @fa-var-stop-circle; }
.@{fa-css-prefix}-stop-circle-o:before { content: @fa-var-stop-circle-o; }
.@{fa-css-prefix}-shopping-bag:before { content: @fa-var-shopping-bag; }
.@{fa-css-prefix}-shopping-basket:before { content: @fa-var-shopping-basket; }
.@{fa-css-prefix}-hashtag:before { content: @fa-var-hashtag; }
.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; }
.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; }
.@{fa-css-prefix}-percent:before { content: @fa-var-percent; }
.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; }
.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; }
.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; }
.@{fa-css-prefix}-envira:before { content: @fa-var-envira; }
.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; }
.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; }
.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; }
.@{fa-css-prefix}-blind:before { content: @fa-var-blind; }
.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; }
.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; }
.@{fa-css-prefix}-braille:before { content: @fa-var-braille; }
.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; }
.@{fa-css-prefix}-asl-interpreting:before,
.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; }
.@{fa-css-prefix}-deafness:before,
.@{fa-css-prefix}-hard-of-hearing:before,
.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; }
.@{fa-css-prefix}-glide:before { content: @fa-var-glide; }
.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; }
.@{fa-css-prefix}-signing:before,
.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; }
.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; }
.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; }
.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; }
.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; }
.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; }
.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; }
.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; }
.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; }
.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; }
.@{fa-css-prefix}-google-plus-circle:before,
.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus-official; }
.@{fa-css-prefix}-fa:before,
.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; }
.@{fa-css-prefix}-handshake-o:before { content: @fa-var-handshake-o; }
.@{fa-css-prefix}-envelope-open:before { content: @fa-var-envelope-open; }
.@{fa-css-prefix}-envelope-open-o:before { content: @fa-var-envelope-open-o; }
.@{fa-css-prefix}-linode:before { content: @fa-var-linode; }
.@{fa-css-prefix}-address-book:before { content: @fa-var-address-book; }
.@{fa-css-prefix}-address-book-o:before { content: @fa-var-address-book-o; }
.@{fa-css-prefix}-vcard:before,
.@{fa-css-prefix}-address-card:before { content: @fa-var-address-card; }
.@{fa-css-prefix}-vcard-o:before,
.@{fa-css-prefix}-address-card-o:before { content: @fa-var-address-card-o; }
.@{fa-css-prefix}-user-circle:before { content: @fa-var-user-circle; }
.@{fa-css-prefix}-user-circle-o:before { content: @fa-var-user-circle-o; }
.@{fa-css-prefix}-user-o:before { content: @fa-var-user-o; }
.@{fa-css-prefix}-id-badge:before { content: @fa-var-id-badge; }
.@{fa-css-prefix}-drivers-license:before,
.@{fa-css-prefix}-id-card:before { content: @fa-var-id-card; }
.@{fa-css-prefix}-drivers-license-o:before,
.@{fa-css-prefix}-id-card-o:before { content: @fa-var-id-card-o; }
.@{fa-css-prefix}-quora:before { content: @fa-var-quora; }
.@{fa-css-prefix}-free-code-camp:before { content: @fa-var-free-code-camp; }
.@{fa-css-prefix}-telegram:before { content: @fa-var-telegram; }
.@{fa-css-prefix}-thermometer-4:before,
.@{fa-css-prefix}-thermometer:before,
.@{fa-css-prefix}-thermometer-full:before { content: @fa-var-thermometer-full; }
.@{fa-css-prefix}-thermometer-3:before,
.@{fa-css-prefix}-thermometer-three-quarters:before { content: @fa-var-thermometer-three-quarters; }
.@{fa-css-prefix}-thermometer-2:before,
.@{fa-css-prefix}-thermometer-half:before { content: @fa-var-thermometer-half; }
.@{fa-css-prefix}-thermometer-1:before,
.@{fa-css-prefix}-thermometer-quarter:before { content: @fa-var-thermometer-quarter; }
.@{fa-css-prefix}-thermometer-0:before,
.@{fa-css-prefix}-thermometer-empty:before { content: @fa-var-thermometer-empty; }
.@{fa-css-prefix}-shower:before { content: @fa-var-shower; }
.@{fa-css-prefix}-bathtub:before,
.@{fa-css-prefix}-s15:before,
.@{fa-css-prefix}-bath:before { content: @fa-var-bath; }
.@{fa-css-prefix}-podcast:before { content: @fa-var-podcast; }
.@{fa-css-prefix}-window-maximize:before { content: @fa-var-window-maximize; }
.@{fa-css-prefix}-window-minimize:before { content: @fa-var-window-minimize; }
.@{fa-css-prefix}-window-restore:before { content: @fa-var-window-restore; }
.@{fa-css-prefix}-times-rectangle:before,
.@{fa-css-prefix}-window-close:before { content: @fa-var-window-close; }
.@{fa-css-prefix}-times-rectangle-o:before,
.@{fa-css-prefix}-window-close-o:before { content: @fa-var-window-close-o; }
.@{fa-css-prefix}-bandcamp:before { content: @fa-var-bandcamp; }
.@{fa-css-prefix}-grav:before { content: @fa-var-grav; }
.@{fa-css-prefix}-etsy:before { content: @fa-var-etsy; }
.@{fa-css-prefix}-imdb:before { content: @fa-var-imdb; }
.@{fa-css-prefix}-ravelry:before { content: @fa-var-ravelry; }
.@{fa-css-prefix}-eercast:before { content: @fa-var-eercast; }
.@{fa-css-prefix}-microchip:before { content: @fa-var-microchip; }
.@{fa-css-prefix}-snowflake-o:before { content: @fa-var-snowflake-o; }
.@{fa-css-prefix}-superpowers:before { content: @fa-var-superpowers; }
.@{fa-css-prefix}-wpexplorer:before { content: @fa-var-wpexplorer; }
.@{fa-css-prefix}-meetup:before { content: @fa-var-meetup; }

@ -3,25 +3,58 @@
.fa-icon() {
display: inline-block;
font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform: translate(0, 0); // ensures no half-pixel rendering in firefox
}
.fa-icon-rotate(@degrees, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})";
-webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees);
transform: rotate(@degrees);
}
.fa-icon-flip(@horiz, @vert, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)";
-webkit-transform: scale(@horiz, @vert);
-ms-transform: scale(@horiz, @vert);
transform: scale(@horiz, @vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
.sr-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
.sr-only-focusable() {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

@ -9,7 +9,7 @@
url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'),
url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'),
url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg');
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
}

@ -0,0 +1,5 @@
// Screen Readers
// -------------------------
.sr-only { .sr-only(); }
.sr-only-focusable { .sr-only-focusable(); }

@ -3,20 +3,28 @@
@fa-font-path: "../Content/FontAwesome";
@fa-font-size-base: 14px;
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.3.0/fonts"; // for referencing Bootstrap CDN font files directly
@fa-line-height-base: 1;
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly
@fa-css-prefix: fa;
@fa-version: "4.3.0";
@fa-version: "4.7.0";
@fa-border-color: #eee;
@fa-inverse: #fff;
@fa-li-width: (30em / 14);
@fa-var-500px: "\f26e";
@fa-var-address-book: "\f2b9";
@fa-var-address-book-o: "\f2ba";
@fa-var-address-card: "\f2bb";
@fa-var-address-card-o: "\f2bc";
@fa-var-adjust: "\f042";
@fa-var-adn: "\f170";
@fa-var-align-center: "\f037";
@fa-var-align-justify: "\f039";
@fa-var-align-left: "\f036";
@fa-var-align-right: "\f038";
@fa-var-amazon: "\f270";
@fa-var-ambulance: "\f0f9";
@fa-var-american-sign-language-interpreting: "\f2a3";
@fa-var-anchor: "\f13d";
@fa-var-android: "\f17b";
@fa-var-angellist: "\f209";
@ -47,16 +55,34 @@
@fa-var-arrows-alt: "\f0b2";
@fa-var-arrows-h: "\f07e";
@fa-var-arrows-v: "\f07d";
@fa-var-asl-interpreting: "\f2a3";
@fa-var-assistive-listening-systems: "\f2a2";
@fa-var-asterisk: "\f069";
@fa-var-at: "\f1fa";
@fa-var-audio-description: "\f29e";
@fa-var-automobile: "\f1b9";
@fa-var-backward: "\f04a";
@fa-var-balance-scale: "\f24e";
@fa-var-ban: "\f05e";
@fa-var-bandcamp: "\f2d5";
@fa-var-bank: "\f19c";
@fa-var-bar-chart: "\f080";
@fa-var-bar-chart-o: "\f080";
@fa-var-barcode: "\f02a";
@fa-var-bars: "\f0c9";
@fa-var-bath: "\f2cd";
@fa-var-bathtub: "\f2cd";
@fa-var-battery: "\f240";
@fa-var-battery-0: "\f244";
@fa-var-battery-1: "\f243";
@fa-var-battery-2: "\f242";
@fa-var-battery-3: "\f241";
@fa-var-battery-4: "\f240";
@fa-var-battery-empty: "\f244";
@fa-var-battery-full: "\f240";
@fa-var-battery-half: "\f242";
@fa-var-battery-quarter: "\f243";
@fa-var-battery-three-quarters: "\f241";
@fa-var-bed: "\f236";
@fa-var-beer: "\f0fc";
@fa-var-behance: "\f1b4";
@ -71,12 +97,17 @@
@fa-var-bitbucket: "\f171";
@fa-var-bitbucket-square: "\f172";
@fa-var-bitcoin: "\f15a";
@fa-var-black-tie: "\f27e";
@fa-var-blind: "\f29d";
@fa-var-bluetooth: "\f293";
@fa-var-bluetooth-b: "\f294";
@fa-var-bold: "\f032";
@fa-var-bolt: "\f0e7";
@fa-var-bomb: "\f1e2";
@fa-var-book: "\f02d";
@fa-var-bookmark: "\f02e";
@fa-var-bookmark-o: "\f097";
@fa-var-braille: "\f2a1";
@fa-var-briefcase: "\f0b1";
@fa-var-btc: "\f15a";
@fa-var-bug: "\f188";
@ -89,7 +120,11 @@
@fa-var-cab: "\f1ba";
@fa-var-calculator: "\f1ec";
@fa-var-calendar: "\f073";
@fa-var-calendar-check-o: "\f274";
@fa-var-calendar-minus-o: "\f272";
@fa-var-calendar-o: "\f133";
@fa-var-calendar-plus-o: "\f271";
@fa-var-calendar-times-o: "\f273";
@fa-var-camera: "\f030";
@fa-var-camera-retro: "\f083";
@fa-var-car: "\f1b9";
@ -105,7 +140,9 @@
@fa-var-cart-plus: "\f217";
@fa-var-cc: "\f20a";
@fa-var-cc-amex: "\f1f3";
@fa-var-cc-diners-club: "\f24c";
@fa-var-cc-discover: "\f1f2";
@fa-var-cc-jcb: "\f24b";
@fa-var-cc-mastercard: "\f1f1";
@fa-var-cc-paypal: "\f1f4";
@fa-var-cc-stripe: "\f1f5";
@ -127,12 +164,14 @@
@fa-var-chevron-right: "\f054";
@fa-var-chevron-up: "\f077";
@fa-var-child: "\f1ae";
@fa-var-chrome: "\f268";
@fa-var-circle: "\f111";
@fa-var-circle-o: "\f10c";
@fa-var-circle-o-notch: "\f1ce";
@fa-var-circle-thin: "\f1db";
@fa-var-clipboard: "\f0ea";
@fa-var-clock-o: "\f017";
@fa-var-clone: "\f24d";
@fa-var-close: "\f00d";
@fa-var-cloud: "\f0c2";
@fa-var-cloud-download: "\f0ed";
@ -141,20 +180,26 @@
@fa-var-code: "\f121";
@fa-var-code-fork: "\f126";
@fa-var-codepen: "\f1cb";
@fa-var-codiepie: "\f284";
@fa-var-coffee: "\f0f4";
@fa-var-cog: "\f013";
@fa-var-cogs: "\f085";
@fa-var-columns: "\f0db";
@fa-var-comment: "\f075";
@fa-var-comment-o: "\f0e5";
@fa-var-commenting: "\f27a";
@fa-var-commenting-o: "\f27b";
@fa-var-comments: "\f086";
@fa-var-comments-o: "\f0e6";
@fa-var-compass: "\f14e";
@fa-var-compress: "\f066";
@fa-var-connectdevelop: "\f20e";
@fa-var-contao: "\f26d";
@fa-var-copy: "\f0c5";
@fa-var-copyright: "\f1f9";
@fa-var-creative-commons: "\f25e";
@fa-var-credit-card: "\f09d";
@fa-var-credit-card-alt: "\f283";
@fa-var-crop: "\f125";
@fa-var-crosshairs: "\f05b";
@fa-var-css3: "\f13c";
@ -165,6 +210,8 @@
@fa-var-dashboard: "\f0e4";
@fa-var-dashcube: "\f210";
@fa-var-database: "\f1c0";
@fa-var-deaf: "\f2a4";
@fa-var-deafness: "\f2a4";
@fa-var-dedent: "\f03b";
@fa-var-delicious: "\f1a5";
@fa-var-desktop: "\f108";
@ -175,17 +222,25 @@
@fa-var-dot-circle-o: "\f192";
@fa-var-download: "\f019";
@fa-var-dribbble: "\f17d";
@fa-var-drivers-license: "\f2c2";
@fa-var-drivers-license-o: "\f2c3";
@fa-var-dropbox: "\f16b";
@fa-var-drupal: "\f1a9";
@fa-var-edge: "\f282";
@fa-var-edit: "\f044";
@fa-var-eercast: "\f2da";
@fa-var-eject: "\f052";
@fa-var-ellipsis-h: "\f141";
@fa-var-ellipsis-v: "\f142";
@fa-var-empire: "\f1d1";
@fa-var-envelope: "\f0e0";
@fa-var-envelope-o: "\f003";
@fa-var-envelope-open: "\f2b6";
@fa-var-envelope-open-o: "\f2b7";
@fa-var-envelope-square: "\f199";
@fa-var-envira: "\f299";
@fa-var-eraser: "\f12d";
@fa-var-etsy: "\f2d7";
@fa-var-eur: "\f153";
@fa-var-euro: "\f153";
@fa-var-exchange: "\f0ec";
@ -193,11 +248,13 @@
@fa-var-exclamation-circle: "\f06a";
@fa-var-exclamation-triangle: "\f071";
@fa-var-expand: "\f065";
@fa-var-expeditedssl: "\f23e";
@fa-var-external-link: "\f08e";
@fa-var-external-link-square: "\f14c";
@fa-var-eye: "\f06e";
@fa-var-eye-slash: "\f070";
@fa-var-eyedropper: "\f1fb";
@fa-var-fa: "\f2b4";
@fa-var-facebook: "\f09a";
@fa-var-facebook-f: "\f09a";
@fa-var-facebook-official: "\f230";
@ -205,6 +262,7 @@
@fa-var-fast-backward: "\f049";
@fa-var-fast-forward: "\f050";
@fa-var-fax: "\f1ac";
@fa-var-feed: "\f09e";
@fa-var-female: "\f182";
@fa-var-fighter-jet: "\f0fb";
@fa-var-file: "\f15b";
@ -230,6 +288,8 @@
@fa-var-filter: "\f0b0";
@fa-var-fire: "\f06d";
@fa-var-fire-extinguisher: "\f134";
@fa-var-firefox: "\f269";
@fa-var-first-order: "\f2b0";
@fa-var-flag: "\f024";
@fa-var-flag-checkered: "\f11e";
@fa-var-flag-o: "\f11d";
@ -242,9 +302,13 @@
@fa-var-folder-open: "\f07c";
@fa-var-folder-open-o: "\f115";
@fa-var-font: "\f031";
@fa-var-font-awesome: "\f2b4";
@fa-var-fonticons: "\f280";
@fa-var-fort-awesome: "\f286";
@fa-var-forumbee: "\f211";
@fa-var-forward: "\f04e";
@fa-var-foursquare: "\f180";
@fa-var-free-code-camp: "\f2c5";
@fa-var-frown-o: "\f119";
@fa-var-futbol-o: "\f1e3";
@fa-var-gamepad: "\f11b";
@ -253,29 +317,50 @@
@fa-var-ge: "\f1d1";
@fa-var-gear: "\f013";
@fa-var-gears: "\f085";
@fa-var-genderless: "\f1db";
@fa-var-genderless: "\f22d";
@fa-var-get-pocket: "\f265";
@fa-var-gg: "\f260";
@fa-var-gg-circle: "\f261";
@fa-var-gift: "\f06b";
@fa-var-git: "\f1d3";
@fa-var-git-square: "\f1d2";
@fa-var-github: "\f09b";
@fa-var-github-alt: "\f113";
@fa-var-github-square: "\f092";
@fa-var-gitlab: "\f296";
@fa-var-gittip: "\f184";
@fa-var-glass: "\f000";
@fa-var-glide: "\f2a5";
@fa-var-glide-g: "\f2a6";
@fa-var-globe: "\f0ac";
@fa-var-google: "\f1a0";
@fa-var-google-plus: "\f0d5";
@fa-var-google-plus-circle: "\f2b3";
@fa-var-google-plus-official: "\f2b3";
@fa-var-google-plus-square: "\f0d4";
@fa-var-google-wallet: "\f1ee";
@fa-var-graduation-cap: "\f19d";
@fa-var-gratipay: "\f184";
@fa-var-grav: "\f2d6";
@fa-var-group: "\f0c0";
@fa-var-h-square: "\f0fd";
@fa-var-hacker-news: "\f1d4";
@fa-var-hand-grab-o: "\f255";
@fa-var-hand-lizard-o: "\f258";
@fa-var-hand-o-down: "\f0a7";
@fa-var-hand-o-left: "\f0a5";
@fa-var-hand-o-right: "\f0a4";
@fa-var-hand-o-up: "\f0a6";
@fa-var-hand-paper-o: "\f256";
@fa-var-hand-peace-o: "\f25b";
@fa-var-hand-pointer-o: "\f25a";
@fa-var-hand-rock-o: "\f255";
@fa-var-hand-scissors-o: "\f257";
@fa-var-hand-spock-o: "\f259";
@fa-var-hand-stop-o: "\f256";
@fa-var-handshake-o: "\f2b5";
@fa-var-hard-of-hearing: "\f2a4";
@fa-var-hashtag: "\f292";
@fa-var-hdd-o: "\f0a0";
@fa-var-header: "\f1dc";
@fa-var-headphones: "\f025";
@ -286,16 +371,33 @@
@fa-var-home: "\f015";
@fa-var-hospital-o: "\f0f8";
@fa-var-hotel: "\f236";
@fa-var-hourglass: "\f254";
@fa-var-hourglass-1: "\f251";
@fa-var-hourglass-2: "\f252";
@fa-var-hourglass-3: "\f253";
@fa-var-hourglass-end: "\f253";
@fa-var-hourglass-half: "\f252";
@fa-var-hourglass-o: "\f250";
@fa-var-hourglass-start: "\f251";
@fa-var-houzz: "\f27c";
@fa-var-html5: "\f13b";
@fa-var-i-cursor: "\f246";
@fa-var-id-badge: "\f2c1";
@fa-var-id-card: "\f2c2";
@fa-var-id-card-o: "\f2c3";
@fa-var-ils: "\f20b";
@fa-var-image: "\f03e";
@fa-var-imdb: "\f2d8";
@fa-var-inbox: "\f01c";
@fa-var-indent: "\f03c";
@fa-var-industry: "\f275";
@fa-var-info: "\f129";
@fa-var-info-circle: "\f05a";
@fa-var-inr: "\f156";
@fa-var-instagram: "\f16d";
@fa-var-institution: "\f19c";
@fa-var-internet-explorer: "\f26b";
@fa-var-intersex: "\f224";
@fa-var-ioxhost: "\f208";
@fa-var-italic: "\f033";
@fa-var-joomla: "\f1aa";
@ -323,6 +425,7 @@
@fa-var-link: "\f0c1";
@fa-var-linkedin: "\f0e1";
@fa-var-linkedin-square: "\f08c";
@fa-var-linode: "\f2b8";
@fa-var-linux: "\f17c";
@fa-var-list: "\f03a";
@fa-var-list-alt: "\f022";
@ -334,13 +437,18 @@
@fa-var-long-arrow-left: "\f177";
@fa-var-long-arrow-right: "\f178";
@fa-var-long-arrow-up: "\f176";
@fa-var-low-vision: "\f2a8";
@fa-var-magic: "\f0d0";
@fa-var-magnet: "\f076";
@fa-var-mail-forward: "\f064";
@fa-var-mail-reply: "\f112";
@fa-var-mail-reply-all: "\f122";
@fa-var-male: "\f183";
@fa-var-map: "\f279";
@fa-var-map-marker: "\f041";
@fa-var-map-o: "\f278";
@fa-var-map-pin: "\f276";
@fa-var-map-signs: "\f277";
@fa-var-mars: "\f222";
@fa-var-mars-double: "\f227";
@fa-var-mars-stroke: "\f229";
@ -350,25 +458,37 @@
@fa-var-meanpath: "\f20c";
@fa-var-medium: "\f23a";
@fa-var-medkit: "\f0fa";
@fa-var-meetup: "\f2e0";
@fa-var-meh-o: "\f11a";
@fa-var-mercury: "\f223";
@fa-var-microchip: "\f2db";
@fa-var-microphone: "\f130";
@fa-var-microphone-slash: "\f131";
@fa-var-minus: "\f068";
@fa-var-minus-circle: "\f056";
@fa-var-minus-square: "\f146";
@fa-var-minus-square-o: "\f147";
@fa-var-mixcloud: "\f289";
@fa-var-mobile: "\f10b";
@fa-var-mobile-phone: "\f10b";
@fa-var-modx: "\f285";
@fa-var-money: "\f0d6";
@fa-var-moon-o: "\f186";
@fa-var-mortar-board: "\f19d";
@fa-var-motorcycle: "\f21c";
@fa-var-mouse-pointer: "\f245";
@fa-var-music: "\f001";
@fa-var-navicon: "\f0c9";
@fa-var-neuter: "\f22c";
@fa-var-newspaper-o: "\f1ea";
@fa-var-object-group: "\f247";
@fa-var-object-ungroup: "\f248";
@fa-var-odnoklassniki: "\f263";
@fa-var-odnoklassniki-square: "\f264";
@fa-var-opencart: "\f23d";
@fa-var-openid: "\f19b";
@fa-var-opera: "\f26a";
@fa-var-optin-monster: "\f23c";
@fa-var-outdent: "\f03b";
@fa-var-pagelines: "\f18c";
@fa-var-paint-brush: "\f1fc";
@ -378,18 +498,22 @@
@fa-var-paragraph: "\f1dd";
@fa-var-paste: "\f0ea";
@fa-var-pause: "\f04c";
@fa-var-pause-circle: "\f28b";
@fa-var-pause-circle-o: "\f28c";
@fa-var-paw: "\f1b0";
@fa-var-paypal: "\f1ed";
@fa-var-pencil: "\f040";
@fa-var-pencil-square: "\f14b";
@fa-var-pencil-square-o: "\f044";
@fa-var-percent: "\f295";
@fa-var-phone: "\f095";
@fa-var-phone-square: "\f098";
@fa-var-photo: "\f03e";
@fa-var-picture-o: "\f03e";
@fa-var-pie-chart: "\f200";
@fa-var-pied-piper: "\f1a7";
@fa-var-pied-piper: "\f2ae";
@fa-var-pied-piper-alt: "\f1a8";
@fa-var-pied-piper-pp: "\f1a7";
@fa-var-pinterest: "\f0d2";
@fa-var-pinterest-p: "\f231";
@fa-var-pinterest-square: "\f0d3";
@ -402,28 +526,36 @@
@fa-var-plus-circle: "\f055";
@fa-var-plus-square: "\f0fe";
@fa-var-plus-square-o: "\f196";
@fa-var-podcast: "\f2ce";
@fa-var-power-off: "\f011";
@fa-var-print: "\f02f";
@fa-var-product-hunt: "\f288";
@fa-var-puzzle-piece: "\f12e";
@fa-var-qq: "\f1d6";
@fa-var-qrcode: "\f029";
@fa-var-question: "\f128";
@fa-var-question-circle: "\f059";
@fa-var-question-circle-o: "\f29c";
@fa-var-quora: "\f2c4";
@fa-var-quote-left: "\f10d";
@fa-var-quote-right: "\f10e";
@fa-var-ra: "\f1d0";
@fa-var-random: "\f074";
@fa-var-ravelry: "\f2d9";
@fa-var-rebel: "\f1d0";
@fa-var-recycle: "\f1b8";
@fa-var-reddit: "\f1a1";
@fa-var-reddit-alien: "\f281";
@fa-var-reddit-square: "\f1a2";
@fa-var-refresh: "\f021";
@fa-var-registered: "\f25d";
@fa-var-remove: "\f00d";
@fa-var-renren: "\f18b";
@fa-var-reorder: "\f0c9";
@fa-var-repeat: "\f01e";
@fa-var-reply: "\f112";
@fa-var-reply-all: "\f122";
@fa-var-resistance: "\f1d0";
@fa-var-retweet: "\f079";
@fa-var-rmb: "\f157";
@fa-var-road: "\f018";
@ -436,8 +568,11 @@
@fa-var-rub: "\f158";
@fa-var-ruble: "\f158";
@fa-var-rupee: "\f156";
@fa-var-s15: "\f2cd";
@fa-var-safari: "\f267";
@fa-var-save: "\f0c7";
@fa-var-scissors: "\f0c4";
@fa-var-scribd: "\f28a";
@fa-var-search: "\f002";
@fa-var-search-minus: "\f010";
@fa-var-search-plus: "\f00e";
@ -455,10 +590,15 @@
@fa-var-shield: "\f132";
@fa-var-ship: "\f21a";
@fa-var-shirtsinbulk: "\f214";
@fa-var-shopping-bag: "\f290";
@fa-var-shopping-basket: "\f291";
@fa-var-shopping-cart: "\f07a";
@fa-var-shower: "\f2cc";
@fa-var-sign-in: "\f090";
@fa-var-sign-language: "\f2a7";
@fa-var-sign-out: "\f08b";
@fa-var-signal: "\f012";
@fa-var-signing: "\f2a7";
@fa-var-simplybuilt: "\f215";
@fa-var-sitemap: "\f0e8";
@fa-var-skyatlas: "\f216";
@ -467,6 +607,10 @@
@fa-var-sliders: "\f1de";
@fa-var-slideshare: "\f1e7";
@fa-var-smile-o: "\f118";
@fa-var-snapchat: "\f2ab";
@fa-var-snapchat-ghost: "\f2ac";
@fa-var-snapchat-square: "\f2ad";
@fa-var-snowflake-o: "\f2dc";
@fa-var-soccer-ball-o: "\f1e3";
@fa-var-sort: "\f0dc";
@fa-var-sort-alpha-asc: "\f15d";
@ -499,7 +643,11 @@
@fa-var-step-backward: "\f048";
@fa-var-step-forward: "\f051";
@fa-var-stethoscope: "\f0f1";
@fa-var-sticky-note: "\f249";
@fa-var-sticky-note-o: "\f24a";
@fa-var-stop: "\f04d";
@fa-var-stop-circle: "\f28d";
@fa-var-stop-circle-o: "\f28e";
@fa-var-street-view: "\f21d";
@fa-var-strikethrough: "\f0cc";
@fa-var-stumbleupon: "\f1a4";
@ -508,6 +656,7 @@
@fa-var-subway: "\f239";
@fa-var-suitcase: "\f0f2";
@fa-var-sun-o: "\f185";
@fa-var-superpowers: "\f2dd";
@fa-var-superscript: "\f12b";
@fa-var-support: "\f1cd";
@fa-var-table: "\f0ce";
@ -517,6 +666,8 @@
@fa-var-tags: "\f02c";
@fa-var-tasks: "\f0ae";
@fa-var-taxi: "\f1ba";
@fa-var-telegram: "\f2c6";
@fa-var-television: "\f26c";
@fa-var-tencent-weibo: "\f1d5";
@fa-var-terminal: "\f120";
@fa-var-text-height: "\f034";
@ -524,6 +675,18 @@
@fa-var-th: "\f00a";
@fa-var-th-large: "\f009";
@fa-var-th-list: "\f00b";
@fa-var-themeisle: "\f2b2";
@fa-var-thermometer: "\f2c7";
@fa-var-thermometer-0: "\f2cb";
@fa-var-thermometer-1: "\f2ca";
@fa-var-thermometer-2: "\f2c9";
@fa-var-thermometer-3: "\f2c8";
@fa-var-thermometer-4: "\f2c7";
@fa-var-thermometer-empty: "\f2cb";
@fa-var-thermometer-full: "\f2c7";
@fa-var-thermometer-half: "\f2c9";
@fa-var-thermometer-quarter: "\f2ca";
@fa-var-thermometer-three-quarters: "\f2c8";
@fa-var-thumb-tack: "\f08d";
@fa-var-thumbs-down: "\f165";
@fa-var-thumbs-o-down: "\f088";
@ -533,6 +696,8 @@
@fa-var-times: "\f00d";
@fa-var-times-circle: "\f057";
@fa-var-times-circle-o: "\f05c";
@fa-var-times-rectangle: "\f2d3";
@fa-var-times-rectangle-o: "\f2d4";
@fa-var-tint: "\f043";
@fa-var-toggle-down: "\f150";
@fa-var-toggle-left: "\f191";
@ -540,6 +705,7 @@
@fa-var-toggle-on: "\f205";
@fa-var-toggle-right: "\f152";
@fa-var-toggle-up: "\f151";
@fa-var-trademark: "\f25c";
@fa-var-train: "\f238";
@fa-var-transgender: "\f224";
@fa-var-transgender-alt: "\f225";
@ -547,6 +713,7 @@
@fa-var-trash-o: "\f014";
@fa-var-tree: "\f1bb";
@fa-var-trello: "\f181";
@fa-var-tripadvisor: "\f262";
@fa-var-trophy: "\f091";
@fa-var-truck: "\f0d1";
@fa-var-try: "\f195";
@ -554,33 +721,45 @@
@fa-var-tumblr: "\f173";
@fa-var-tumblr-square: "\f174";
@fa-var-turkish-lira: "\f195";
@fa-var-tv: "\f26c";
@fa-var-twitch: "\f1e8";
@fa-var-twitter: "\f099";
@fa-var-twitter-square: "\f081";
@fa-var-umbrella: "\f0e9";
@fa-var-underline: "\f0cd";
@fa-var-undo: "\f0e2";
@fa-var-universal-access: "\f29a";
@fa-var-university: "\f19c";
@fa-var-unlink: "\f127";
@fa-var-unlock: "\f09c";
@fa-var-unlock-alt: "\f13e";
@fa-var-unsorted: "\f0dc";
@fa-var-upload: "\f093";
@fa-var-usb: "\f287";
@fa-var-usd: "\f155";
@fa-var-user: "\f007";
@fa-var-user-circle: "\f2bd";
@fa-var-user-circle-o: "\f2be";
@fa-var-user-md: "\f0f0";
@fa-var-user-o: "\f2c0";
@fa-var-user-plus: "\f234";
@fa-var-user-secret: "\f21b";
@fa-var-user-times: "\f235";
@fa-var-users: "\f0c0";
@fa-var-vcard: "\f2bb";
@fa-var-vcard-o: "\f2bc";
@fa-var-venus: "\f221";
@fa-var-venus-double: "\f226";
@fa-var-venus-mars: "\f228";
@fa-var-viacoin: "\f237";
@fa-var-viadeo: "\f2a9";
@fa-var-viadeo-square: "\f2aa";
@fa-var-video-camera: "\f03d";
@fa-var-vimeo: "\f27d";
@fa-var-vimeo-square: "\f194";
@fa-var-vine: "\f1ca";
@fa-var-vk: "\f189";
@fa-var-volume-control-phone: "\f2a0";
@fa-var-volume-down: "\f027";
@fa-var-volume-off: "\f026";
@fa-var-volume-up: "\f028";
@ -590,16 +769,31 @@
@fa-var-weixin: "\f1d7";
@fa-var-whatsapp: "\f232";
@fa-var-wheelchair: "\f193";
@fa-var-wheelchair-alt: "\f29b";
@fa-var-wifi: "\f1eb";
@fa-var-wikipedia-w: "\f266";
@fa-var-window-close: "\f2d3";
@fa-var-window-close-o: "\f2d4";
@fa-var-window-maximize: "\f2d0";
@fa-var-window-minimize: "\f2d1";
@fa-var-window-restore: "\f2d2";
@fa-var-windows: "\f17a";
@fa-var-won: "\f159";
@fa-var-wordpress: "\f19a";
@fa-var-wpbeginner: "\f297";
@fa-var-wpexplorer: "\f2de";
@fa-var-wpforms: "\f298";
@fa-var-wrench: "\f0ad";
@fa-var-xing: "\f168";
@fa-var-xing-square: "\f169";
@fa-var-y-combinator: "\f23b";
@fa-var-y-combinator-square: "\f1d4";
@fa-var-yahoo: "\f19e";
@fa-var-yc: "\f23b";
@fa-var-yc-square: "\f1d4";
@fa-var-yelp: "\f1e9";
@fa-var-yen: "\f157";
@fa-var-yoast: "\f2b1";
@fa-var-youtube: "\f167";
@fa-var-youtube-play: "\f16a";
@fa-var-youtube-square: "\f166";

@ -268,6 +268,11 @@
.fa-icon-color(@brand-warning);
}
.icon-radarr-download-warning {
.fa-icon-content(@fa-var-download);
.fa-icon-color(@brand-warning);
}
.icon-sonarr-shutdown {
.fa-icon-content(@fa-var-power-off);
.fa-icon-color(@brand-danger);

@ -64,6 +64,11 @@ Handlebars.registerHelper('alternativeTitlesString', function() {
if (titles.length === 0) {
return "";
}
titles = _.map(titles, function(item){
return item.title;
});
if (titles.length === 1) {
return titles[0];
}

@ -12,6 +12,7 @@ var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEdito
var HistoryLayout = require('../History/MovieHistoryLayout');
var SearchLayout = require('../Search/MovieSearchLayout');
var FilesLayout = require("../Files/FilesLayout");
var TitlesLayout = require("../Titles/TitlesLayout");
require('backstrech');
require('../../Mixins/backbone.signalr.mixin');
@ -24,7 +25,8 @@ module.exports = Marionette.Layout.extend({
info : '#info',
search : '#movie-search',
history : '#movie-history',
files : "#movie-files"
files : "#movie-files",
titles: "#movie-titles",
},
@ -39,7 +41,8 @@ module.exports = Marionette.Layout.extend({
manualSearch : '.x-manual-search',
history : '.x-movie-history',
search : '.x-movie-search',
files : ".x-movie-files"
files : ".x-movie-files",
titles: ".x-movie-titles",
},
events : {
@ -53,6 +56,7 @@ module.exports = Marionette.Layout.extend({
'click .x-movie-history' : '_showHistory',
'click .x-movie-search' : '_showSearch',
"click .x-movie-files" : "_showFiles",
"click .x-movie-titles" : "_showTitles",
},
initialize : function() {
@ -83,6 +87,7 @@ module.exports = Marionette.Layout.extend({
this.searchLayout.startManualSearch = true;
this.filesLayout = new FilesLayout({ model : this.model });
this.titlesLayout = new TitlesLayout({ model : this.model });
this._showBackdrop();
this._showSeasons();
@ -170,6 +175,15 @@ module.exports = Marionette.Layout.extend({
this.files.show(this.filesLayout);
},
_showTitles : function(e) {
if (e) {
e.preventDefault();
}
this.ui.titles.tab("show");
this.titles.show(this.titlesLayout);
},
_toggleMonitored : function() {
var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true });

@ -6,7 +6,8 @@
<div>
<h1 class="header-text">
<i class="x-monitored" title="Toggle monitored state for movie"/>
{{title}}
{{title}} <span class="year">({{year}}{{#if secondaryYear}} / <a href="https://mappings.radarr.video/mapping/{{secondaryYearSourceId}}" target="_blank"><span title="Secondary year pulled from Radarr Mappings.
Click to head on over there and tell us whether this is correct or not.">{{secondaryYear}}</span></a>{{/if}})</span>
<div class="movie-actions pull-right">
<div class="x-episode-file-editor">
<i class="icon-sonarr-episode-file" title="Modify movie files"/>
@ -43,11 +44,13 @@
<li><a href="#movie-history" class="x-movie-history">History</a></li>
<li><a href="#movie-search" class="x-movie-search">Search</a></li>
<li><a href="#movie-files" class="x-movie-files">Files</a></li>
<li><a href="#movie-titles" class="x-movie-titles">Titles</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="movie-history"/>
<div class="tab-pane" id="movie-search"/>
<div class="tab-pane" id="movie-files"/>
<div class="tab-pane" id="movie-titles"/>
</div>
</div>
</div>

@ -23,7 +23,7 @@ var view = Marionette.ItemView.extend({
initialize : function() {
this.model.set('profiles', Profiles);
var pathState = this.model.get("pathState");
if (pathState == "static") {
if (pathState === "static") {
this.model.set("pathState", true);
} else {
this.model.set("pathState", false);

@ -0,0 +1,22 @@
var NzbDroneCell = require('../../Cells/NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'language-cell',
render : function() {
this.$el.empty();
var language = this.model.get("language");
this.$el.html(this.toTitleCase(language));
return this;
},
toTitleCase : function(str)
{
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
});

@ -0,0 +1,5 @@
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'Movies/Titles/NoTitlesViewTemplate'
});

@ -0,0 +1,3 @@
<p class="text-warning">
No alternative titles for this movie.
</p>

@ -0,0 +1,42 @@
var NzbDroneCell = require('../../Cells/NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'title-source-cell',
render : function() {
this.$el.empty();
var link;
var sourceTitle = this.model.get("sourceType");
var sourceId = this.model.get("sourceId");
switch (sourceTitle) {
case "tmdb":
sourceTitle = "TMDB";
link = "https://themoviedb.org/movie/" + sourceId;
break;
case "mappings":
sourceTitle = "Radarr Mappings";
link = "https://mappings.radarr.video/mapping/" + sourceId;
break;
case "user":
sourceTitle = "Force Download";
break;
case "indexer":
sourceTitle = "Indexer";
break;
}
var a = "{0}";
if (link) {
a = "<a href='"+link+"' target='_blank'>{0}</a>";
}
this.$el.html(a.format(sourceTitle));
return this;
}
});

@ -0,0 +1,6 @@
var TemplatedCell = require('../../Cells/TemplatedCell');
module.exports = TemplatedCell.extend({
className : 'series-title-cell',
template : 'Movies/Titles/TitleTemplate'
});

@ -0,0 +1,3 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({});

@ -0,0 +1,30 @@
var PagableCollection = require('backbone.pageable');
var TitleModel = require('./TitleModel');
var AsSortedCollection = require('../../Mixins/AsSortedCollection');
var Collection = PagableCollection.extend({
url : window.NzbDrone.ApiRoot + "/aka",
model : TitleModel,
state : {
pageSize : 2000,
sortKey : 'title',
order : -1
},
mode : 'client',
sortMappings : {
"source" : {
sortKey : "sourceType"
},
"language" : {
sortKey : "language"
}
},
});
Collection = AsSortedCollection.call(Collection);
module.exports = Collection;

@ -0,0 +1,117 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
//var ButtonsView = require('./ButtonsView');
//var ManualSearchLayout = require('./ManualLayout');
var TitlesCollection = require('./TitlesCollection');
var CommandController = require('../../Commands/CommandController');
var LoadingView = require('../../Shared/LoadingView');
var NoResultsView = require('./NoTitlesView');
var TitleModel = require("./TitleModel");
var TitleCell = require("./TitleCell");
var SourceCell = require("./SourceCell");
var LanguageCell = require("./LanguageCell");
module.exports = Marionette.Layout.extend({
template : 'Movies/Titles/TitlesLayoutTemplate',
regions : {
main : '#movie-titles-region',
grid : "#movie-titles-grid"
},
events : {
'click .x-search-auto' : '_searchAuto',
'click .x-search-manual' : '_searchManual',
'click .x-search-back' : '_showButtons'
},
columns : [
{
name : 'title',
label : 'Title',
cell : Backgrid.StringCell
},
{
name : "this",
label : "Source",
cell : SourceCell,
sortKey : "sourceType",
},
{
name : "this",
label : "Language",
cell : LanguageCell
}
],
initialize : function(movie) {
this.titlesCollection = new TitlesCollection();
var titles = movie.model.get("alternativeTitles");
this.movie = movie;
this.titlesCollection.add(titles);
//this.listenTo(this.releaseCollection, 'sync', this._showSearchResults);
this.listenTo(this.model, 'change', function(model, options) {
if (options && options.changeSource === 'signalr') {
this._refresh(model);
}
});
//vent.on(vent.Commands.MovieFileEdited, this._showGrid, this);
},
_refresh : function(model) {
this.titlesCollection = new TitlesCollection();
var file = model.get("alternativeTitles");
this.titlesCollection.add(file);
this.onShow();
},
_refreshClose : function(options) {
this.titlesCollection = new TitlesCollection();
var file = this.movie.model.get("alternativeTitles");
this.titlesCollection.add(file);
this._showGrid();
},
onShow : function() {
this.grid.show(new Backgrid.Grid({
row : Backgrid.Row,
columns : this.columns,
collection : this.titlesCollection,
className : 'table table-hover'
}));
},
_showGrid : function() {
this.regionManager.get('grid').show(new Backgrid.Grid({
row : Backgrid.Row,
columns : this.columns,
collection : this.titlesCollection,
className : 'table table-hover'
}));
},
_showMainView : function() {
this.main.show(this.mainView);
},
_showButtons : function() {
this._showMainView();
},
_showSearchResults : function() {
if (this.releaseCollection.length === 0) {
this.mainView = new NoResultsView();
}
else {
//this.mainView = new ManualSearchLayout({ collection : this.releaseCollection });
}
this._showMainView();
}
});

@ -0,0 +1,3 @@
<div id="movie-titles-region">
<div id="movie-titles-grid" class="table-responsive"></div>
</div>

@ -534,3 +534,9 @@
list-style-type : none;
}
}
.header-text {
.year {
color : gray;
}
}

@ -0,0 +1,6 @@
var Backbone = require('backbone');
var _ = require('underscore');
module.exports = Backbone.Model.extend({
urlRoot : window.NzbDrone.ApiRoot + '/alttitle',
});

@ -0,0 +1,6 @@
var Backbone = require('backbone');
var _ = require('underscore');
module.exports = Backbone.Model.extend({
urlRoot : window.NzbDrone.ApiRoot + '/altyear',
});

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

Loading…
Cancel
Save