add cinema mode feature

This commit is contained in:
Luke Pulverenti
2014-09-22 17:56:54 -04:00
parent ac201a6cdb
commit 1afb28b487
55 changed files with 1014 additions and 141 deletions

View File

@@ -332,7 +332,7 @@ namespace MediaBrowser.Server.Implementations.Channels
{
return new ITaskTrigger[]
{
new IntervalTrigger{ Interval = TimeSpan.FromHours(6)},
new IntervalTrigger{ Interval = TimeSpan.FromHours(3)},
};
}

View File

@@ -62,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.Channels
{
get
{
return TimeSpan.FromHours(12);
return TimeSpan.FromHours(6);
}
}
@@ -663,7 +663,7 @@ namespace MediaBrowser.Server.Implementations.Channels
private async Task<IEnumerable<ChannelItemInfo>> GetLatestItems(ISupportsLatestMedia indexable, IChannel channel, string userId, CancellationToken cancellationToken)
{
var cacheLength = TimeSpan.FromHours(12);
var cacheLength = CacheLength;
var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-latest", null, false);
try
@@ -720,7 +720,7 @@ namespace MediaBrowser.Server.Implementations.Channels
}
}
public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken)
public async Task<QueryResult<BaseItem>> GetAllMediaInternal(AllChannelMediaQuery query, CancellationToken cancellationToken)
{
var user = string.IsNullOrWhiteSpace(query.UserId)
? null
@@ -798,19 +798,43 @@ namespace MediaBrowser.Server.Implementations.Channels
var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false);
await RefreshIfNeeded(internalItems, cancellationToken).ConfigureAwait(false);
var returnItemArray = internalItems.Select(i => _dtoService.GetBaseItemDto(i, query.Fields, user))
.ToArray();
var returnItemArray = internalItems.ToArray();
return new QueryResult<BaseItemDto>
return new QueryResult<BaseItem>
{
TotalRecordCount = totalCount,
Items = returnItemArray
};
}
public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken)
{
var user = string.IsNullOrWhiteSpace(query.UserId)
? null
: _userManager.GetUserById(query.UserId);
var internalResult = await GetAllMediaInternal(query, cancellationToken).ConfigureAwait(false);
// Get everything
var fields = Enum.GetNames(typeof(ItemFields))
.Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
.ToList();
var returnItems = internalResult.Items.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
.ToArray();
var result = new QueryResult<BaseItemDto>
{
Items = returnItems,
TotalRecordCount = internalResult.TotalRecordCount
};
return result;
}
private async Task<ChannelItemResult> GetAllItems(IIndexableChannel indexable, IChannel channel, string userId, CancellationToken cancellationToken)
{
var cacheLength = TimeSpan.FromHours(12);
var cacheLength = CacheLength;
var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-allitems", null, false);
try
@@ -1199,7 +1223,6 @@ namespace MediaBrowser.Server.Implementations.Channels
item.Genres = info.Genres;
item.Studios = info.Studios;
item.CommunityRating = info.CommunityRating;
item.OfficialRating = info.OfficialRating;
item.Overview = info.Overview;
item.IndexNumber = info.IndexNumber;
item.ParentIndexNumber = info.ParentIndexNumber;
@@ -1207,6 +1230,7 @@ namespace MediaBrowser.Server.Implementations.Channels
item.PremiereDate = info.PremiereDate;
item.ProductionYear = info.ProductionYear;
item.ProviderIds = info.ProviderIds;
item.OfficialRating = info.OfficialRating;
item.DateCreated = info.DateCreated.HasValue ?
info.DateCreated.Value :

View File

@@ -0,0 +1,136 @@
using System.Collections.Generic;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Logging;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Channels
{
public class ChannelPostScanTask : ILibraryPostScanTask
{
private readonly IChannelManager _channelManager;
private readonly IUserManager _userManager;
private readonly ILogger _logger;
public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger)
{
_channelManager = channelManager;
_userManager = userManager;
_logger = logger;
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var users = _userManager.Users
.Select(i => i.Id.ToString("N"))
.ToList();
var numComplete = 0;
foreach (var user in users)
{
double percentPerUser = 1;
percentPerUser /= users.Count;
var startingPercent = numComplete * percentPerUser * 100;
var innerProgress = new ActionableProgress<double>();
innerProgress.RegisterAction(p => progress.Report(startingPercent + (percentPerUser * p)));
await DownloadContent(user, cancellationToken, innerProgress).ConfigureAwait(false);
numComplete++;
double percent = numComplete;
percent /= users.Count;
progress.Report(percent * 100);
}
progress.Report(100);
}
private async Task DownloadContent(string user, CancellationToken cancellationToken, IProgress<double> progress)
{
var channels = await _channelManager.GetChannelsInternal(new ChannelQuery
{
UserId = user
}, cancellationToken);
var numComplete = 0;
foreach (var channel in channels.Items)
{
try
{
await GetAllItems(user, channel.Id.ToString("N"), null, false, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error getting channel content", ex);
}
numComplete++;
double percent = numComplete;
percent /= channels.Items.Length;
progress.Report(percent * 100);
}
progress.Report(100);
}
private async Task GetAllItems(string user, string channelId, string folderId, bool recursive, CancellationToken cancellationToken)
{
var folderItems = new List<string>();
var result = await _channelManager.GetChannelItemsInternal(new ChannelItemQuery
{
ChannelId = channelId,
UserId = user,
FolderId = folderId
}, cancellationToken);
folderItems.AddRange(result.Items.Where(i => i.IsFolder).Select(i => i.Id.ToString("N")));
var totalRetrieved = result.Items.Length;
var totalCount = result.TotalRecordCount;
while (totalRetrieved < totalCount)
{
result = await _channelManager.GetChannelItemsInternal(new ChannelItemQuery
{
ChannelId = channelId,
UserId = user,
StartIndex = totalRetrieved,
FolderId = folderId
}, cancellationToken);
folderItems.AddRange(result.Items.Where(i => i.IsFolder).Select(i => i.Id.ToString("N")));
totalRetrieved += result.Items.Length;
totalCount = result.TotalRecordCount;
}
if (recursive)
{
foreach (var folder in folderItems)
{
try
{
await GetAllItems(user, channelId, folder, false, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error getting channel content", ex);
}
}
}
}
}
}

View File

@@ -402,11 +402,11 @@ namespace MediaBrowser.Server.Implementations.Connect
}
else if (!string.IsNullOrWhiteSpace(query.Name))
{
url = url + "?nameoremail=" + WebUtility.UrlEncode(query.Name);
url = url + "?name=" + WebUtility.UrlEncode(query.Name);
}
else if (!string.IsNullOrWhiteSpace(query.Email))
{
url = url + "?nameoremail=" + WebUtility.UrlEncode(query.Email);
url = url + "?name=" + WebUtility.UrlEncode(query.Email);
}
var options = new HttpRequestOptions

View File

@@ -0,0 +1,361 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Security;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Localization;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Intros
{
public class DefaultIntroProvider : IIntroProvider
{
private readonly ISecurityManager _security;
private readonly IChannelManager _channelManager;
private readonly ILocalizationManager _localization;
private readonly IConfigurationManager _serverConfig;
public DefaultIntroProvider(ISecurityManager security, IChannelManager channelManager, ILocalizationManager localization, IConfigurationManager serverConfig)
{
_security = security;
_channelManager = channelManager;
_localization = localization;
_serverConfig = serverConfig;
}
public async Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user)
{
var config = GetOptions();
if (item is Movie)
{
if (!config.EnableIntrosForMovies)
{
return new List<IntroInfo>();
}
}
else if (item is Episode)
{
if (!config.EnableIntrosForEpisodes)
{
return new List<IntroInfo>();
}
}
else
{
return new List<IntroInfo>();
}
if (!IsSupporter)
{
return new List<IntroInfo>();
}
var ratingLevel = string.IsNullOrWhiteSpace(item.OfficialRating)
? (int?)null
: _localization.GetRatingLevel(item.OfficialRating);
var libaryItems = user.RootFolder.GetRecursiveChildren(user, false)
.ToList();
var random = new Random(Environment.TickCount + Guid.NewGuid().GetHashCode());
var candidates = new List<ItemWithTrailer>();
if (config.EnableIntrosFromMoviesInLibrary)
{
var itemsWithTrailers = libaryItems
.Where(i =>
{
var hasTrailers = i as IHasTrailers;
if (hasTrailers != null && hasTrailers.LocalTrailerIds.Count > 0)
{
if (i is Movie)
{
return true;
}
}
return false;
});
candidates.AddRange(itemsWithTrailers.Select(i => new ItemWithTrailer
{
Item = i,
Type = ItemWithTrailerType.ItemWithTrailer,
User = user,
WatchingItem = item,
Random = random
}));
}
if (config.EnableIntrosFromUpcomingTrailers)
{
var channelTrailers = await _channelManager.GetAllMediaInternal(new AllChannelMediaQuery
{
ContentTypes = new[] { ChannelMediaContentType.Trailer },
UserId = user.Id.ToString("N")
}, CancellationToken.None);
candidates.AddRange(channelTrailers.Items.Select(i => new ItemWithTrailer
{
Item = i,
Type = ItemWithTrailerType.ChannelTrailer,
User = user,
WatchingItem = item,
Random = random
}));
candidates.AddRange(libaryItems.Where(i => i is Trailer).Select(i => new ItemWithTrailer
{
Item = i,
Type = ItemWithTrailerType.LibraryTrailer,
User = user,
WatchingItem = item,
Random = random
}));
}
var customIntros = config.EnableCustomIntro ?
GetCustomIntros(item) :
new List<IntroInfo>();
var trailerLimit = 2;
if (customIntros.Count > 0)
{
trailerLimit--;
}
// Avoid implicitly captured closure
var currentUser = user;
return candidates.Where(i =>
{
if (config.EnableIntrosParentalControl && !FilterByParentalRating(ratingLevel, i.Item))
{
return false;
}
if (!config.EnableIntrosForWatchedContent && i.IsPlayed)
{
return false;
}
return true;
})
.OrderByDescending(i => i.Score)
.ThenBy(i => Guid.NewGuid())
.ThenByDescending(i => (i.IsPlayed ? 0 : 1))
.Select(i => i.IntroInfo)
.Take(trailerLimit)
.Concat(customIntros.Take(1));
}
private CinemaModeConfiguration GetOptions()
{
return _serverConfig.GetConfiguration<CinemaModeConfiguration>("cinemamode");
}
private List<IntroInfo> GetCustomIntros(BaseItem item)
{
return new List<IntroInfo>();
}
private bool FilterByParentalRating(int? ratingLevel, BaseItem item)
{
// Only content rated same or lower
if (ratingLevel.HasValue)
{
var level = string.IsNullOrWhiteSpace(item.OfficialRating)
? (int?)null
: _localization.GetRatingLevel(item.OfficialRating);
return level.HasValue && level.Value <= ratingLevel.Value;
}
return true;
}
internal static int GetSimiliarityScore(BaseItem item1, BaseItem item2, Random random)
{
var points = 0;
if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
{
points += 10;
}
// Find common genres
points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
// Find common tags
points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
// Find common keywords
points += GetKeywords(item1).Where(i => GetKeywords(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
// Find common studios
points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
var item2PeopleNames = item2.People.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
{
if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
{
return 5;
}
if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
return 1;
});
// Add some randomization so that you're not always seeing the same ones for a given movie
points += random.Next(0, 50);
return points;
}
private static IEnumerable<string> GetTags(BaseItem item)
{
var hasTags = item as IHasTags;
if (hasTags != null)
{
return hasTags.Tags;
}
return new List<string>();
}
private static IEnumerable<string> GetKeywords(BaseItem item)
{
var hasTags = item as IHasKeywords;
if (hasTags != null)
{
return hasTags.Keywords;
}
return new List<string>();
}
public IEnumerable<string> GetAllIntroFiles()
{
return new List<string>();
}
private bool IsSupporter
{
get { return _security.IsMBSupporter; }
}
public string Name
{
get { return "Default"; }
}
internal class ItemWithTrailer
{
internal BaseItem Item;
internal ItemWithTrailerType Type;
internal User User;
internal BaseItem WatchingItem;
internal Random Random;
private bool? _isPlayed;
public bool IsPlayed
{
get
{
if (!_isPlayed.HasValue)
{
_isPlayed = Item.IsPlayed(User);
}
return _isPlayed.Value;
}
}
private int? _score;
public int Score
{
get
{
if (!_score.HasValue)
{
_score = GetSimiliarityScore(WatchingItem, Item, Random);
}
return _score.Value;
}
}
public IntroInfo IntroInfo
{
get
{
var id = Item.Id;
if (Type == ItemWithTrailerType.ItemWithTrailer)
{
var hasTrailers = Item as IHasTrailers;
if (hasTrailers != null)
{
id = hasTrailers.LocalTrailerIds.FirstOrDefault();
}
}
return new IntroInfo
{
ItemId = id
};
}
}
}
internal enum ItemWithTrailerType
{
LibraryTrailer,
ChannelTrailer,
ItemWithTrailer
}
}
public class CinemaModeConfigurationFactory : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
{
new ConfigurationStore
{
ConfigurationType = typeof(CinemaModeConfiguration),
Key = "cinemamode"
}
};
}
}
}

