/// <summary>
        ///     Decrypts the selected file.
        /// </summary>
        private async Task DecryptFileAsync(string srcFile, string destFile)
        {
            if (string.IsNullOrWhiteSpace(srcFile) || !File.Exists(srcFile))
            {
                _loggingService.Log(LogLevel.Warning, $"Invalid source file {srcFile}, skipping...");
                return;
            }
            await DecryptionHelper.DecryptFileAsync(srcFile, destFile).ConfigureAwait(false);

            _loggingService.Log(LogLevel.Information, $"Decrypted clip {Path.GetFileName(destFile)}.");
        }
        public async Task StartAsync(int?maxDegreeOfParallelism = null)
        {
            int threads = maxDegreeOfParallelism ?? Environment.ProcessorCount;

            using (var db = new PsvContext(_psvInformation))
            {
                IEnumerable <Course> courses = db.Courses;
                foreach (var course in courses)
                {
                    _loggingService.Log(LogLevel.Information, $"Processing course \"{course.Name}\"...");
                    // Checks
                    string courseSource = Path.Combine(_psvInformation.CoursesPath, course.Name);
                    string courseOutput = Path.Combine(_psvInformation.Output,
                                                       _stringProcessor.SanitizeTitle(course.Title));
                    if (!Directory.Exists(courseSource))
                    {
                        _loggingService.Log(LogLevel.Warning,
                                            $"Courses directory for \"{course.Name}\" not found. Skipping...");
                        continue;
                    }

                    if (!Directory.Exists(courseOutput))
                    {
                        courseOutput = Directory.CreateDirectory(courseOutput).FullName;
                    }

                    // Course image copy
                    await CopyCourseImageAsync(courseSource, courseOutput).ConfigureAwait(false);

                    // Write course info
                    await WriteCourseInfoAsync(course, courseOutput).ConfigureAwait(false);

                    var modules = await db.Modules.Where(x => x.CourseName == course.Name).ToListAsync()
                                  .ConfigureAwait(false);

                    _loggingService.Log(LogLevel.Information,
                                        $"Found {modules.Count} modules under course \"{course.Name}\"...");

                    foreach (var module in modules)
                    {
                        // Preps
                        _loggingService.Log(LogLevel.Information, $"Processing module: {module.Name}...");
                        string moduleHash = await Task
                                            .Run(() => DecryptionHelper.GetModuleHash(module.Name, module.AuthorHandle))
                                            .ConfigureAwait(false);

                        string moduleOutput = Path.Combine(courseOutput,
                                                           $"{_stringProcessor.TitleToFileIndex(module.ModuleIndex)}. {_stringProcessor.SanitizeTitle(module.Title)}");
                        string moduleSource = Path.Combine(courseSource, moduleHash);
                        if (!Directory.Exists(moduleOutput))
                        {
                            moduleOutput = Directory.CreateDirectory(moduleOutput).FullName;
                        }

                        // Write module info
                        await WriteModuleInfoAsync(module, moduleOutput).ConfigureAwait(false);

                        // Process each clip
                        var clips = await db.Clips.Where(x => x.ModuleId == module.Id).ToListAsync()
                                    .ConfigureAwait(false);

                        // Bail if no courses are found in database
                        if (clips.Count == 0)
                        {
                            _loggingService.Log(LogLevel.Warning,
                                                $"No corresponding clips found for module {module.Name}, skipping...");
                            continue;
                        }

                        // Write clip info
                        await WriteClipInfoAsync(clips, moduleOutput).ConfigureAwait(false);

                        await clips.ParallelForEachAsync(async clip =>
                        {
                            string clipSource = Path.Combine(moduleSource, $"{clip.Name}.psv");
                            string clipName   =
                                $"{_stringProcessor.TitleToFileIndex(clip.ClipIndex)}. {_stringProcessor.SanitizeTitle(clip.Title)}";
                            string clipFilePath = Path.Combine(moduleOutput, $"{clipName}.mp4");

                            // Decrypt individual clip
                            await DecryptFileAsync(clipSource, clipFilePath).ConfigureAwait(false);

                            // Create subtitles for each clip
                            if (course.HasTranscript != true)
                            {
                                return;
                            }
                            using (var psvContext = new PsvContext(_psvInformation))
                            {
                                var transcripts = psvContext.ClipTranscripts.Where(x => x.ClipId == clip.Id);
                                if (!await transcripts.AnyAsync().ConfigureAwait(false))
                                {
                                    return;
                                }
                                await BuildSubtitlesAsync(transcripts, moduleOutput, clipName).ConfigureAwait(false);
                            }
                        }, threads).ConfigureAwait(false);
                    }
                }
            }
        }