// --------------------------------------------------------------------------------------------------------------------
//
// This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License.
//
//
// The Chapters View Model
//
// --------------------------------------------------------------------------------------------------------------------
namespace HandBrakeWPF.ViewModels
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using Caliburn.Micro;
using HandBrakeWPF.EventArgs;
using HandBrakeWPF.Properties;
using HandBrakeWPF.Services.Interfaces;
using HandBrakeWPF.Services.Presets.Model;
using HandBrakeWPF.Services.Scan.Model;
using HandBrakeWPF.Utilities.Input;
using HandBrakeWPF.Utilities.Output;
using HandBrakeWPF.ViewModels.Interfaces;
using ChapterMarker = HandBrakeWPF.Services.Encode.Model.Models.ChapterMarker;
using EncodeTask = HandBrakeWPF.Services.Encode.Model.EncodeTask;
using GeneralApplicationException = HandBrakeWPF.Exceptions.GeneralApplicationException;
///
/// The Chapters View Model
///
public class ChaptersViewModel : ViewModelBase, IChaptersViewModel
{
#region Constants and Fields
private readonly IErrorService errorService;
///
/// The source chapters backing field
///
private List sourceChaptersList;
#endregion
#region Constructors and Destructors
///
/// Initializes a new instance of the class.
///
///
/// The window manager.
///
///
/// The user Setting Service.
///
///
/// The Error Service
///
public ChaptersViewModel(IWindowManager windowManager, IUserSettingService userSettingService, IErrorService errorService)
{
this.Task = new EncodeTask();
this.errorService = errorService;
}
public event EventHandler TabStatusChanged;
#endregion
#region Public Properties
///
/// Gets or sets Task.
///
public EncodeTask Task { get; set; }
///
/// Gets or sets a value indicating whether chapter markers are enabled.
///
public bool IncludeChapterMarkers
{
get
{
return this.Task.IncludeChapterMarkers;
}
set
{
this.Task.IncludeChapterMarkers = value;
this.NotifyOfPropertyChange(() => this.IncludeChapterMarkers);
this.OnTabStatusChanged(null);
}
}
public ObservableCollection Chapters
{
get
{
return this.Task.ChapterNames;
}
set
{
this.Task.ChapterNames = value;
this.NotifyOfPropertyChange(() => this.Chapters);
}
}
#endregion
#region Properties
///
/// Gets or sets SourceChapterList.
///
private ObservableCollection SourceChapterList { get; set; }
#endregion
#region Public Methods
///
/// Export the Chapter Markers to a CSV file
///
///
/// Thrown if exporting fails.
///
public void Export()
{
string fileName = null;
using (var saveFileDialog = new SaveFileDialog()
{
Filter = "Csv File|*.csv",
DefaultExt = "csv",
CheckPathExists = true,
OverwritePrompt = true
})
{
var dialogResult = saveFileDialog.ShowDialog();
fileName = saveFileDialog.FileName;
// Exit early if the user cancelled or the filename is invalid
if (dialogResult != DialogResult.OK || string.IsNullOrWhiteSpace(fileName))
return;
}
try
{
using (var csv = new StreamWriter(fileName))
{
foreach (ChapterMarker row in this.Chapters)
{
csv.Write("{0},{1}{2}", row.ChapterNumber, CsvHelper.Escape(row.ChapterName), Environment.NewLine);
}
}
}
catch (Exception exc)
{
throw new GeneralApplicationException(
Resources.ChaptersViewModel_UnableToExportChaptersWarning,
Resources.ChaptersViewModel_UnableToExportChaptersMsg,
exc);
}
}
///
/// Imports a Chapter marker file
///
///
/// Thrown if importing fails.
///
public void Import()
{
string filename = null;
string fileExtension = null;
using (var dialog = new OpenFileDialog()
{
Filter = string.Join("|", "All Supported Formats (*.csv;*.tsv,*.xml,*.txt)|*.csv;*.tsv;*.xml;*.txt", ChapterImporterCsv.FileFilter, ChapterImporterXml.FileFilter, ChapterImporterTxt.FileFilter),
FilterIndex = 1, // 1 based, the index value of the first filter entry is 1
CheckFileExists = true
})
{
var dialogResult = dialog.ShowDialog();
filename = dialog.FileName;
// Exit if the user didn't press the OK button or the file name is invalid
if (dialogResult != DialogResult.OK || string.IsNullOrWhiteSpace(filename))
return;
// Retrieve the file extension after we've confirmed that the user selected something to open
fileExtension = Path.GetExtension(filename)?.ToLowerInvariant();
}
var importedChapters = new Dictionary>();
// Execute the importer based on the file extension
switch (fileExtension)
{
case ".csv": // comma separated file
case ".tsv": // tab separated file
ChapterImporterCsv.Import(filename, ref importedChapters);
break;
case ".xml":
ChapterImporterXml.Import(filename, ref importedChapters);
break;
case ".txt":
ChapterImporterTxt.Import(filename, ref importedChapters);
break;
default:
throw new GeneralApplicationException(
Resources.ChaptersViewModel_UnsupportedFileFormatWarning,
string.Format(Resources.ChaptersViewModel_UnsupportedFileFormatMsg, fileExtension));
}
// Exit early if no chapter information was extracted
if (importedChapters == null || importedChapters.Count <= 0)
return;
// Validate the chaptermap against the current chapter list extracted from the source
bool hasTimestamps = importedChapters.Select(importedChapter => importedChapter.Value.Item2).Any(t => t != TimeSpan.Zero);
string validationErrorMessage;
if (!this.ValidateImportedChapters(importedChapters, out validationErrorMessage, hasTimestamps))
{
if (!string.IsNullOrEmpty(validationErrorMessage))
{
throw new GeneralApplicationException(
Resources.ChaptersViewModel_ValidationFailedWarning,
validationErrorMessage);
}
// The user has cancelled the import, so exit
return;
}
// Now iterate over each chatper we have, and set it's name
foreach (ChapterMarker item in this.Chapters)
{
// If we don't have a chapter name for this chapter then
// fallback to the value that is already set for the chapter
string chapterName = item.ChapterName;
// Attempt to retrieve the imported chapter name
Tuple chapterInfo;
if (importedChapters.TryGetValue(item.ChapterNumber, out chapterInfo))
chapterName = chapterInfo.Item1;
// Assign the chapter name unless the name is not set or only whitespace characters
item.ChapterName = !string.IsNullOrWhiteSpace(chapterName) ? chapterName : string.Empty;
}
}
///
/// Setup this window for a new source
///
///
/// The source.
///
///
/// The title.
///
///
/// The preset.
///
///
/// The task.
///
public void SetSource(Source source, Title title, Preset preset, EncodeTask task)
{
this.Task = task;
if (preset != null)
{
this.IncludeChapterMarkers = preset.Task.IncludeChapterMarkers;
}
this.sourceChaptersList = title.Chapters;
this.SetSourceChapters(title.Chapters);
}
///
/// Setup this tab for the specified preset.
///
///
/// The preset.
///
///
/// The task.
///
public void SetPreset(Preset preset, EncodeTask task)
{
this.Task = task;
this.IncludeChapterMarkers = preset.Task.IncludeChapterMarkers;
this.NotifyOfPropertyChange(() => this.Chapters);
}
///
/// Update all the UI controls based on the encode task passed in.
///
///
/// The task.
///
public void UpdateTask(EncodeTask task)
{
this.Task = task;
this.NotifyOfPropertyChange(() => this.IncludeChapterMarkers);
this.NotifyOfPropertyChange(() => this.Chapters);
}
public bool MatchesPreset(Preset preset)
{
if (preset.Task.IncludeChapterMarkers != this.IncludeChapterMarkers)
{
return false;
}
return true;
}
///
/// Reset Chapter Names
///
public void Reset()
{
if (this.sourceChaptersList != null)
{
this.SetSourceChapters(this.sourceChaptersList);
}
}
///
/// Set the Source Chapters List
///
///
/// The source chapters.
///
public void SetSourceChapters(IEnumerable sourceChapters)
{
// Cache the chapters in this screen
this.SourceChapterList = new ObservableCollection(sourceChapters);
this.Chapters.Clear();
// Then Add new Chapter Markers.
int counter = 1;
foreach (Chapter chapter in this.SourceChapterList)
{
string chapterName = string.IsNullOrEmpty(chapter.ChapterName) ? string.Format(Resources.ChapterViewModel_Chapter, counter) : chapter.ChapterName;
var marker = new ChapterMarker(chapter.ChapterNumber, chapterName, chapter.Duration);
this.Chapters.Add(marker);
counter += 1;
}
}
#endregion
#region Private Methods
protected virtual void OnTabStatusChanged(TabStatusEventArgs e)
{
this.TabStatusChanged?.Invoke(this, e);
}
///
/// Validates any imported chapter information against the currently detected chapter information in the
/// source media. If validation fails then an error message is returned via the out parameter
///
/// The list of imported chapter information
/// In case of a validation error this variable will hold
/// a detailed message that can be presented to the user
/// True if there are no errors with imported chapters, false otherwise
private bool ValidateImportedChapters(Dictionary> importedChapters, out string validationErrorMessage, bool hasTimestamps)
{
validationErrorMessage = null;
// If the number of chapters don't match, prompt for confirmation
if (importedChapters.Count != this.Chapters.Count)
{
if (this.errorService.ShowMessageBox(
string.Format(Resources.ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchMsg, this.Chapters.Count, importedChapters.Count),
Resources.ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchWarning,
MessageBoxButton.YesNo,
MessageBoxImage.Question) !=
MessageBoxResult.Yes)
{
return false;
}
}
// If the average discrepancy in timings between chapters is either:
// a) more than 15 sec for more than 2 chapters
// (I chose 15sec based on empirical evidence from testing a few DVDs and comparing to chapter-marker files I downloaded)
// => This check will not be performed for the first and last chapter as they're very likely to differ significantly due to language and region
// differences (e.g. longer title sequences and different distributor credits)
if (hasTimestamps)
{
List diffs = new List();
foreach (KeyValuePair> import in importedChapters)
{
ChapterMarker sourceMarker = this.Chapters[import.Key - 1];
TimeSpan source = sourceMarker.Duration;
TimeSpan diff = source - import.Value.Item2;
diffs.Add(diff);
}
// var diffs = importedChapters.Zip(this.Chapters, (import, source) => source.Duration - import.Value.Item2);
if (diffs.Count(diff => Math.Abs(diff.TotalSeconds) > 15) > 2)
{
if (this.errorService.ShowMessageBox(
Resources.ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchMsg,
Resources.ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchWarning,
MessageBoxButton.YesNo,
MessageBoxImage.Question) != MessageBoxResult.Yes)
{
return false;
}
}
}
// All is well, we should import chapters
return true;
}
#endregion
}
}