diff --git a/MediaBrowser.Model/ApiClient/IApiClient.cs b/MediaBrowser.Model/ApiClient/IApiClient.cs index 21894819da..3f66f195e6 100644 --- a/MediaBrowser.Model/ApiClient/IApiClient.cs +++ b/MediaBrowser.Model/ApiClient/IApiClient.cs @@ -859,6 +859,14 @@ namespace MediaBrowser.Model.ApiClient /// System.String. string GetArtImageUrl(BaseItemDto item, ImageOptions options); + /// + /// Gets the thumb image URL. + /// + /// The item. + /// The options. + /// System.String. + string GetThumbImageUrl(BaseItemDto item, ImageOptions options); + /// /// Gets the url needed to stream an audio file /// diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 22280f6ba8..501095f241 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -468,6 +468,30 @@ namespace MediaBrowser.Model.Dto /// /// The parent art image tag. public Guid? ParentArtImageTag { get; set; } + + /// + /// Gets or sets the series thumb image tag. + /// + /// The series thumb image tag. + public Guid? SeriesThumbImageTag { get; set; } + + /// + /// Gets or sets the series studio. + /// + /// The series studio. + public string SeriesStudio { get; set; } + + /// + /// Gets or sets the parent thumb item id. + /// + /// The parent thumb item id. + public string ParentThumbItemId { get; set; } + + /// + /// Gets or sets the parent thumb image tag. + /// + /// The parent thumb image tag. + public Guid? ParentThumbImageTag { get; set; } /// /// Gets or sets the chapters. diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs index f2c9ece326..12dfa96261 100644 --- a/MediaBrowser.Model/Querying/ItemSortBy.cs +++ b/MediaBrowser.Model/Querying/ItemSortBy.cs @@ -83,5 +83,6 @@ namespace MediaBrowser.Model.Querying public const string MusicVideoCount = "MusicVideoCount"; public const string SeriesSortName = "SeriesSortName"; public const string VideoBitRate = "VideoBitRate"; + public const string AirTime = "AirTime"; } } diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index 92759e0135..9ed0860b2e 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbProvider.cs @@ -386,7 +386,7 @@ namespace MediaBrowser.Providers.Movies name = name.Replace(",", " "); name = name.Replace(".", " "); name = name.Replace("_", " "); - name = name.Replace("-", ""); + name = name.Replace("-", " "); // Search again if the new name is different if (!string.Equals(name, originalName)) diff --git a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs index d639c67439..322fcd2280 100644 --- a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs @@ -295,6 +295,9 @@ namespace MediaBrowser.Providers.TV }).ConfigureAwait(false)) { + // Delete existing files + DeleteXmlFiles(seriesDataPath); + // Copy to memory stream because we need a seekable stream using (var ms = new MemoryStream()) { @@ -315,6 +318,23 @@ namespace MediaBrowser.Providers.TV await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, ConfigurationManager.Configuration.PreferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false); } + private void DeleteXmlFiles(string path) + { + try + { + foreach (var file in new DirectoryInfo(path) + .EnumerateFiles("*.xml", SearchOption.AllDirectories) + .ToList()) + { + file.Delete(); + } + } + catch (DirectoryNotFoundException) + { + // No biggie + } + } + /// /// Sanitizes the XML file. /// diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index 51b38f4cbd..d35e5e9d84 100644 --- a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs +++ b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs @@ -1,7 +1,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; @@ -25,13 +24,11 @@ namespace MediaBrowser.Providers.TV private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; - private readonly IDirectoryWatchers _directoryWatchers; - public SeriesPostScanTask(ILibraryManager libraryManager, ILogger logger, IDirectoryWatchers directoryWatchers, IServerConfigurationManager config) + public SeriesPostScanTask(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config) { _libraryManager = libraryManager; _logger = logger; - _directoryWatchers = directoryWatchers; _config = config; } @@ -53,7 +50,7 @@ namespace MediaBrowser.Providers.TV { cancellationToken.ThrowIfCancellationRequested(); - await new MissingEpisodeProvider(_logger, _directoryWatchers, _config).Run(series, cancellationToken).ConfigureAwait(false); + await new MissingEpisodeProvider(_logger, _config).Run(series, cancellationToken).ConfigureAwait(false); var episodes = series.RecursiveChildren .OfType() @@ -88,14 +85,12 @@ namespace MediaBrowser.Providers.TV { private readonly IServerConfigurationManager _config; private readonly ILogger _logger; - private readonly IDirectoryWatchers _directoryWatchers; private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public MissingEpisodeProvider(ILogger logger, IDirectoryWatchers directoryWatchers, IServerConfigurationManager config) + public MissingEpisodeProvider(ILogger logger, IServerConfigurationManager config) { _logger = logger; - _directoryWatchers = directoryWatchers; _config = config; } @@ -141,19 +136,21 @@ namespace MediaBrowser.Providers.TV .Where(i => i.Item1 != -1 && i.Item2 != -1) .ToList(); - var hasChanges = false; + var anySeasonsRemoved = await RemoveObsoleteOrMissingSeasons(series, episodeLookup, cancellationToken).ConfigureAwait(false); + + var anyEpisodesRemoved = await RemoveObsoleteOrMissingEpisodes(series, episodeLookup, cancellationToken).ConfigureAwait(false); + + var hasNewEpisodes = false; if (_config.Configuration.CreateVirtualMissingEpisodes || _config.Configuration.CreateVirtualFutureEpisodes) { if (_config.Configuration.EnableInternetProviders) { - hasChanges = await AddMissingEpisodes(series, seriesDataPath, episodeLookup, cancellationToken).ConfigureAwait(false); + hasNewEpisodes = await AddMissingEpisodes(series, seriesDataPath, episodeLookup, cancellationToken).ConfigureAwait(false); } } - var anyRemoved = await RemoveObsoleteMissingEpisodes(series, cancellationToken).ConfigureAwait(false); - - if (hasChanges || anyRemoved) + if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved) { await series.RefreshMetadata(cancellationToken, true).ConfigureAwait(false); await series.ValidateChildren(new Progress(), cancellationToken, true).ConfigureAwait(false); @@ -231,7 +228,7 @@ namespace MediaBrowser.Providers.TV /// /// Removes the virtual entry after a corresponding physical version has been added /// - private async Task RemoveObsoleteMissingEpisodes(Series series, CancellationToken cancellationToken) + private async Task RemoveObsoleteOrMissingEpisodes(Series series, IEnumerable> episodeLookup, CancellationToken cancellationToken) { var existingEpisodes = series.RecursiveChildren .OfType() @@ -250,10 +247,27 @@ namespace MediaBrowser.Providers.TV { if (i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue) { - return physicalEpisodes.Any(p => p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == i.ParentIndexNumber.Value && p.ContainsEpisodeNumber(i.IndexNumber.Value)); + var seasonNumber = i.ParentIndexNumber.Value; + var episodeNumber = i.IndexNumber.Value; + + // If there's a physical episode with the same season and episode number, delete it + if (physicalEpisodes.Any(p => + p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber && + p.ContainsEpisodeNumber(episodeNumber))) + { + return true; + } + + // If the episode no longer exists in the remote lookup, delete it + if (!episodeLookup.Any(e => e.Item1 == seasonNumber && e.Item2 == episodeNumber)) + { + return true; + } + + return false; } - return false; + return true; }) .ToList(); @@ -261,7 +275,7 @@ namespace MediaBrowser.Providers.TV foreach (var episodeToRemove in episodesToRemove) { - _logger.Info("Removing {0} {1}x{2}", series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber); + _logger.Info("Removing missing/unaired episode {0} {1}x{2}", series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber); await episodeToRemove.Parent.RemoveChild(episodeToRemove, cancellationToken).ConfigureAwait(false); @@ -271,6 +285,67 @@ namespace MediaBrowser.Providers.TV return hasChanges; } + /// + /// Removes the obsolete or missing seasons. + /// + /// The series. + /// The episode lookup. + /// The cancellation token. + /// Task{System.Boolean}. + private async Task RemoveObsoleteOrMissingSeasons(Series series, IEnumerable> episodeLookup, CancellationToken cancellationToken) + { + var existingSeasons = series.Children + .OfType() + .ToList(); + + var physicalSeasons = existingSeasons + .Where(i => i.LocationType != LocationType.Virtual) + .ToList(); + + var virtualSeasons = existingSeasons + .Where(i => i.LocationType == LocationType.Virtual) + .ToList(); + + var seasonsToRemove = virtualSeasons + .Where(i => + { + if (i.IndexNumber.HasValue) + { + var seasonNumber = i.IndexNumber.Value; + + // If there's a physical season with the same number, delete it + if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber)) + { + return true; + } + + // If the season no longer exists in the remote lookup, delete it + if (episodeLookup.All(e => e.Item1 != seasonNumber)) + { + return true; + } + + return false; + } + + return true; + }) + .ToList(); + + var hasChanges = false; + + foreach (var seasonToRemove in seasonsToRemove) + { + _logger.Info("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber); + + await seasonToRemove.Parent.RemoveChild(seasonToRemove, cancellationToken).ConfigureAwait(false); + + hasChanges = true; + } + + return hasChanges; + } + /// /// Adds the episode. /// @@ -319,35 +394,17 @@ namespace MediaBrowser.Providers.TV var name = string.Format("Season {0}", seasonNumber.ToString(UsCulture)); - var path = Path.Combine(series.Path, name); - var season = new Season { Name = name, IndexNumber = seasonNumber, - Path = path, Parent = series, - DisplayMediaType = typeof(Season).Name + DisplayMediaType = typeof(Season).Name, + Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Season)) }; - _directoryWatchers.TemporarilyIgnore(path); - - try - { - var info = Directory.CreateDirectory(path); - - season.DateCreated = info.CreationTimeUtc; - season.DateModified = info.LastWriteTimeUtc; - - await series.AddChild(season, cancellationToken).ConfigureAwait(false); - - await season.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - finally - { - _directoryWatchers.RemoveTempIgnore(path); - } - + await series.AddChild(season, cancellationToken).ConfigureAwait(false); + await season.RefreshMetadata(cancellationToken).ConfigureAwait(false); return season; } diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index fea49880e1..5f4a6a52f8 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -868,6 +868,19 @@ namespace MediaBrowser.Server.Implementations.Dto } } + // If there is no thumb, indicate what parent has one in case the Ui wants to allow inheritance + if (!dto.HasThumb) + { + var parentWithImage = GetParentImageItem(item, ImageType.Thumb, owner); + + if (parentWithImage != null) + { + dto.ParentThumbItemId = GetDtoId(parentWithImage); + + dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb, parentWithImage.GetImage(ImageType.Thumb)); + } + } + if (fields.Contains(ItemFields.Path)) { dto.Path = item.Path; @@ -1022,6 +1035,13 @@ namespace MediaBrowser.Server.Implementations.Dto dto.SeriesId = GetDtoId(series); dto.SeriesName = series.Name; + dto.AirTime = series.AirTime; + dto.SeriesStudio = series.Studios.FirstOrDefault(); + + if (series.HasImage(ImageType.Thumb)) + { + dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb, series.GetImage(ImageType.Thumb)); + } } // Add SeasonInfo @@ -1033,6 +1053,8 @@ namespace MediaBrowser.Server.Implementations.Dto dto.SeriesId = GetDtoId(series); dto.SeriesName = series.Name; + dto.AirTime = series.AirTime; + dto.SeriesStudio = series.Studios.FirstOrDefault(); } var game = item as Game; diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 3857127008..05c5f5a826 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -186,6 +186,7 @@ + diff --git a/MediaBrowser.Server.Implementations/Sorting/AirTimeComparer.cs b/MediaBrowser.Server.Implementations/Sorting/AirTimeComparer.cs new file mode 100644 index 0000000000..46c3df07b7 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sorting/AirTimeComparer.cs @@ -0,0 +1,48 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Querying; +using System; + +namespace MediaBrowser.Server.Implementations.Sorting +{ + public class AirTimeComparer : IBaseItemComparer + { + /// + /// Compares the specified x. + /// + /// The x. + /// The y. + /// System.Int32. + public int Compare(BaseItem x, BaseItem y) + { + return DateTime.Compare(GetValue(x), GetValue(y)); + } + + /// + /// Gets the value. + /// + /// The x. + /// System.String. + private DateTime GetValue(BaseItem x) + { + var series = (x as Series) ?? x.FindParent(); + + DateTime result; + if (series != null && DateTime.TryParse(series.AirTime, out result)) + { + return result; + } + return DateTime.MinValue; + } + + /// + /// Gets the name. + /// + /// The name. + public string Name + { + get { return ItemSortBy.AirTime; } + } + } +} diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 264eaeeeb8..126b8d5caa 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -513,6 +513,7 @@ namespace MediaBrowser.WebDashboard.Api "tvrecommended.js", "tvshows.js", "tvstudios.js", + "tvupcoming.js", "updatepasswordpage.js", "userimagepage.js", "userprofilespage.js", diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index 8c2dfc7201..97a443e844 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -2018,11 +2018,28 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi options.imageType = "logo"; - var logoItemId = item.HasLogo ? item.Id : item.ParentLogoItemId; + var logoItemId = item.ImageTags && item.ImageTags.Logo ? item.Id : item.ParentLogoItemId; return logoItemId ? self.getImageUrl(logoItemId, options) : null; }; + self.getThumbImageUrl = function (item, options) { + + if (!item) { + throw new Error("null item"); + } + + options = options || { + + }; + + options.imageType = "thumb"; + + var itemId = item.ImageTags && item.ImageTags.Thumb ? item.Id : item.ParentThumbItemId; + + return itemId ? self.getImageUrl(itemId, options) : null; + }; + /** * Constructs an array of backdrop image url's for an item * If the item doesn't have any backdrops, it will inherit them from a parent diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 5ed3544c62..b2d9a09457 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -341,12 +341,18 @@ PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index f0ac36bc53..9c48b38095 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file