View File

@@ -95,8 +95,7 @@ namespace MediaBrowser.Server.Implementations.Library
return true;
}
// Don't misidentify xbmc trailers as a movie
if (filename.IndexOf(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) != -1)
if (BaseItem.ExtraSuffixes.Any(i => filename.IndexOf(i.Key, StringComparison.OrdinalIgnoreCase) != -1))
{
return true;
}

View File

@@ -1193,13 +1193,42 @@ namespace MediaBrowser.Server.Implementations.Library
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable{System.String}.</returns>
public IEnumerable<Video> GetIntros(BaseItem item, User user)
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
return IntroProviders.SelectMany(i => i.GetIntros(item, user))
var tasks = IntroProviders
.OrderBy(i => (i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 1 : 0))
.Take(1)
.Select(i => GetIntros(i, item, user));
var items = await Task.WhenAll(tasks).ConfigureAwait(false);
return items
.SelectMany(i => i.ToArray())
.Select(ResolveIntro)
.Where(i => i != null);
}
/// <summary>
/// Gets the intros.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
{
try
{
return await provider.GetIntros(item, user).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error getting intros", ex);
return new List<IntroInfo>();
}
}
/// <summary>
/// Gets all intro files.
/// </summary>
@@ -1487,7 +1516,7 @@ namespace MediaBrowser.Server.Implementations.Library
var item = GetItemById(id) as UserView;
if (item == null ||
if (item == null ||
!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
{
Directory.CreateDirectory(path);

View File

@@ -2,8 +2,10 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using System;
using System.IO;
using System.Linq;
namespace MediaBrowser.Server.Implementations.Library.Resolvers
{
@@ -41,9 +43,15 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
}
// Support xbmc local trailer convention, but only when looking for local trailers (hence the parent == null check)
if (args.Parent == null && _fileSystem.GetFileNameWithoutExtension(args.Path).EndsWith(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase))
if (args.Parent == null)
{
return base.Resolve(args);
var nameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(args.Path);
var suffix = BaseItem.ExtraSuffixes.First(i => i.Value == ExtraType.Trailer);
if (nameWithoutExtension.EndsWith(suffix.Key, StringComparison.OrdinalIgnoreCase))
{
return base.Resolve(args);
}
}
}

View File

@@ -111,8 +111,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
}
var filename = Path.GetFileName(args.Path);
// Don't misidentify xbmc trailers as a movie
if (filename.IndexOf(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) != -1)
// Don't misidentify extras or trailers
if (BaseItem.ExtraSuffixes.Any(i => filename.IndexOf(i.Key, StringComparison.OrdinalIgnoreCase) != -1))
{
return null;
}
@@ -229,8 +229,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
continue;
}
// Don't misidentify xbmc trailers as a movie
if (filename.IndexOf(BaseItem.XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) != -1)
// Don't misidentify extras or trailers as a movie
if (BaseItem.ExtraSuffixes.Any(i => filename.IndexOf(i.Key, StringComparison.OrdinalIgnoreCase) != -1))
{
continue;
}

