1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-11 12:02:35 +02:00

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

This commit is contained in:
Leonardo Galli 2017-08-15 23:22:43 +02:00
commit b1e75ffc57
113 changed files with 9077 additions and 890 deletions

View File

@ -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) [![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 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 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) [![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)

View File

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

4190
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,5 @@
using System.Text; using System.Linq;
using System.Text;
using Marr.Data.Mapping; using Marr.Data.Mapping;
using Marr.Data.QGen.Dialects; using Marr.Data.QGen.Dialects;
@ -129,7 +130,16 @@ public void BuildWhereClause(StringBuilder sql)
public void BuildOrderClause(StringBuilder sql) public void BuildOrderClause(StringBuilder sql)
{ {
sql.Append(OrderBy.ToString()); 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) private string TranslateJoin(JoinType join)
{ {

View File

@ -14,8 +14,21 @@ public SqliteRowCountQueryDecorator(SelectQuery innerQuery)
public string Generate() public string Generate()
{ {
StringBuilder sql = new StringBuilder(); StringBuilder sql = new StringBuilder();
BuildSelectCountClause(sql); 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.BuildFromClause(sql);
_innerQuery.BuildJoinClauses(sql); _innerQuery.BuildJoinClauses(sql);
_innerQuery.BuildWhereClause(sql); _innerQuery.BuildWhereClause(sql);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Nancy; using Nancy;
@ -17,7 +17,7 @@ public class IndexHtmlMapper : StaticResourceMapperBase
private readonly IAnalyticsService _analyticsService; private readonly IAnalyticsService _analyticsService;
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory; private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory;
private readonly string _indexPath; 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 API_KEY;
private static string URL_BASE; private static string URL_BASE;

View File

@ -46,7 +46,7 @@ public ReleaseModule(IFetchAndParseRss rssFetcherAndParser,
GetResourceAll = GetReleases; GetResourceAll = GetReleases;
Post["/"] = x => DownloadRelease(this.Bind<ReleaseResource>()); 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(); PostValidator.RuleFor(s => s.Guid).NotEmpty();
_remoteEpisodeCache = cacheManager.GetCache<RemoteEpisode>(GetType(), "remoteEpisodes"); _remoteEpisodeCache = cacheManager.GetCache<RemoteEpisode>(GetType(), "remoteEpisodes");
@ -70,7 +70,7 @@ private Response DownloadRelease(ReleaseResource release)
try try
{ {
_downloadService.DownloadReport(remoteMovie); _downloadService.DownloadReport(remoteMovie, false);
} }
catch (ReleaseDownloadException ex) catch (ReleaseDownloadException ex)
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -105,7 +105,7 @@ private JsonResponse<QueueResource> Grab()
throw new NotFoundException(); throw new NotFoundException();
} }
_downloadService.DownloadReport(pendingRelease.RemoteMovie); _downloadService.DownloadReport(pendingRelease.RemoteMovie, false);
return resource.AsResponse(); return resource.AsResponse();
} }

View File

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

View File

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

View File

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

View File

@ -199,7 +199,7 @@ private void PopulateAlternateTitles(SeriesResource resource)
if (mappings == null) return; 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) public void Handle(EpisodeImportedEvent message)

View File

@ -20,7 +20,7 @@ public SeriesResource()
//View Only //View Only
public string Title { get; set; } public string Title { get; set; }
public List<AlternateTitleResource> AlternateTitles { get; set; } //public List<AlternativeTitleResource> AlternateTitles { get; set; }
public string SortTitle { get; set; } public string SortTitle { get; set; }
public int SeasonCount public int SeasonCount

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
@ -12,7 +12,7 @@ public class TaskModule : NzbDroneRestModuleWithSignalR<TaskResource, ScheduledT
{ {
private readonly ITaskManager _taskManager; 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) public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
: base(broadcastSignalRMessage, "system/task") : base(broadcastSignalRMessage, "system/task")

View File

@ -263,7 +263,7 @@ public void should_not_allow_download_if_series_is_unknown()
result.Should().HaveCount(1); result.Should().HaveCount(1);
result.First().RemoteMovie.DownloadAllowed.Should().BeFalse(); //result.First().RemoteMovie.DownloadAllowed.Should().BeFalse();
} }
[Test] [Test]
@ -278,7 +278,7 @@ public void should_not_allow_download_if_no_episodes_found()
result.Should().HaveCount(1); result.Should().HaveCount(1);
result.First().RemoteMovie.DownloadAllowed.Should().BeFalse(); //result.First().RemoteMovie.DownloadAllowed.Should().BeFalse();
} }
[Test] [Test]

View File

