private string MovieDownloadDirectory(ITorrentClientTorrent torrent, out string torrentFileNameExtension) { torrentFileNameExtension = null; var fileName = Regexes.FileNamePattern.Match(torrent.Name); if (fileName.Success) { torrentFileNameExtension = fileName.Groups["FileExtension"].Value; } return(MovieDownloadDirectory(torrent)); }
/// <summary> /// Cleanup method to be called after succesfull download, transcoding and manipulation of files. /// </summary> protected async Task MovieDownloadedSuccesfulyAsync(ITorrentClientTorrent torrent, Movie movie) { try { _logger.LogDebug($"Will delete: {MovieDownloadDirectory(torrent)}"); #if RELEASE Directory.Delete(MovieDownloadDirectory(torrent), true); #endif } catch (IOException _) { await _movieService.SetMovieStatus(movie.Id, MovieStatusEnum.CouldNotDeleteDownloadDirectory); } _logger.LogDebug($"Will remove torrent: {torrent.Id}"); #if RELEASE await _torrentClient.RemoveTorrent(torrent.Id); #endif await _movieService.SetMovieStatus(movie.Id, MovieStatusEnum.Finished); // Inform Minidlna or other services about the new download. if (!string.IsNullOrWhiteSpace(_configuration.FinishedCommandExecutable)) { var process = new Process() { StartInfo = new ProcessStartInfo { FileName = _configuration.FinishedCommandExecutable, Arguments = _configuration.FinishedCommandArguments, UseShellExecute = false, CreateNoWindow = true } }; await Task.Run(() => { process.Start(); process.WaitForExit(); }); } }
private string MovieDownloadDirectory(ITorrentClientTorrent torrent) { var fileName = Regexes.FileNamePattern.Match(torrent.Name); return(Path.Combine(_configuration.MoviesDownloadDir, fileName.Success ? fileName.Groups["FileName"].Value : torrent.Name)); }
private async Task MoveVideoFilesToFinishedDir(Movie movie, ITorrentClientTorrent torrent, string[] videoFilesToMove, string coverImageJpg = null) { if (movie == null) { throw new ArgumentNullException(nameof(movie)); } if (torrent == null) { throw new ArgumentNullException(nameof(torrent)); } if (videoFilesToMove == null || !videoFilesToMove.Any()) { _logger.LogError($"Cannot move video files: {nameof(videoFilesToMove)} was empty."); throw new ArgumentNullException(nameof(videoFilesToMove)); } _logger.LogWarning($"Moving video files:\n{string.Join("\n ", videoFilesToMove)}"); var finishedMovieDirectory = MovieFinishedDirectory(movie); if (!Directory.Exists(finishedMovieDirectory)) { Directory.CreateDirectory(finishedMovieDirectory); } // Move existing Cover image, or download a new one if null. var finishedCoverImageJpg = Path.Combine(finishedMovieDirectory, "Cover.jpg"); try { if (!File.Exists(finishedCoverImageJpg)) { if (coverImageJpg != null && File.Exists(coverImageJpg)) { File.Move(coverImageJpg, finishedCoverImageJpg); } else { try { await Download.GetFile(movie.ImageUrl, finishedCoverImageJpg); } catch (Exception ex) { finishedCoverImageJpg = null; _logger.LogError($"Could not create a Cover image. {ex.Message}", ex); } } if (finishedCoverImageJpg != null) { await WindowsFolder.SetFolderPictureAsync(finishedCoverImageJpg); File.SetAttributes(finishedCoverImageJpg, File.GetAttributes(finishedCoverImageJpg) | FileAttributes.Hidden); } } } catch (Exception e) { _logger.LogInformation($"Could create a cover image: {e.Message}.", e); } try { // Here we're assuming that the first, largest file will be the main movie file. var mainMovieFile = videoFilesToMove[0]; var destinationFileNameWithoutExtension = Regexes.FileSystemSafeName.Replace($"{movie.Year} {movie.Title}", string.Empty); _logger.LogWarning($"Moving MAIN movie file: {mainMovieFile} to {Path.Combine(finishedMovieDirectory, destinationFileNameWithoutExtension + Regexes.FileNamePattern.Match(mainMovieFile).Groups["FileExtension"].Value)}"); File.Move(mainMovieFile, Path.Combine(finishedMovieDirectory, destinationFileNameWithoutExtension + Regexes.FileNamePattern.Match(mainMovieFile).Groups["FileExtension"].Value)); if (videoFilesToMove.Length > 1) { foreach (var videoFile in videoFilesToMove.Skip(1)) { _logger.LogInformation($"Moving video file: {videoFile} to {Path.Combine(finishedMovieDirectory, Path.GetFileName(videoFile))}"); File.Move(videoFile, Path.Combine(finishedMovieDirectory, Path.GetFileName(videoFile))); } } } catch (Exception e) { await _movieService.SetMovieStatus(movie.Id, MovieStatusEnum.FileInUse); _logger.LogDebug($"Will delete torrent: {torrent.Id}"); #if RELEASE await _torrentClient.RemoveTorrent(torrent.Id); #endif _logger.LogInformation($"Could not move the final downloaded files: {e.Message}. Is FFMPEG still running?", e); } }
// TODO: Split into Analyse and Transcode, so the Analysis part doesn't get called when run from Queued private async Task <TranscodeResult> Transcode(int movieJobId, ITorrentClientTorrent torrent, CancellationToken cancellationToken) { var sourcePath = MovieDownloadDirectory(torrent); var videoFiles = GetVideoFilesInDir(sourcePath); if (videoFiles == null || videoFiles.Length == 0) { await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.TranscodingError); _logger.LogWarning($"{torrent.Name} has no compatible video files."); return(new TranscodeResult(TranscodeResultEnum.NoVideoFiles)); } var movie = (await _movieService.GetMoviesAsync()).Where(x => x.Id == movieJobId).FirstOrDefault(); await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.AnalysisStarted); var pendingTranscodingJobs = new List <ITranscodingJob>(); var filesToMove = new List <string>(); // Check if any files need transcoding foreach (var videoFile in videoFiles) { IMediaInfo mediaInfo = null; _logger.LogInformation($"Analyzing {Path.GetFileName(videoFile)}"); try { using (var encoder = new FFMPEGEncoderService(_loggerEnc, _encoderConfiguration, _sofakingConfiguration)) { mediaInfo = await encoder.GetMediaInfo(videoFile); } } catch (Exception ex) { _logger.LogError($"{nameof(FFMPEGEncoderService.GetMediaInfo)} failed with: {ex.Message}", ex); } if (mediaInfo == null) { _logger.LogWarning($"File {Path.GetFileName(videoFile)} returned no {nameof(mediaInfo)}"); continue; } var flags = EncodingTargetFlags.None; try { if (!HasAcceptableVideo(mediaInfo)) { flags |= EncodingTargetFlags.NeedsNewVideo; } } catch (ArgumentException ex) { _logger.LogError($"{Path.GetFileName(videoFile)}", ex); _logger.LogError($"{ex.ParamName}: {ex.Message}"); await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.AnalysisVideoFailed); continue; } try { if (!HasAcceptableAudio(mediaInfo)) { flags |= EncodingTargetFlags.NeedsNewAudio; } } catch (ArgumentException ex) { _logger.LogError($"{Path.GetFileName(videoFile)}", ex); _logger.LogError($"{ex.ParamName}: {ex.Message}"); await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.AnalysisAudioFailed); continue; } if (movie.Genres.HasFlag(GenreFlags.Animation)) { flags |= EncodingTargetFlags.VideoIsAnimation; } if (flags == EncodingTargetFlags.None) { _logger.LogInformation($"Video file {videoFile} doesn't need transcoding, adding to files to move."); filesToMove.Add(videoFile); continue; } _logger.LogInformation($"Adding {videoFile} to transcoding jobs."); pendingTranscodingJobs.Add(new TranscodingJob { SourceFile = videoFile, Action = flags, Duration = mediaInfo.Duration, CancellationToken = cancellationToken, Metadata = new Dictionary <FFMPEGMetadataEnum, string> { { FFMPEGMetadataEnum.title, movie.Title }, { FFMPEGMetadataEnum.year, movie.Year.ToString() }, { FFMPEGMetadataEnum.director, movie.Director }, { FFMPEGMetadataEnum.description, movie.Description }, { FFMPEGMetadataEnum.episode_id, movie.EpisodeId }, { FFMPEGMetadataEnum.IMDBRating, movie.ImdbScore.ToString() }, { FFMPEGMetadataEnum.genre, movie.Genres.ToString() }, { FFMPEGMetadataEnum.show, movie.Show } } }); } // No transcoding needed for any video files in the folder // Using this pendingTranscodingJobsList allows us to return for movies that need no transcoding, so they won't wait for the transcoding to be complete and can be simply copied over to the resulting folder. if (!pendingTranscodingJobs.Any()) { _logger.LogInformation($"Transcoding not needed, no pending jobs."); return(new TranscodeResult(TranscodeResultEnum.TranscodingNotNeeded, filesToMove.ToArray())); } // If something else is transcoding, then queue var tjmem = _transcodingJobs.Any(); var tjdb = (await _movieService.GetMoviesAsync()).Where(x => x.Status == MovieStatusEnum.TranscodingStarted).Any(); if (tjmem || tjdb) { _logger.LogInformation($"Queuing {movie.TorrentName}"); await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.TranscodingQueued); return(new TranscodeResult(TranscodeResultEnum.Queued)); } // Set the a cover image for the first file (the largest = the movie), so it will be attached to the output file. string coverImageJpg = null; try { coverImageJpg = Path.Combine(_sofakingConfiguration.TempFolder, movie.Id + "-Cover.jpg"); await Download.GetFile(movie.ImageUrl, coverImageJpg); pendingTranscodingJobs[0].CoverImageJpg = coverImageJpg; } catch (Exception ex) { coverImageJpg = null; _logger.LogError($"Could not download a Cover image. {ex.Message}", ex); } // Start transcoding by copying our pending tasks into the static global queue _logger.LogInformation($"Setting movie status to TranscodingStarted."); await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.TranscodingStarted); foreach (var transcodingJob in pendingTranscodingJobs) { int attempts = 0; bool success = false; while (attempts <= 10) { var id = _transcodingJobs.IsEmpty ? 0 : _transcodingJobs.Select(x => x.Key).Max() + 1; _logger.LogInformation($"Trying to add transconding job {id} to the global queue"); if (_transcodingJobs.TryAdd(id, transcodingJob)) { success = true; break; } attempts++; Thread.Sleep(TimeSpan.FromSeconds(3)); } if (!success) { _logger.LogError($"Couldn't add transcoding job {transcodingJob.SourceFile} to the global queue."); } } // Get the first job from the stack, then drop it when done _ = Task.Run(async() => { while (_transcodingJobs.Any() && _transcodingJobs.TryGetValue(_transcodingJobs.First().Key, out var transcodingJob)) { if (_encoderTranscodingInstance != null) { continue; } try { // Do this as the first thing, so no other encoding gets started _encoderTranscodingInstance = new FFMPEGEncoderService(_loggerEnc, _encoderConfiguration, _sofakingConfiguration); _logger.LogWarning($"Preparing transcoding of {transcodingJob.SourceFile}"); Action FirstOut = () => { if (!_transcodingJobs.Any()) { _logger.LogInformation("No transcoding jobs left to remove"); return; } var removed = _transcodingJobs.TryRemove(_transcodingJobs.First().Key, out _); _logger.LogWarning($"Removing first from the queue, result: {removed}."); }; // TODO: Use a factory _encoderTranscodingInstance.OnStart += (object sender, EventArgs e) => { _logger.LogInformation("Transcoding started"); }; _encoderTranscodingInstance.OnProgress += (object sender, EncodingProgressEventArgs e) => { _logger.LogDebug($"Transcoding progress: {e.ProgressPercent:0.##}%"); }; _encoderTranscodingInstance.OnError += async(object sender, EncodingErrorEventArgs e) => { FirstOut(); _encoderTranscodingInstance.Dispose(); _encoderTranscodingInstance = null; if (_transcodingJobs.Count == 0) { await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.TranscodingError); } _logger.LogInformation($"Transcoding failed: {e.Error}"); }; _encoderTranscodingInstance.OnCancelled += async(object sender, EventArgs e) => { FirstOut(); _encoderTranscodingInstance.Dispose(); _encoderTranscodingInstance = null; if (_transcodingJobs.Count == 0) { await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.TranscodingCancelled); } _logger.LogInformation("Transcoding cancelled"); }; _encoderTranscodingInstance.OnSuccess += async(object sender, EncodingSuccessEventArgs e) => { FirstOut(); _logger.LogWarning($"Adding {e.FinishedFile} to the list of files to move ({filesToMove.Count()})"); filesToMove.Add(e.FinishedFile); if (_transcodingJobs.Count == 0) { _logger.LogWarning("All transcoding done."); await MoveVideoFilesToFinishedDir(movie, torrent, filesToMove.ToArray(), coverImageJpg); await MovieDownloadedSuccesfulyAsync(torrent, movie); } // Do this as the last thing, so no other encoding gets started _encoderTranscodingInstance.Kill(); // Note: .Dispose here leads to "Broken pipe" Exception _encoderTranscodingInstance.CleanTempData(); _encoderTranscodingInstance = null; }; _logger.LogWarning($"Starting transcoding of {transcodingJob.SourceFile}"); await _encoderTranscodingInstance.StartTranscodingAsync(transcodingJob, cancellationToken); } catch (Exception e) { if (_transcodingJobs.Any()) { var removed = _transcodingJobs.TryRemove(_transcodingJobs.First().Key, out _); _logger.LogWarning($"Removing first from the queue, result: {removed}."); } await _movieService.SetMovieStatus(movieJobId, MovieStatusEnum.TranscodingError); _logger.LogError(e.Message); _encoderTranscodingInstance.DisposeAndKeepFiles(); _encoderTranscodingInstance = null; } } }); return(new TranscodeResult(TranscodeResultEnum.Transcoding, filesToMove.ToArray())); }