Ejemplo n.º 1
0
        // 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()));
        }
Ejemplo n.º 2
0
        static async Task Main(string[] args)
        {
            if (args.Length == 0)
            {
                throw new Exception("No file to convert provided");
            }

            Console.WriteLine("Encoding: " + args[0]);

            var builder = new ConfigurationBuilder()
                          .SetBasePath(Directory.GetCurrentDirectory())
                          .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                          .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true) // TODO: Change the Production with Enviroment
                          .AddEnvironmentVariables();

            IConfigurationRoot configuration = builder.Build();
            var encoderConfiguration         = new EncoderConfiguration();

            configuration.GetSection("Encoder").Bind(encoderConfiguration);
            var sofakingConfiguration = new SoFakingConfiguration();

            configuration.GetSection("Sofaking").Bind(sofakingConfiguration);
            var dc = new DConf();

            configuration.GetSection("DownloadFinishedWorker").Bind(dc);

            var builder1 = new HostBuilder()
                           .ConfigureServices((hostContext, services) =>
            {
                services
                .AddSingleton(sofakingConfiguration)
                .AddSingleton(encoderConfiguration)
                .AddSingleton(new SoFakingContextFactory());
            }).UseConsoleLifetime();

            var host = builder1.Build();

            using (var serviceScope = host.Services.CreateScope())
            {
                var        done            = false;
                var        serviceProvider = serviceScope.ServiceProvider;
                var        logger          = new NullLogger <FFMPEGEncoderService>();
                var        flags           = EncodingTargetFlags.None;
                var        videoFile       = args[0];
                IMediaInfo mediaInfo       = null;
                var        useCuda         = true;

                Console.Clear();
                Console.WriteLine($"Is this a film (vs. cartoon)? [y/n]");
                if (Console.ReadKey().KeyChar == 'n')
                {
                    flags |= EncodingTargetFlags.VideoIsAnimation;
                }

                var encoderTranscodingInstance = new FFMPEGEncoderService(logger, encoderConfiguration, sofakingConfiguration);

                try
                {
                    mediaInfo = await encoderTranscodingInstance.GetMediaInfo(videoFile);
                }
                catch (Exception ex)
                {
                    Console.Clear();
                    Console.WriteLine("Can not read media info: " + ex.Message);
                    Console.ReadKey();
                    return;
                }

                // Find subtitles
                var filenameBase = Path.GetFileNameWithoutExtension(videoFile);
                var path         = Path.GetDirectoryName(videoFile);
                var subtitles    = new Dictionary <string, string>();

                foreach (var sl in sofakingConfiguration.SubtitleLanguages)
                {
                    var subPath = Path.Combine(path, filenameBase + $".{sl}.srt");
                    if (File.Exists(subPath))
                    {
                        subtitles.Add(sl, subPath);
                    }
                }

                if (subtitles.Count > 0)
                {
                    Console.Clear();
                    Console.WriteLine($"Found {subtitles.Count} subtitles to embed.");
                    Thread.Sleep(3 * 1000);

                    flags |= EncodingTargetFlags.ExternalSubtitles;
                }


                try
                {
                    var videoAudit = HasAcceptableVideo(dc, sofakingConfiguration, mediaInfo);

                    if (videoAudit != VideoCompatibilityFlags.Compatible)
                    {
                        Console.Clear();
                        Console.WriteLine("Video details:");
                        if (videoAudit.HasFlag(VideoCompatibilityFlags.IncompatibleCodec))
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                        }
                        Console.WriteLine($"   Codec: {mediaInfo.VideoCodec}, (Accepted: {string.Join(", ", dc.AcceptedVideoCodecs)})");
                        Console.ResetColor();
                        if (videoAudit.HasFlag(VideoCompatibilityFlags.IncompatibleResolution))
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                        }
                        Console.WriteLine($"   H. Resolution: {mediaInfo.HorizontalVideoResolution}px, (Max: {sofakingConfiguration.MaxHorizontalVideoResolution}px)");
                        Console.ResetColor();
                        if (videoAudit.HasFlag(VideoCompatibilityFlags.IncompatibleBitrate))
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                        }
                        Console.WriteLine($"   Avg bitrate: {(mediaInfo.AVBitrateKbs.HasValue ? ByteSize.FromKiloBytes(mediaInfo.AVBitrateKbs.Value).ToString() : "?")}/s, (Max: {ByteSize.FromKiloBytes(TargetVideoBitrateKbs)}/s");
                        Console.ResetColor();
                        if (videoAudit.HasFlag(VideoCompatibilityFlags.IncompatibleSize))
                        {
                            Console.ForegroundColor = ConsoleColor.Red;
                        }
                        Console.WriteLine($"   Size: {ByteSize.FromBytes(mediaInfo.FileInfo.Length)} (Max: {ByteSize.FromGigaBytes(sofakingConfiguration.MaxSizeGb)})");
                        Console.ResetColor();
                        Console.WriteLine();

                        Console.WriteLine($"Video needs converting. Continue? [y/n]");
                        if (Console.ReadKey().KeyChar == 'n')
                        {
                            return;
                        }

                        if (encoderConfiguration.CanUseCuda)
                        {
                            Console.Clear();
                            Console.WriteLine($"Use CUDA? [y/n]");
                            if (Console.ReadKey().KeyChar == 'n')
                            {
                                useCuda = false;;
                            }
                        }

                        flags |= EncodingTargetFlags.NeedsNewVideo;
                    }
                }
                catch (ArgumentException ex)
                {
                    Console.Clear();
                    Console.WriteLine("Incompatible video: " + ex.Message);
                    Console.ReadKey();
                    return;
                }

                try
                {
                    if (!HasAcceptableAudio(dc, mediaInfo))
                    {
                        Console.Clear();
                        Console.WriteLine($"Audio ({mediaInfo.AudioCodec}, (Accepted: {string.Join(", ", dc.AcceptedAudioCodecs)})) needs converting. Continue? [y/n]");
                        if (Console.ReadKey().KeyChar == 'n')
                        {
                            return;
                        }

                        flags |= EncodingTargetFlags.NeedsNewAudio;
                    }
                }
                catch (ArgumentException ex)
                {
                    Console.Clear();
                    Console.WriteLine("Incompatible audio: " + ex.Message);
                    Console.ReadKey();
                    return;
                }

                if (flags == EncodingTargetFlags.None)
                {
                    Console.Clear();
                    Console.WriteLine($"Video file {videoFile} doesn't need transcoding, adding to files to move.");
                    Console.ReadKey();
                    return;
                }

                Console.Clear();
                Console.WriteLine("Converting...");

                encoderTranscodingInstance.OnSuccess += (object sender, EncodingSuccessEventArgs e) => {
                    done = true;
                };

                encoderTranscodingInstance.OnProgress += (object sender, EncodingProgressEventArgs e) => {
                    Console.Clear();
                    Console.WriteLine($"Transcoding progress: {e.ProgressPercent:0.##}%");
                };

                // non-blocking, only starts the external engine
                await encoderTranscodingInstance.StartTranscodingAsync(new TranscodingJob
                {
                    SourceFile = videoFile,
                    Action     = flags,
                    Duration   = mediaInfo.Duration,
                    Subtitles  = subtitles,
                    UseCuda    = useCuda
                });

                while (!done)
                {
                    // wait until encoding finished
                }

                Console.Clear();
                Console.WriteLine("Done!");
                Thread.Sleep(3 * 1000);
            }
        }