@ -76,7 +76,7 @@ public void should_download_report_if_movie_was_not_already_downloaded()
decisions.Add(new DownloadDecision(remoteMovie)); decisions.Add(new DownloadDecision(remoteMovie));
Subject.ProcessDecisions(decisions); 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] [Test]
@ -89,7 +89,7 @@ public void should_only_download_movie_once()
decisions.Add(new DownloadDecision(remoteMovie)); decisions.Add(new DownloadDecision(remoteMovie));
Subject.ProcessDecisions(decisions); 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] [Test]
@ -157,7 +157,7 @@ public void should_not_add_to_downloaded_list_when_download_fails()
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteMovie)); 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(); Subject.ProcessDecisions(decisions).Grabbed.Should().BeEmpty();
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
@ -183,7 +183,7 @@ public void should_not_grab_if_pending()
decisions.Add(new DownloadDecision(remoteMovie)); decisions.Add(new DownloadDecision(remoteMovie));
Subject.ProcessDecisions(decisions); 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] [Test]

View File

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Test.HealthCheck
[TestFixture] [TestFixture]
public class HealthCheckFixture : CoreTest 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", 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")] [TestCase("I blew up because of some weird user mistake", "#my-health-check", WikiRoot + "Health-checks#my-health-check")]

View File

@ -54,6 +54,7 @@ public class LanguageParserFixture : CoreTest
[TestCase("Der.Soldat.James.German.Bluray.FuckYou.Pso.Why.cant.you.follow.scene.rules.1998", Language.German)] [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("Passengers.German.DL.AC3.Dubbed..BluRay.x264-PsO", Language.German)]
[TestCase("Valana la Legende FRENCH BluRay 720p 2016 kjhlj", Language.French)] [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) public void should_parse_language(string postTitle, Language language)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);

View File

@ -6,7 +6,9 @@
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -43,7 +45,7 @@ public void Setup()
.With(m => m.Title = "Fack Ju Göthe 2") .With(m => m.Title = "Fack Ju Göthe 2")
.With(m => m.CleanTitle = "fackjugoethe2") .With(m => m.CleanTitle = "fackjugoethe2")
.With(m => m.Year = 2015) .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(); .Build();
_episodes = Builder<Episode>.CreateListOfSize(1) _episodes = Builder<Episode>.CreateListOfSize(1)
@ -80,7 +82,7 @@ public void Setup()
_alternativeTitleInfo = new ParsedMovieInfo _alternativeTitleInfo = new ParsedMovieInfo
{ {
MovieTitle = _movie.AlternativeTitles.First(), MovieTitle = _movie.AlternativeTitles.First().Title,
Year = _movie.Year, Year = _movie.Year,
}; };

View File

@ -307,6 +307,7 @@ public void should_parse_quality_from_extension(string title)
[TestCase("Movie.Title.2016.1080p.KORSUB.WEBRip.x264.AAC2.0-RADARR", "korsub")] [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("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) public void should_parse_hardcoded_subs(string postTitle, string sub)
{ {
QualityParser.ParseQuality(postTitle).HardcodedSubs.Should().Be(sub); QualityParser.ParseQuality(postTitle).HardcodedSubs.Should().Be(sub);

View File

@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
@ -39,15 +39,15 @@ public void Init_should_skip_if_any_profiles_already_exist()
[Test] [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) .Random(1)
.With(c => c.ProfileId = 2) .With(c => c.ProfileId = 2)
.Build().ToList(); .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)); Assert.Throws<ProfileInUseException>(() => Subject.Delete(2));
@ -57,15 +57,15 @@ public void should_not_be_able_to_delete_profile_if_assigned_to_series()
[Test] [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() .All()
.With(c => c.ProfileId = 2) .With(c => c.ProfileId = 2)
.Build().ToList(); .Build().ToList();
Mocker.GetMock<ISeriesService>().Setup(c => c.GetAllSeries()).Returns(seriesList); Mocker.GetMock<IMovieService>().Setup(c => c.GetAllMovies()).Returns(movieList);
Subject.Delete(1); Subject.Delete(1);

View File

@ -3,8 +3,10 @@
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Web.Hosting;
using Marr.Data; using Marr.Data;
using Marr.Data.QGen; using Marr.Data.QGen;
using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
@ -48,7 +50,7 @@ public BasicRepository(IDatabase database, IEventAggregator eventAggregator)
_eventAggregator = eventAggregator; _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) protected void Delete(Expression<Func<TModel, bool>> filter)
{ {
@ -246,18 +248,23 @@ public void SetFields(TModel model, params Expression<Func<TModel, object>>[] pr
public virtual PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec) 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(); pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount();
var queryStr = GetPagedQuery(Query, pagingSpec).BuildQuery();
var beforeQuery = Query.BuildQuery();
pagingSpec.SortKey = beforeQuery;
pagingSpec.SortKey = queryStr;
return pagingSpec; return pagingSpec;
} }
protected virtual SortBuilder<TModel> GetPagedQuery(QueryBuilder<TModel> query, PagingSpec<TModel> pagingSpec) protected virtual SortBuilder<TModel> GetPagedQuery(QueryBuilder<TModel> query, PagingSpec<TModel> pagingSpec)
{ {
return query.Where(pagingSpec.FilterExpression) return query.Where(pagingSpec.FilterExpression)
.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection());
.Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize);
} }
protected void ModelCreated(TModel model) protected void ModelCreated(TModel model)
@ -283,6 +290,11 @@ private void PublishModelEvent(TModel model, ModelAction action)
} }
} }
protected virtual QueryBuilder<TModel> AddJoinQueries(QueryBuilder<TModel> baseQuery)
{
return baseQuery;
}
protected virtual bool PublishModelEvents => false; protected virtual bool PublishModelEvents => false;
} }
} }