View File

@@ -569,5 +569,6 @@
"MediaInfoStreamTypeVideo": "Video",
"MediaInfoStreamTypeSubtitle": "Subtitle",
"MediaInfoStreamTypeEmbeddedImage": "Embedded Image",
"MediaInfoRefFrames": "Ref frames"
"MediaInfoRefFrames": "Ref frames",
"TabPlayback": "Playback"
}

View File

@@ -288,7 +288,7 @@
"ButtonAutoScroll": "Auto-scroll",
"LabelImageSavingConvention": "Image saving convention:",
"LabelImageSavingConventionHelp": "Media Browser recognizes images from most major media applications. Choosing your downloading convention is useful if you also use other products.",
"OptionImageSavingCompatible": "Compatible - Media Browser/Xbmc/Plex",
"OptionImageSavingCompatible": "Compatible - Media Browser/Kodi/Plex",
"OptionImageSavingStandard": "Standard - MB2",
"ButtonSignIn": "Sign In",
"TitleSignIn": "Sign In",
@@ -883,22 +883,22 @@
"OptionLatestTvRecordings": "Latest recordings",
"LabelProtocolInfo": "Protocol info:",
"LabelProtocolInfoHelp": "The value that will be used when responding to GetProtocolInfo requests from the device.",
"TabXbmcMetadata": "Xbmc",
"HeaderXbmcMetadataHelp": "Media Browser includes native support for Xbmc Nfo metadata and images. To enable or disable Xbmc metadata, use the Advanced tab to configure options for your media types.",
"LabelXbmcMetadataUser": "Add user watch data to nfo's for:",
"LabelXbmcMetadataUserHelp": "Enable this to keep watch data in sync between Media Browser and Xbmc.",
"LabelXbmcMetadataDateFormat": "Release date format:",
"LabelXbmcMetadataDateFormatHelp": "All dates within nfo's will be read and written to using this format.",
"LabelXbmcMetadataSaveImagePaths": "Save image paths within nfo files",
"LabelXbmcMetadataSaveImagePathsHelp": "This is recommended if you have image file names that don't conform to Xbmc guidelines.",
"LabelXbmcMetadataEnablePathSubstitution": "Enable path substitution",
"LabelXbmcMetadataEnablePathSubstitutionHelp": "Enables path substitution of image paths using the server's path substitution settings.",
"LabelXbmcMetadataEnablePathSubstitutionHelp2": "See path substitution.",
"TabKodiMetadata": "Kodi",
"HeaderKodiMetadataHelp": "Media Browser includes native support for Kodi Nfo metadata and images. To enable or disable Kodi metadata, use the Advanced tab to configure options for your media types.",
"LabelKodiMetadataUser": "Add user watch data to nfo's for:",
"LabelKodiMetadataUserHelp": "Enable this to keep watch data in sync between Media Browser and Kodi.",
"LabelKodiMetadataDateFormat": "Release date format:",
"LabelKodiMetadataDateFormatHelp": "All dates within nfo's will be read and written to using this format.",
"LabelKodiMetadataSaveImagePaths": "Save image paths within nfo files",
"LabelKodiMetadataSaveImagePathsHelp": "This is recommended if you have image file names that don't conform to Kodi guidelines.",
"LabelKodiMetadataEnablePathSubstitution": "Enable path substitution",
"LabelKodiMetadataEnablePathSubstitutionHelp": "Enables path substitution of image paths using the server's path substitution settings.",
"LabelKodiMetadataEnablePathSubstitutionHelp2": "See path substitution.",
"LabelGroupChannelsIntoViews": "Display the following channels directly within my views:",
"LabelGroupChannelsIntoViewsHelp": "If enabled, these channels will be displayed directly alongside other views. If disabled, they'll be displayed within a separate Channels view.",
"LabelDisplayCollectionsView": "Display a collections view to show movie collections",
"LabelXbmcMetadataEnableExtraThumbs": "Copy extrafanart into extrathumbs",
"LabelXbmcMetadataEnableExtraThumbsHelp": "When downloading images they can be saved into both extrafanart and extrathumbs for maximum Xbmc skin compatibility.",
"LabelKodiMetadataEnableExtraThumbs": "Copy extrafanart into extrathumbs",
"LabelKodiMetadataEnableExtraThumbsHelp": "When downloading images they can be saved into both extrafanart and extrathumbs for maximum Kodi skin compatibility.",
"TabServices": "Services",
"TabLogs": "Logs",
"HeaderServerLogFiles": "Server log files:",
@@ -1179,5 +1179,21 @@
"OptionExternallyDownloaded": "External download",
"OptionHlsSegmentedSubtitles": "Hls segmented subtitles",
"LabelSubtitleFormatHelp": "Example: srt",
"ButtonLearnMore": "Learn more"
"ButtonLearnMore": "Learn more",
"TabPlayback": "Playback",
"HeaderTrailersAndExtras": "Trailers & Extras",
"OptionFindTrailers": "Find trailers from the internet automatically",
"HeaderLanguagePreferences": "Language Preferences",
"TabCinemaMode": "Cinema Mode",
"TitlePlayback": "Playback",
"LabelEnableCinemaModeFor": "Enable cinema mode for:",
"CinemaModeConfigurationHelp": "Cinema mode brings the theater experience straight to your living room with the ability to play trailers and custom intros before the main feature.",
"LabelEnableTheFollowingIntros": "Enable the following types of intros:",
"OptionTrailersFromMyMovies": "Trailers from movies in my library",
"OptionUpcomingMoviesInTheaters": "Trailers from upcoming movies",
"LabelLimitIntrosToUnwatchedContent": "Only use trailers from unwatched content",
"LabelEnableIntroParentalControl": "Enable smart parental control",
"LabelEnableIntroParentalControlHelp": "Intros will only used from content with a parental rating equal to or less than the content being watched.",
"LabelEnableTheFollowingIntrosHelp": "Trailers from existing movies requires setup of local trailers. Theater trailers require installation of the Trailer channel plugin.",
"ButtonThisFeatureRequiresSupporter": "This feature requires an active supporter membership"
}

View File

@@ -111,6 +111,7 @@
<Compile Include="Channels\ChannelImageProvider.cs" />
<Compile Include="Channels\ChannelItemImageProvider.cs" />
<Compile Include="Channels\ChannelManager.cs" />
<Compile Include="Channels\ChannelPostScanTask.cs" />
<Compile Include="Channels\RefreshChannelsScheduledTask.cs" />
<Compile Include="Collections\CollectionManager.cs" />
<Compile Include="Collections\CollectionsDynamicFolder.cs" />
@@ -173,6 +174,7 @@
<Compile Include="HttpServer\SocketSharp\WebSocketSharpRequest.cs" />
<Compile Include="HttpServer\SocketSharp\WebSocketSharpResponse.cs" />
<Compile Include="HttpServer\ThrottledStream.cs" />
<Compile Include="Intros\DefaultIntroProvider.cs" />
<Compile Include="IO\LibraryMonitor.cs" />
<Compile Include="Library\CoreResolutionIgnoreRule.cs" />
<Compile Include="Library\LibraryManager.cs" />