using MediaBrowser.Common.IO; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.Serialization; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Net; using MediaBrowser.Model.Tasks; using MediaBrowser.Plugins.Trailers.Entities; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Plugins.Trailers.ScheduledTasks { /// /// Downloads trailers from the web at scheduled times /// [Export(typeof(IScheduledTask))] public class CurrentTrailerDownloadTask : BaseScheduledTask { /// /// Creates the triggers that define when the task will run /// /// IEnumerable{BaseTaskTrigger}. protected override IEnumerable GetDefaultTriggers() { var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }; //2am return new[] { trigger }; } /// /// Returns the task to be executed /// /// The cancellation token. /// The progress. /// Task. protected override async Task ExecuteInternal(CancellationToken cancellationToken, IProgress progress) { // Get the list of trailers var trailers = await AppleTrailerListingDownloader.GetTrailerList(cancellationToken).ConfigureAwait(false); progress.Report(new TaskProgress { PercentComplete = 1 }); var trailersToDownload = trailers.Where(t => !IsOldTrailer(t.Video)).ToList(); cancellationToken.ThrowIfCancellationRequested(); var numComplete = 0; // Fetch them all in parallel var tasks = trailersToDownload.Select(t => Task.Run(async () => { cancellationToken.ThrowIfCancellationRequested(); try { await DownloadTrailer(t, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Logger.ErrorException("Error downloading {0}", ex, t.TrailerUrl); } // Update progress lock (progress) { numComplete++; double percent = numComplete; percent /= trailersToDownload.Count; // Leave 1% for DeleteOldTrailers progress.Report(new TaskProgress { PercentComplete = (99 * percent) + 1 }); } })); cancellationToken.ThrowIfCancellationRequested(); await Task.WhenAll(tasks).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (Plugin.Instance.Configuration.DeleteOldTrailers) { // Enforce MaxTrailerAge DeleteOldTrailers(); } progress.Report(new TaskProgress { PercentComplete = 100 }); } /// /// Downloads a single trailer into the trailers directory /// /// The trailer. /// The cancellation token. /// Task. private async Task DownloadTrailer(TrailerInfo trailer, CancellationToken cancellationToken) { // Construct the trailer foldername var folderName = FileSystem.GetValidFilename(trailer.Video.Name); if (trailer.Video.ProductionYear.HasValue) { folderName += string.Format(" ({0})", trailer.Video.ProductionYear); } var folderPath = Path.Combine(Plugin.Instance.DownloadPath, folderName); // Figure out which image we're going to download var imageUrl = trailer.HdImageUrl ?? trailer.ImageUrl; // Construct the video filename (to match the folder name) var videoFileName = Path.ChangeExtension(folderName, Path.GetExtension(trailer.TrailerUrl)); // Construct the image filename (folder + original extension) var imageFileName = Path.ChangeExtension("folder", Path.GetExtension(imageUrl)); // Construct full paths var videoFilePath = Path.Combine(folderPath, videoFileName); var imageFilePath = Path.Combine(folderPath, imageFileName); // Create tasks to download each of them, if we don't already have them Task videoTask = null; Task imageTask = null; var tasks = new List(); if (!File.Exists(videoFilePath)) { Logger.Info("Downloading trailer: " + trailer.TrailerUrl); // Fetch the video to a temp file because it's too big to put into a MemoryStream videoTask = Kernel.HttpManager.FetchToTempFile(trailer.TrailerUrl, Kernel.ResourcePools.AppleTrailerVideos, cancellationToken, new Progress { }, "QuickTime/7.6.2"); tasks.Add(videoTask); } if (!string.IsNullOrWhiteSpace(imageUrl) && !File.Exists(imageFilePath)) { // Fetch the image to a memory stream Logger.Info("Downloading trailer image: " + imageUrl); imageTask = Kernel.HttpManager.FetchToMemoryStream(imageUrl, Kernel.ResourcePools.AppleTrailerImages, cancellationToken); tasks.Add(imageTask); } try { // Wait for both downloads to finish await Task.WhenAll(tasks).ConfigureAwait(false); } catch (HttpException ex) { Logger.ErrorException("Error downloading trailer file or image", ex); } var videoFailed = false; var directoryEnsured = false; // Proces the video file task result if (videoTask != null) { if (videoTask.Status == TaskStatus.RanToCompletion) { EnsureDirectory(folderPath); directoryEnsured = true; // Move the temp file to the final destination try { File.Move(videoTask.Result, videoFilePath); } catch (IOException ex) { Logger.ErrorException("Error moving temp file", ex); File.Delete(videoTask.Result); videoFailed = true; } } else { Logger.Info("Trailer download failed: " + trailer.TrailerUrl); // Don't bother with the image if the video download failed videoFailed = true; } } // Process the image file task result if (imageTask != null && !videoFailed && imageTask.Status == TaskStatus.RanToCompletion) { if (!directoryEnsured) { EnsureDirectory(folderPath); } try { // Save the image to the file system using (var fs = new FileStream(imageFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { using (var sourceStream = imageTask.Result) { await sourceStream.CopyToAsync(fs).ConfigureAwait(false); } } } catch (IOException ex) { Logger.ErrorException("Error saving image to file system", ex); } } // Save metadata only if the video was downloaded if (!videoFailed && videoTask != null) { JsonSerializer.SerializeToFile(trailer.Video, Path.Combine(folderPath, "trailer.json")); } } /// /// Determines whether [is old trailer] [the specified trailer]. /// /// The trailer. /// true if [is old trailer] [the specified trailer]; otherwise, false. private bool IsOldTrailer(Trailer trailer) { if (!Plugin.Instance.Configuration.MaxTrailerAge.HasValue) { return false; } if (!trailer.PremiereDate.HasValue) { return false; } var now = DateTime.UtcNow; // Not old if it still hasn't premiered. if (now < trailer.PremiereDate.Value) { return false; } return (DateTime.UtcNow - trailer.PremiereDate.Value).TotalDays > Plugin.Instance.Configuration.MaxTrailerAge.Value; } /// /// Deletes trailers that are older than the supplied date /// private void DeleteOldTrailers() { var collectionFolder = (Folder)Kernel.RootFolder.Children.First(c => c.GetType().Name.Equals(typeof(TrailerCollectionFolder).Name)); foreach (var trailer in collectionFolder.RecursiveChildren.OfType().Where(IsOldTrailer)) { Logger.Info("Deleting old trailer: " + trailer.Name); Directory.Delete(Path.GetDirectoryName(trailer.Path), true); } } /// /// Ensures the directory. /// /// The path. private void EnsureDirectory(string path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } } /// /// Gets the name of the task /// /// The name. public override string Name { get { return "Find current trailers"; } } /// /// Gets the category. /// /// The category. public override string Category { get { return "Trailers"; } } /// /// Gets the description. /// /// The description. public override string Description { get { return "Searches the web for upcoming movie trailers, and downloads them based on your Trailer plugin settings."; } } } }