View File

@ -110,10 +110,10 @@ public IDatabase Create(MigrationContext migrationContext)
{ {
if (OsInfo.IsOsx) 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);
} }
} }

View File

@ -28,12 +28,12 @@ public static RelationshipBuilder<TParent> Relationship<TParent>(this ColumnMapB
return mapBuilder.Relationships.AutoMapComplexTypeProperties<ILazyLoaded>(); 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 TParent : ModelBase
where TChild : ModelBase where TChild : ModelBase
{ {
return relationshipBuilder.For(portalExpression.GetMemberName()) 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) private static string GetMemberName<T, TMember>(this Expression<Func<T, TMember>> member)

View File

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

View File

@ -36,6 +36,45 @@
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.NetImport; using NzbDrone.Core.NetImport;
using NzbDrone.Core.NetImport.ImportExclusions; 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 namespace NzbDrone.Core.Datastore
{ {
@ -101,12 +140,19 @@ public static void Map()
query: (db, parent) => db.Query<Movie>().Where(c => c.MovieFileId == parent.Id).ToList()) query: (db, parent) => db.Query<Movie>().Where(c => c.MovieFileId == parent.Id).ToList())
.HasOne(file => file.Movie, file => file.MovieId); .HasOne(file => file.Movie, file => file.MovieId);
Mapper.Entity<Movie>().RegisterModel("Movies") Mapper.Entity<Movie>().RegisterModel("Movies")
.Ignore(s => s.RootFolderPath) .Ignore(s => s.RootFolderPath)
.Relationship() .Relationship()
.HasOne(s => s.Profile, s => s.ProfileId) .HasOne(s => s.Profile, s => s.ProfileId)
.HasOne(m => m.MovieFile, m => m.MovieFileId); .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"); Mapper.Entity<ImportExclusion>().RegisterModel("ImportExclusions");

View File

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

View File

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

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@ -111,7 +111,7 @@ public override IEnumerable<DownloadClientItem> GetItems()
{ {
case "error": // some error occurred, applies to paused torrents case "error": // some error occurred, applies to paused torrents
item.Status = DownloadItemStatus.Failed; item.Status = DownloadItemStatus.Failed;
item.Message = "QBittorrent is reporting an error"; item.Message = "qBittorrent is reporting an error";
break; break;
case "pausedDL": // torrent is paused and has NOT finished downloading case "pausedDL": // torrent is paused and has NOT finished downloading
@ -212,7 +212,7 @@ private ValidationFailure TestConnection()
var config = _proxy.GetConfig(Settings); var config = _proxy.GetConfig(Settings);
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio) 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'." 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'."
}; };

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using NLog; using NLog;
@ -72,7 +72,13 @@ public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
.Post() .Post()
.AddFormParameter("urls", torrentUrl); .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) public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
@ -81,7 +87,13 @@ public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentS
.Post() .Post()
.AddFormUpload("torrents", fileName, fileContent); .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) public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
@ -90,7 +102,7 @@ public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings s
.Post() .Post()
.AddFormParameter("hashes", hash); .AddFormParameter("hashes", hash);
ProcessRequest<object>(request, settings); ProcessRequest(request, settings);
} }
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings) public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
@ -101,7 +113,7 @@ public void SetTorrentLabel(string hash, string label, QBittorrentSettings setti
.AddFormParameter("category", label); .AddFormParameter("category", label);
try try
{ {
ProcessRequest<object>(setCategoryRequest, settings); ProcessRequest(setCategoryRequest, settings);
} }
catch(DownloadClientException ex) catch(DownloadClientException ex)
{ {
@ -112,7 +124,8 @@ public void SetTorrentLabel(string hash, string label, QBittorrentSettings setti
.Post() .Post()
.AddFormParameter("hashes", hash) .AddFormParameter("hashes", hash)
.AddFormParameter("label", label); .AddFormParameter("label", label);
ProcessRequest<object>(setLabelRequest, settings);
ProcessRequest(setLabelRequest, settings);
} }
} }
} }
@ -125,7 +138,7 @@ public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
try try
{ {
var response = ProcessRequest<object>(request, settings); ProcessRequest(request, settings);
} }
catch (DownloadClientException ex) catch (DownloadClientException ex)
{ {
@ -152,10 +165,18 @@ private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
where TResult : new() where TResult : new()
{
var responseContent = ProcessRequest(requestBuilder, settings);
return Json.Deserialize<TResult>(responseContent);
}
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
{ {
AuthenticateClient(requestBuilder, settings); AuthenticateClient(requestBuilder, settings);
var request = requestBuilder.Build(); var request = requestBuilder.Build();
request.LogResponseContent = true;
HttpResponse response; HttpResponse response;
try try
@ -176,15 +197,15 @@ private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBitt
} }
else 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) 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) private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
@ -218,23 +239,23 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSe
_logger.Debug("qbitTorrent authentication failed."); _logger.Debug("qbitTorrent authentication failed.");
if (ex.Response.StatusCode == HttpStatusCode.Forbidden) 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) 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 if (response.Content != "Ok.") // returns "Fails." on bad login
{ {
_logger.Debug("qbitTorrent authentication failed."); _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(); cookies = response.GetCookies();

View File

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

View File

@ -88,7 +88,7 @@ public ProcessedDecisions ProcessDecisions(List<DownloadDecision> decisions)
try try
{ {
_downloadService.DownloadReport(remoteMovie); _downloadService.DownloadReport(remoteMovie, false);
grabbed.Add(report); grabbed.Add(report);
} }
catch (Exception e) catch (Exception e)

View File

@ -39,7 +39,7 @@ private static string MakeWikiFragment(string message)
private static HttpUri MakeWikiUrl(string fragment) 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);
} }
} }

