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