Added poor man's multi-file movie support

This commit is contained in:
Luke Pulverenti 2013-06-12 17:46:50 -04:00
parent 455de48a65
commit def3428199
20 changed files with 353 additions and 54 deletions

View File

@ -415,15 +415,13 @@ namespace MediaBrowser.Api
: DtoBuilder.GetItemByClientId(request.Id, _userManager, _libraryManager, request.UserId);
// Get everything
var fields =
Enum.GetNames(typeof(ItemFields))
var fields = Enum.GetNames(typeof(ItemFields))
.Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
.ToList();
var dtoBuilder = new DtoBuilder(Logger, _libraryManager, _userDataRepository);
var items =
_itemRepo.GetItems(item.ThemeSongIds)
var items = _itemRepo.GetItems(item.ThemeSongIds)
.OrderBy(i => i.SortName)
.Select(i => dtoBuilder.GetBaseItemDto(i, fields, user))
.Select(t => t.Result)

View File

@ -114,6 +114,7 @@
<Compile Include="UserLibrary\YearsService.cs" />
<Compile Include="UserService.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="VideosService.cs" />
<Compile Include="WeatherService.cs" />
<Compile Include="WebSocket\LogFileWebSocketListener.cs" />
<Compile Include="WebSocket\SessionInfoWebSocketListener.cs" />

View File