View File

@ -59,7 +59,7 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC
else else
{ {
var searchTitle = System.Web.HttpUtility.UrlPathEncode(Parser.Parser.ReplaceGermanUmlauts(Parser.Parser.NormalizeTitle(searchCriteria.Movie.Title))); 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); var realMaxPages = (int)MaxPages / (altTitles.Count() + 1);

View File

@ -3,7 +3,10 @@
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.MetadataSource.RadarrAPI namespace NzbDrone.Core.MetadataSource.RadarrAPI
{ {
@ -11,6 +14,10 @@ public interface IRadarrAPIClient
{ {
IHttpRequestBuilderFactory RadarrAPI { get; } IHttpRequestBuilderFactory RadarrAPI { get; }
List<MovieResult> DiscoverMovies(string action, Func<HttpRequest, HttpRequest> enhanceRequest); 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; } string APIURL { get; }
} }
@ -65,7 +72,7 @@ private T Execute<T>(HttpRequest request)
{ {
var error = JsonConvert.DeserializeObject<RadarrError>(response.Content); 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); throw new RadarrAPIException(error);
} }
@ -96,6 +103,83 @@ public List<MovieResult> DiscoverMovies(string action, Func<HttpRequest, HttpReq
return Execute<List<MovieResult>>(request); 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; } public IHttpRequestBuilderFactory RadarrAPI { get; private set; }
} }
} }

View File

@ -27,21 +27,183 @@ public class RadarrError
public class RadarrAPIException : Exception public class RadarrAPIException : Exception
{ {
RadarrError APIErrors; public RadarrError APIErrors;
public RadarrAPIException(RadarrError apiError) : base(HumanReadable(apiError)) 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 firstError = apiErrors.Errors.First();
var details = string.Join("\n", APIErrors.Errors.Select(error => var details = string.Join("\n", apiErrors.Errors.Select(error =>
{ {
return $"{error.Title} ({error.Status}, RayId: {error.RayId}), Details: {error.Detail}"; return $"{error.Title} ({error.Status}, RayId: {error.RayId}), Details: {error.Detail}";
})); }));
return $"Error while calling api: {firstError.Title}\nFull error(s): {details}"; 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; }
}
} }

View File

@ -2,7 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using NLog; using System.ServiceModel;
using NLog;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
@ -19,6 +20,7 @@
using NzbDrone.Core.NetImport.ImportExclusions; using NzbDrone.Core.NetImport.ImportExclusions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource.RadarrAPI; using NzbDrone.Core.MetadataSource.RadarrAPI;
using NzbDrone.Core.Movies.AlternativeTitles;
namespace NzbDrone.Core.MetadataSource.SkyHook namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
@ -33,12 +35,13 @@ public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries, IProvideMov
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IPreDBService _predbService; private readonly IPreDBService _predbService;
private readonly IImportExclusionsService _exclusionService; private readonly IImportExclusionsService _exclusionService;
private readonly IAlternativeTitleService _altTitleService;
private readonly IRadarrAPIClient _radarrAPI; private readonly IRadarrAPIClient _radarrAPI;
private readonly IHttpRequestBuilderFactory _apiBuilder; private readonly IHttpRequestBuilderFactory _apiBuilder;
public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, IMovieService movieService, 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; _httpClient = httpClient;
_requestBuilder = requestBuilder.SkyHookTvdb; _requestBuilder = requestBuilder.SkyHookTvdb;
@ -47,6 +50,7 @@ public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBu
_movieService = movieService; _movieService = movieService;
_predbService = predbService; _predbService = predbService;
_exclusionService = exclusionService; _exclusionService = exclusionService;
_altTitleService = altTitleService;
_radarrAPI = radarrAPI; _radarrAPI = radarrAPI;
_logger = logger; _logger = logger;
@ -133,21 +137,28 @@ public Movie GetMovieInfo(int TmdbId, Profile profile = null, bool hasPreDBEntry
} }
var movie = new Movie(); 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) foreach (var alternativeTitle in resource.alternative_titles.titles)
{ {
if (alternativeTitle.iso_3166_1.ToLower() == langCode) 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") 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 @@ public Movie GetMovieInfo(int TmdbId, Profile profile = null, bool hasPreDBEntry
} }
} }
movie.AlternativeTitles.AddRange(altTitles);
return movie; return movie;
} }

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@ public class SlackPayload
[JsonProperty("icon_emoji")] [JsonProperty("icon_emoji")]
public string IconEmoji { get; set; } public string IconEmoji { get; set; }
[JsonProperty("icon_url")]
public string IconUrl { get; set; }
public List<Attachment> Attachments { get; set; } public List<Attachment> Attachments { get; set; }
} }
} }

View File

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

View File

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

View File

@ -24,7 +24,7 @@ public class SlackSettings : IProviderConfig
[FieldDefinition(1, Label = "Username", HelpText = "Choose the username that this integration will post as", Type = FieldType.Textbox)] [FieldDefinition(1, Label = "Username", HelpText = "Choose the username that this integration will post as", Type = FieldType.Textbox)]
public string Username { get; set; } 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 string Icon { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()

View File

@ -37,10 +37,10 @@ public TwitterSettings()
AuthorizeNotification = "step1"; 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; } 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; } public string ConsumerSecret { get; set; }
[FieldDefinition(2, Label = "Access Token", Advanced = true)] [FieldDefinition(2, Label = "Access Token", Advanced = true)]

View File

@ -15,7 +15,7 @@ public Webhook(IWebhookService service)
_service = service; _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) public override void OnGrab(GrabMessage message)
{ {

View File

@ -125,6 +125,7 @@
<Compile Include="Authentication\UserRepository.cs" /> <Compile Include="Authentication\UserRepository.cs" />
<Compile Include="Authentication\UserService.cs" /> <Compile Include="Authentication\UserService.cs" />
<Compile Include="Datastore\Migration\123_create_netimport_table.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="MediaFiles\Events\MovieFileUpdatedEvent.cs" />
<Compile Include="Datastore\Migration\134_add_remux_qualities_for_the_wankers.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" /> <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="Datastore\Migration\133_add_minimumavailability.cs" />
<Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" /> <Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" />
<Compile Include="Indexers\HDBits\HDBitsInfo.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\NetImportListLevels.cs" />
<Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" /> <Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" />
<Compile Include="NetImport\TMDb\TMDbSettings.cs" /> <Compile Include="NetImport\TMDb\TMDbSettings.cs" />
@ -966,6 +970,7 @@
<Compile Include="Notifications\Slack\Payloads\SlackPayload.cs" /> <Compile Include="Notifications\Slack\Payloads\SlackPayload.cs" />
<Compile Include="Notifications\Slack\Slack.cs" /> <Compile Include="Notifications\Slack\Slack.cs" />
<Compile Include="Notifications\Slack\SlackExeption.cs" /> <Compile Include="Notifications\Slack\SlackExeption.cs" />
<Compile Include="Notifications\Slack\SlackProxy.cs" />
<Compile Include="Notifications\Slack\SlackSettings.cs" /> <Compile Include="Notifications\Slack\SlackSettings.cs" />
<Compile Include="Notifications\Synology\SynologyException.cs" /> <Compile Include="Notifications\Synology\SynologyException.cs" />
<Compile Include="Notifications\Synology\SynologyIndexer.cs" /> <Compile Include="Notifications\Synology\SynologyIndexer.cs" />

View File

@ -28,7 +28,8 @@ public static class IsoLanguages
// new IsoLanguage("nl", "nld", Language.Flemish), // new IsoLanguage("nl", "nld", Language.Flemish),
new IsoLanguage("el", "ell", Language.Greek), new IsoLanguage("el", "ell", Language.Greek),
new IsoLanguage("ko", "kor", Language.Korean), 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) public static IsoLanguage Find(string isoCode)

View File

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

View File

@ -11,7 +11,7 @@ public static class LanguageParser
{ {
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser)); 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); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -77,6 +77,9 @@ public static Language ParseLanguage(string title)
if (lowerTitle.Contains("hungarian")) if (lowerTitle.Contains("hungarian"))
return Language.Hungarian; return Language.Hungarian;
if (lowerTitle.Contains("hebrew"))
return Language.Hebrew;
var match = LanguageRegex.Match(title); var match = LanguageRegex.Match(title);
if (match.Groups["italian"].Captures.Cast<Capture>().Any()) if (match.Groups["italian"].Captures.Cast<Capture>().Any())
@ -103,6 +106,9 @@ public static Language ParseLanguage(string title)
if (match.Groups["hungarian"].Success) if (match.Groups["hungarian"].Success)
return Language.Hungarian; return Language.Hungarian;
if (match.Groups["hebrew"].Success)
return Language.Hebrew;
return Language.English; return Language.English;
} }