@ -0,0 +1,82 @@
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Querying;
using ServiceStack.ServiceHost;
using System;
using System.Linq;
namespace MediaBrowser.Api
{
[Route("/Videos/{Id}/AdditionalParts", "GET")]
[Api(Description = "Gets additional parts for a video.")]
public class GetAdditionalParts : IReturn<ItemsResult>
{
[ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { get; set; }
}
public class VideosService : BaseApiService
{
private readonly IItemRepository _itemRepo;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _userDataRepository;
public VideosService(IItemRepository itemRepo, ILibraryManager libraryManager, IUserManager userManager, IUserDataRepository userDataRepository)
{
_itemRepo = itemRepo;
_libraryManager = libraryManager;
_userManager = userManager;
_userDataRepository = userDataRepository;
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetAdditionalParts request)
{
var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null;
var item = string.IsNullOrEmpty(request.Id)
? (request.UserId.HasValue
? user.RootFolder
: (Folder)_libraryManager.RootFolder)
: DtoBuilder.GetItemByClientId(request.Id, _userManager, _libraryManager, request.UserId);
// Get everything
var fields = Enum.GetNames(typeof(ItemFields))
.Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
.ToList();
var dtoBuilder = new DtoBuilder(Logger, _libraryManager, _userDataRepository);
var video = (Video)item;
var items = _itemRepo.GetItems(video.AdditionalPartIds)
.OrderBy(i => i.SortName)
.Select(i => dtoBuilder.GetBaseItemDto(i, fields, user))
.Select(t => t.Result)
.ToArray();
var result = new ItemsResult
{
Items = items,
TotalRecordCount = items.Length
};
return ToOptimizedResult(result);
}
}
}

View File

@ -441,6 +441,8 @@ namespace MediaBrowser.Controller.Dto
dto.VideoFormat = video.VideoFormat;
dto.IsoType = video.IsoType;
dto.PartCount = video.AdditionalPartIds.Count + 1;
if (fields.Contains(ItemFields.Chapters) && video.Chapters != null)
{
dto.Chapters = video.Chapters.Select(c => GetChapterInfoDto(c, item)).ToList();

View File

@ -753,7 +753,7 @@ namespace MediaBrowser.Controller.Entities
// Support xbmc trailers (-trailer suffix on video file names)
files.AddRange(resolveArgs.FileSystemChildren.Where(i =>
{
if (!i.Attributes.HasFlag(FileAttributes.Directory))
if ((i.Attributes & FileAttributes.Directory) != FileAttributes.Directory)
{
if (System.IO.Path.GetFileNameWithoutExtension(i.Name).EndsWith(XbmcTrailerFileSuffix, StringComparison.OrdinalIgnoreCase) && !string.Equals(Path, i.FullName, StringComparison.OrdinalIgnoreCase))
{
@ -916,14 +916,11 @@ namespace MediaBrowser.Controller.Entities
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
/// <returns>true if a provider reports we changed</returns>
public virtual async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
{
if (resetResolveArgs)
public virtual async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
{
// Reload this
ResolveArgs = null;
}
// Refresh for the item
var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders);

View File

@ -768,7 +768,7 @@ namespace MediaBrowser.Controller.Entities
var child = currentTuple.Item1;
//refresh it
await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata).ConfigureAwait(false);
await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata).ConfigureAwait(false);
// Refresh children if a folder and the item changed or recursive is set to true
var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value));

View File

@ -195,9 +195,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
/// <returns>Task{System.Boolean}.</returns>
public override Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
public override Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
{
// We should never get in here since these are not part of the library
return Task.FromResult(false);

View File

@ -62,12 +62,11 @@ namespace MediaBrowser.Controller.Entities.Movies
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
/// <returns>Task{System.Boolean}.</returns>
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
{
// Kick off a task to refresh the main item
var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
var specialFeaturesChanged = await RefreshSpecialFeatures(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
@ -127,7 +126,7 @@ namespace MediaBrowser.Controller.Entities.Movies
}
catch (IOException ex)
{
Logger.ErrorException("Error loading trailers for {0}", ex, Name);
Logger.ErrorException("Error loading special features for {0}", ex, Name);
return new List<Video>();
}

View File

@ -322,14 +322,11 @@ namespace MediaBrowser.Controller.Entities
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
/// <returns>true if a provider reports we changed</returns>
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
{
if (resetResolveArgs)
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
{
// Reload this
ResolveArgs = null;
}
var changed = await ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders).ConfigureAwait(false);

View File

@ -1,8 +1,13 @@
using MediaBrowser.Model.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Entities
{
@ -11,11 +16,16 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Video : BaseItem, IHasMediaStreams
{
public bool IsMultiPart { get; set; }
public List<Guid> AdditionalPartIds { get; set; }
public Video()
{
MediaStreams = new List<MediaStream>();
Chapters = new List<ChapterInfo>();
PlayableStreamFileNames = new List<string>();
AdditionalPartIds = new List<Guid>();
}
/// <summary>
@ -112,5 +122,102 @@ namespace MediaBrowser.Controller.Entities
return Model.Entities.MediaType.Video;
}
}
/// <summary>
/// Overrides the base implementation to refresh metadata for local trailers
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
/// <returns>true if a provider reports we changed</returns>
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
{
// Kick off a task to refresh the main item
var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
var additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
return additionalPartsChanged || result;
}
/// <summary>
/// Refreshes the additional parts.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="forceSave">if set to <c>true</c> [force save].</param>
/// <param name="forceRefresh">if set to <c>true</c> [force refresh].</param>
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
/// <returns>Task{System.Boolean}.</returns>
private async Task<bool> RefreshAdditionalParts(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
{
var newItems = LoadAdditionalParts().ToList();
var newItemIds = newItems.Select(i => i.Id).ToList();
var itemsChanged = !AdditionalPartIds.SequenceEqual(newItemIds);
var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
AdditionalPartIds = newItemIds;
return itemsChanged || results.Contains(true);
}
/// <summary>
/// Loads the additional parts.
/// </summary>
/// <returns>IEnumerable{Video}.</returns>
private IEnumerable<Video> LoadAdditionalParts()
{
if (!IsMultiPart || LocationType != LocationType.FileSystem)
{
return new List<Video>();
}
ItemResolveArgs resolveArgs;
try
{
resolveArgs = ResolveArgs;
}
catch (IOException ex)
{
Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path);
return new List<Video>();
}
if (!resolveArgs.IsDirectory)
{
return new List<Video>();
}
var files = resolveArgs.FileSystemChildren.Where(i =>
{
if ((i.Attributes & FileAttributes.Directory) == FileAttributes.Directory)
{
return false;
}
return !string.Equals(i.FullName, Path, StringComparison.OrdinalIgnoreCase) && EntityResolutionHelper.IsVideoFile(i.FullName) && EntityResolutionHelper.IsMultiPartFile(i.FullName);
});
return LibraryManager.ResolvePaths<Video>(files, null).Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = LibraryManager.RetrieveItem(video.Id) as Video;
if (dbItem != null)
{
dbItem.ResolveArgs = video.ResolveArgs;
video = dbItem;
}
return video;
}).ToList();
}
}
}

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Entities;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using System;
using System.Collections.Generic;
@ -45,6 +46,20 @@ namespace MediaBrowser.Controller.Resolvers
".mts"
};
private static readonly Regex MultiFileRegex = new Regex(
@"(.*?)([ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck]|d)[ _.-]*[0-9]+)(.*?)(\.[^.]+)$",
RegexOptions.Compiled);
/// <summary>
/// Determines whether [is multi part file] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [is multi part file] [the specified path]; otherwise, <c>false</c>.</returns>
public static bool IsMultiPartFile(string path)
{
return MultiFileRegex.Match(path).Success;
}
/// <summary>
/// The audio file extensions
/// </summary>

View File

@ -348,6 +348,12 @@ namespace MediaBrowser.Model.Dto
/// <value>The display type of the media.</value>
public string DisplayMediaType { get; set; }
/// <summary>
/// Gets or sets the part count.
/// </summary>
/// <value>The part count.</value>
public int? PartCount { get; set; }
/// <summary>
/// Determines whether the specified type is type.
/// </summary>

View File

@ -8,6 +8,7 @@ using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
{
@ -196,10 +197,41 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
}
}
// If there are multiple video files, return null, and let the VideoResolver catch them later as plain videos
if (movies.Count > 1)
{
return GetMultiFileMovie(movies);
}
return movies.Count == 1 ? movies[0] : null;
}
/// <summary>
/// Gets the multi file movie.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="movies">The movies.</param>
/// <returns>``0.</returns>
private T GetMultiFileMovie<T>(List<T> movies)
where T : Video, new()
{
var multiPartMovies = movies.OrderBy(i => i.Path)
.Where(i => EntityResolutionHelper.IsMultiPartFile(i.Path))
.ToList();
// They must all be part of the sequence
if (multiPartMovies.Count != movies.Count)
{
return null;
}
var firstPart = multiPartMovies[0];
firstPart.IsMultiPart = true;
return firstPart;
}
/// <summary>
/// Determines whether [is DVD directory] [the specified directory name].
/// </summary>
@ -209,6 +241,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
{
return directoryName.Equals("video_ts", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether [is hd DVD directory] [the specified directory name].
/// </summary>

View File

@ -142,11 +142,17 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
var video = item as Video;
if (video != null && video.Chapters != null)
if (video != null)
{
if (video.Chapters != null)
{
images = images.Concat(video.Chapters.Where(i => !string.IsNullOrEmpty(i.ImagePath)).Select(i => i.ImagePath));
}
var additionalParts = _itemRepo.GetItems(video.AdditionalPartIds).ToList();
images = additionalParts.Aggregate(images, (current, subItem) => current.Concat(GetPathsInUse(subItem)));
}
var movie = item as Movie;
if (movie != null)

View File

@ -222,6 +222,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
items.AddRange(themeVideos);
items.AddRange(videos.SelectMany(i => _itemRepo.GetItems(i.AdditionalPartIds).Cast<Video>()).ToList());
items.AddRange(videos.OfType<Movie>().SelectMany(i => _itemRepo.GetItems(i.SpecialFeatureIds).Cast<Video>()).ToList());
return items.Where(i =>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -6,8 +6,8 @@
<ProjectGuid>{E22BFD35-0FCD-4A85-978A-C22DCD73A081}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MediaBrowser.Specs</RootNamespace>
<AssemblyName>MediaBrowser.Specs</AssemblyName>
<RootNamespace>MediaBrowser.Tests</RootNamespace>
<AssemblyName>MediaBrowser.Tests</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
@ -50,7 +50,8 @@
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="Controller\Library\TvUtilTests.cs" />
<Compile Include="Resolvers\MovieResolverTests.cs" />
<Compile Include="Resolvers\TvUtilTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
@ -58,6 +59,10 @@
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
<Name>MediaBrowser.Controller</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
<Name>MediaBrowser.Model</Name>
</ProjectReference>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">

View File

@ -0,0 +1,30 @@
using MediaBrowser.Controller.Resolvers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MediaBrowser.Tests.Resolvers
{
[TestClass]
public class MovieResolverTests
{
[TestMethod]
public void TestMultiPartFiles()
{
Assert.IsFalse(EntityResolutionHelper.IsMultiPartFile(@"blah blah.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - cd1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disc1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disk1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - pt1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - part1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - dvd1.mkv"));
// Add a space
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - cd 1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disc 1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - disk 1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - pt 1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - part 1.mkv"));
Assert.IsTrue(EntityResolutionHelper.IsMultiPartFile(@"blah blah - dvd 1.mkv"));
}
}
}

View File

@ -1,7 +1,7 @@
using MediaBrowser.Controller.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MediaBrowser.Tests.Controller.Library
namespace MediaBrowser.Tests.Resolvers
{
[TestClass]
public class TvUtilTests

View File

@ -2047,6 +2047,27 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
});
};
self.getAdditionalVideoParts = function (userId, itemId) {
if (!itemId) {
throw new Error("null itemId");
}
var options = {};
if (userId) {
options.userId = userId;
}
var url = self.getUrl("Videos/" + itemId + "/AdditionalParts", options);
return self.ajax({
type: "GET",
url: url,
dataType: "json"
});
};
/**
* Gets theme songs for an item
*/

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MediaBrowser.ApiClient.Javascript" version="3.0.123" targetFramework="net45" />
<package id="MediaBrowser.ApiClient.Javascript" version="3.0.124" targetFramework="net45" />
<package id="ServiceStack.Common" version="3.9.46" targetFramework="net45" />
<package id="ServiceStack.Text" version="3.9.45" targetFramework="net45" />
</packages>