View File

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

View File

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

View File

@ -22,13 +22,13 @@ public interface IProfileService
public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent> public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent>
{ {
private readonly IProfileRepository _profileRepository; private readonly IProfileRepository _profileRepository;
private readonly ISeriesService _seriesService; private readonly IMovieService _movieService;
private readonly Logger _logger; private readonly Logger _logger;
public ProfileService(IProfileRepository profileRepository, ISeriesService seriesService, Logger logger) public ProfileService(IProfileRepository profileRepository, IMovieService movieService, Logger logger)
{ {
_profileRepository = profileRepository; _profileRepository = profileRepository;
_seriesService = seriesService; _movieService = movieService;
_logger = logger; _logger = logger;
} }
@ -44,7 +44,7 @@ public void Update(Profile profile)
public void Delete(int id) 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); throw new ProfileInUseException(id);
} }

View File

@ -6,6 +6,8 @@
using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using System.IO; using System.IO;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
@ -17,7 +19,7 @@ public Movie()
Genres = new List<string>(); Genres = new List<string>();
Actors = new List<Actor>(); Actors = new List<Actor>();
Tags = new HashSet<int>(); Tags = new HashSet<int>();
AlternativeTitles = new List<string>(); AlternativeTitles = new List<AlternativeTitle>();
} }
public int TmdbId { get; set; } public int TmdbId { get; set; }
public string ImdbId { get; set; } public string ImdbId { get; set; }
@ -52,7 +54,10 @@ public Movie()
public LazyLoaded<MovieFile> MovieFile { get; set; } public LazyLoaded<MovieFile> MovieFile { get; set; }
public bool HasPreDBEntry { get; set; } public bool HasPreDBEntry { get; set; }
public int MovieFileId { 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 YouTubeTrailerId{ get; set; }
public string Studio { get; set; } public string Studio { get; set; }

View File

@ -6,6 +6,7 @@
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
using Marr.Data.QGen; using Marr.Data.QGen;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Parser.RomanNumerals; using NzbDrone.Core.Parser.RomanNumerals;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using CoreParser = NzbDrone.Core.Parser.Parser; using CoreParser = NzbDrone.Core.Parser.Parser;
@ -103,7 +104,7 @@ public PagingSpec<Movie> MoviesWithoutFiles(PagingSpec<Movie> pagingSpec)
public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec) public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec)
{ {
if (pagingSpec.SortKey == "downloadedQuality") /*if (pagingSpec.SortKey == "downloadedQuality")
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
var offset = pagingSpec.PagingOffset(); var offset = pagingSpec.PagingOffset();
@ -113,7 +114,7 @@ public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec)
{ {
direction = "DESC"; 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 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(); //var ok = q.BuildQuery();
@ -122,9 +123,11 @@ public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec)
pagingSpec.TotalRecords = q2.Count(); pagingSpec.TotalRecords = q2.Count();
} }
else else*/
{ {
pagingSpec = base.GetPaged(pagingSpec); pagingSpec = base.GetPaged(pagingSpec);
//pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList();
//pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount();
} }
if (pagingSpec.Records.Count == 0 && pagingSpec.Page != 1) if (pagingSpec.Records.Count == 0 && pagingSpec.Page != 1)
@ -136,6 +139,22 @@ public override PagingSpec<Movie> GetPaged(PagingSpec<Movie> pagingSpec)
return pagingSpec; 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) public SortBuilder<Movie> GetMoviesWithoutFilesQuery(PagingSpec<Movie> pagingSpec)
{ {
@ -247,22 +266,39 @@ private Movie FindByTitle(string cleanTitle, int? year)
if (result == null) if (result == null)
{ {
IEnumerable<Movie> movies = All(); /*IEnumerable<Movie> movies = All();
Func<string, string> titleCleaner = title => CoreParser.CleanSeriesTitle(title.ToLower()); Func<string, string> titleCleaner = title => CoreParser.CleanSeriesTitle(title.ToLower());
Func<IEnumerable<string>, string, bool> altTitleComparer = Func<IEnumerable<AlternativeTitle>, string, bool> altTitleComparer =
(alternativeTitles, atitle) => (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, 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 result;
/*return year.HasValue /*return year.HasValue
? results?.FirstOrDefault(movie => movie.Year == year.Value) ? 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) public Movie FindByTmdbId(int tmdbid)

View File

@ -16,7 +16,7 @@ public static class QueryExtensions
{ {
public static Movie FirstWithYear(this SortBuilder<Movie> query, int? year) 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 @@ public static class EnumerableExtensions
{ {
public static Movie FirstWithYear(this IEnumerable<Movie> query, int? year) 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();
} }
} }
} }

View File

@ -14,6 +14,8 @@
using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Tv.Commands;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MetadataSource.RadarrAPI;
using NzbDrone.Core.Movies.AlternativeTitles;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
@ -21,27 +23,34 @@ public class RefreshMovieService : IExecute<RefreshMovieCommand>
{ {
private readonly IProvideMovieInfo _movieInfo; private readonly IProvideMovieInfo _movieInfo;
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IAlternativeTitleService _titleService;
private readonly IRefreshEpisodeService _refreshEpisodeService; private readonly IRefreshEpisodeService _refreshEpisodeService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed; private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed;
private readonly IRadarrAPIClient _apiClient;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshMovieService(IProvideMovieInfo movieInfo, public RefreshMovieService(IProvideMovieInfo movieInfo,
IMovieService movieService, IMovieService movieService,
IAlternativeTitleService titleService,
IRefreshEpisodeService refreshEpisodeService, IRefreshEpisodeService refreshEpisodeService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IDiskScanService diskScanService, IDiskScanService diskScanService,
IRadarrAPIClient apiClient,
ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed, ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed,
IManageCommandQueue commandQueue, IManageCommandQueue commandQueue,
Logger logger) Logger logger)
{ {
_movieInfo = movieInfo; _movieInfo = movieInfo;
_movieService = movieService; _movieService = movieService;
_titleService = titleService;
_refreshEpisodeService = refreshEpisodeService; _refreshEpisodeService = refreshEpisodeService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_commandQueueManager = commandQueue; _apiClient = apiClient;
_commandQueueManager = commandQueue;
_diskScanService = diskScanService; _diskScanService = diskScanService;
_checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed; _checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed;
_logger = logger; _logger = logger;
@ -85,7 +94,7 @@ private void RefreshMovieInfo(Movie movie)
movie.Certification = movieInfo.Certification; movie.Certification = movieInfo.Certification;
movie.InCinemas = movieInfo.InCinemas; movie.InCinemas = movieInfo.InCinemas;
movie.Website = movieInfo.Website; movie.Website = movieInfo.Website;
movie.AlternativeTitles = movieInfo.AlternativeTitles; //movie.AlternativeTitles = movieInfo.AlternativeTitles;
movie.Year = movieInfo.Year; movie.Year = movieInfo.Year;
movie.PhysicalRelease = movieInfo.PhysicalRelease; movie.PhysicalRelease = movieInfo.PhysicalRelease;
movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId; movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId;
@ -102,8 +111,47 @@ private void RefreshMovieInfo(Movie movie)
_logger.Warn(e, "Couldn't update movie path for " + movie.Path); _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); _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); _logger.Debug("Finished movie refresh for {0}", movie.Title);
_eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie));
} }

View File

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

View File

@ -47,7 +47,7 @@ private bool BeValidRelease(ReleaseResource releaseResource)
releaseResource.Age.Should().BeGreaterOrEqualTo(-1); releaseResource.Age.Should().BeGreaterOrEqualTo(-1);
releaseResource.Title.Should().NotBeNullOrWhiteSpace(); releaseResource.Title.Should().NotBeNullOrWhiteSpace();
releaseResource.DownloadUrl.Should().NotBeNullOrWhiteSpace(); releaseResource.DownloadUrl.Should().NotBeNullOrWhiteSpace();
releaseResource.SeriesTitle.Should().NotBeNullOrWhiteSpace(); releaseResource.MovieTitle.Should().NotBeNullOrWhiteSpace();
//TODO: uncomment these after moving to restsharp for rss //TODO: uncomment these after moving to restsharp for rss
//releaseResource.NzbInfoUrl.Should().NotBeNullOrWhiteSpace(); //releaseResource.NzbInfoUrl.Should().NotBeNullOrWhiteSpace();
//releaseResource.Size.Should().BeGreaterThan(0); //releaseResource.Size.Should().BeGreaterThan(0);

View File

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

View File

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

View File

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

View File

@ -7,6 +7,15 @@
border-radius: .1em; 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-right { float: right; }
.pull-left { float: left; } .pull-left { float: left; }

View File

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

View File

@ -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) * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/ */
@ -15,3 +15,4 @@
@import "rotated-flipped.less"; @import "rotated-flipped.less";
@import "stacked.less"; @import "stacked.less";
@import "icons.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

View File

@ -163,6 +163,7 @@
.@{fa-css-prefix}-github:before { content: @fa-var-github; } .@{fa-css-prefix}-github:before { content: @fa-var-github; }
.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } .@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; }
.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } .@{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}-rss:before { content: @fa-var-rss; }
.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } .@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; }
.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } .@{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}-stumbleupon:before { content: @fa-var-stumbleupon; }
.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } .@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; }
.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } .@{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}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; }
.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } .@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; }
.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } .@{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}-life-ring:before { content: @fa-var-life-ring; }
.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } .@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; }
.@{fa-css-prefix}-ra:before, .@{fa-css-prefix}-ra:before,
.@{fa-css-prefix}-resistance:before,
.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } .@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; }
.@{fa-css-prefix}-ge:before, .@{fa-css-prefix}-ge:before,
.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } .@{fa-css-prefix}-empire:before { content: @fa-var-empire; }
.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } .@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; }
.@{fa-css-prefix}-git:before { content: @fa-var-git; } .@{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}-hacker-news:before { content: @fa-var-hacker-news; }
.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } .@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; }
.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } .@{fa-css-prefix}-qq:before { content: @fa-var-qq; }
@ -502,7 +506,6 @@
.@{fa-css-prefix}-send-o:before, .@{fa-css-prefix}-send-o:before,
.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } .@{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}-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}-circle-thin:before { content: @fa-var-circle-thin; }
.@{fa-css-prefix}-header:before { content: @fa-var-header; } .@{fa-css-prefix}-header:before { content: @fa-var-header; }
.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } .@{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}-venus:before { content: @fa-var-venus; }
.@{fa-css-prefix}-mars:before { content: @fa-var-mars; } .@{fa-css-prefix}-mars:before { content: @fa-var-mars; }
.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; } .@{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:before { content: @fa-var-transgender; }
.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; } .@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; }
.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; } .@{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-v:before { content: @fa-var-mars-stroke-v; }
.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; } .@{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}-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}-facebook-official:before { content: @fa-var-facebook-official; }
.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; } .@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; }
.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; } .@{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}-train:before { content: @fa-var-train; }
.@{fa-css-prefix}-subway:before { content: @fa-var-subway; } .@{fa-css-prefix}-subway:before { content: @fa-var-subway; }
.@{fa-css-prefix}-medium:before { content: @fa-var-medium; } .@{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; }

View File

@ -3,25 +3,58 @@
.fa-icon() { .fa-icon() {
display: inline-block; 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 font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094 text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
transform: translate(0, 0); // ensures no half-pixel rendering in firefox
} }
.fa-icon-rotate(@degrees, @rotation) { .fa-icon-rotate(@degrees, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})";
-webkit-transform: rotate(@degrees); -webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees); -ms-transform: rotate(@degrees);
transform: rotate(@degrees); transform: rotate(@degrees);
} }
.fa-icon-flip(@horiz, @vert, @rotation) { .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); -webkit-transform: scale(@horiz, @vert);
-ms-transform: scale(@horiz, @vert); -ms-transform: scale(@horiz, @vert);
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;
}
}

View File

@ -9,7 +9,7 @@
url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 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.ttf?v=@{fa-version}') format('truetype'),
url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 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-weight: normal;
font-style: normal; font-style: normal;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,8 @@
<div> <div>
<h1 class="header-text"> <h1 class="header-text">
<i class="x-monitored" title="Toggle monitored state for movie"/> <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="movie-actions pull-right">
<div class="x-episode-file-editor"> <div class="x-episode-file-editor">
<i class="icon-sonarr-episode-file" title="Modify movie files"/> <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-history" class="x-movie-history">History</a></li>
<li><a href="#movie-search" class="x-movie-search">Search</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-files" class="x-movie-files">Files</a></li>
<li><a href="#movie-titles" class="x-movie-titles">Titles</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane" id="movie-history"/> <div class="tab-pane" id="movie-history"/>
<div class="tab-pane" id="movie-search"/> <div class="tab-pane" id="movie-search"/>
<div class="tab-pane" id="movie-files"/> <div class="tab-pane" id="movie-files"/>
<div class="tab-pane" id="movie-titles"/>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
{{this}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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