diff options
author | Sverrir Sigmundarson <[email protected]> | 2015-11-20 01:00:13 +0100 |
---|---|---|
committer | Sverrir Sigmundarson <[email protected]> | 2015-11-23 14:39:32 +0100 |
commit | 4137a887b8fad760eb0c90d3ca90303aa3674740 (patch) | |
tree | ea381f573d587170d3664b716e67b232e0c92703 | |
parent | ec2474b1e9024d053915f9d91cf2da84f1f4fafe (diff) |
Adding support for ChapterDb.org input formats (XML and TXT) files. Minor refactorings to accomodate the parsing of the new input formats.
-rw-r--r-- | win/CS/HandBrakeWPF/Exceptions/GeneralApplicationException.cs | 12 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/HandBrakeWPF.csproj | 5 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/Helpers/TimeSpanHelper.cs | 31 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/Properties/Resources.Designer.cs | 82 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/Properties/Resources.resx | 34 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterCsv.cs | 59 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterTxt.cs | 60 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterXml.cs | 77 | ||||
-rw-r--r-- | win/CS/HandBrakeWPF/ViewModels/ChaptersViewModel.cs | 142 |
9 files changed, 461 insertions, 41 deletions
diff --git a/win/CS/HandBrakeWPF/Exceptions/GeneralApplicationException.cs b/win/CS/HandBrakeWPF/Exceptions/GeneralApplicationException.cs index 10c5192af..cbbcd4058 100644 --- a/win/CS/HandBrakeWPF/Exceptions/GeneralApplicationException.cs +++ b/win/CS/HandBrakeWPF/Exceptions/GeneralApplicationException.cs @@ -36,6 +36,18 @@ namespace HandBrakeWPF.Exceptions } /// <summary> + /// Initializes a new instance of the <see cref="GeneralApplicationException"/> class with no wrapped exception. + /// </summary> + /// <param name="error"> + /// The error. + /// </param> + /// <param name="solution"> + /// The solution. + /// </param> + public GeneralApplicationException(string error, string solution) : this(error, solution, null) + {} + + /// <summary> /// Gets or sets FailureReason. /// </summary> public string Error { get; set; } diff --git a/win/CS/HandBrakeWPF/HandBrakeWPF.csproj b/win/CS/HandBrakeWPF/HandBrakeWPF.csproj index 00b0560e9..16c16d777 100644 --- a/win/CS/HandBrakeWPF/HandBrakeWPF.csproj +++ b/win/CS/HandBrakeWPF/HandBrakeWPF.csproj @@ -153,6 +153,7 @@ <Compile Include="EventArgs\SettingChangedEventArgs.cs" />
<Compile Include="Exceptions\GeneralApplicationException.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
+ <Compile Include="Helpers\TimeSpanHelper.cs" />
<Compile Include="Helpers\Validate.cs" />
<Compile Include="Model\Audio\AudioTrackDefaultsMode.cs" />
<Compile Include="Model\Audio\AudioBehaviourModes.cs" />
@@ -231,6 +232,9 @@ <Compile Include="Utilities\ExtensionMethods.cs" />
<Compile Include="Utilities\GeneralUtilities.cs" />
<Compile Include="Utilities\HandBrakeApp.cs" />
+ <Compile Include="Utilities\Input\ChapterImporterCsv.cs" />
+ <Compile Include="Utilities\Input\ChapterImporterTxt.cs" />
+ <Compile Include="Utilities\Input\ChapterImporterXml.cs" />
<Compile Include="Utilities\Interfaces\INotifyPropertyChangedEx.cs" />
<Compile Include="Utilities\Output\CsvHelper.cs" />
<Compile Include="Utilities\PropertyChangedBase.cs" />
@@ -640,6 +644,7 @@ <ItemGroup>
<Resource Include="Views\Images\information64.png" />
</ItemGroup>
+ <ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(ProgramFiles)\MSBuild\StyleCop\v4.*\StyleCop.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
diff --git a/win/CS/HandBrakeWPF/Helpers/TimeSpanHelper.cs b/win/CS/HandBrakeWPF/Helpers/TimeSpanHelper.cs new file mode 100644 index 000000000..94d7629b9 --- /dev/null +++ b/win/CS/HandBrakeWPF/Helpers/TimeSpanHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HandBrakeWPF.Helpers +{ + using System.Globalization; + + /// <summary> + /// Helper functions for handling <see cref="TimeSpan"/> structures + /// </summary> + internal static class TimeSpanHelper + { + /// <summary> + /// Parses chapter time start value from a chapter marker input file. + /// </summary> + /// <param name="chapterStartRaw">The raw string value parsed from the input file</param> + internal static TimeSpan ParseChapterTimeStart(string chapterStartRaw) + { + //Format: 02:35:05 and 02:35:05.2957333 + return TimeSpan.ParseExact(chapterStartRaw, + new[] + { + @"hh\:mm\:ss", // Handle whole seconds + @"hh\:mm\:ss\.FFFFFFF" // Handle fraction seconds + }, CultureInfo.InvariantCulture); + } + } +} diff --git a/win/CS/HandBrakeWPF/Properties/Resources.Designer.cs b/win/CS/HandBrakeWPF/Properties/Resources.Designer.cs index b53620537..d5265941a 100644 --- a/win/CS/HandBrakeWPF/Properties/Resources.Designer.cs +++ b/win/CS/HandBrakeWPF/Properties/Resources.Designer.cs @@ -493,6 +493,88 @@ namespace HandBrakeWPF.Properties { }
/// <summary>
+ /// Looks up a localized string similar to Chapter files of type '{0}' are not currently supported..
+ /// </summary>
+ public static string ChaptersViewModel_UnsupportedFileFormatMsg {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_UnsupportedFileFormatMsg", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unsupported chapter file type.
+ /// </summary>
+ public static string ChaptersViewModel_UnsupportedFileFormatWarning {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_UnsupportedFileFormatWarning", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The number of chapters on the source media
+ ///and the number of chapters in the input file do not match ({0} vs {1}).
+ ///
+ ///Do you still want to import the chapter names?.
+ /// </summary>
+ public static string ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatch {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The number of chapters on the source media
+ ///and the number of chapters in the input file do not match ({0} vs {1}).
+ ///
+ ///Do you still want to import the chapter names?.
+ /// </summary>
+ public static string ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchMsg {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchMsg", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Chapter count doesn't match between source and input file.
+ /// </summary>
+ public static string ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchWarning {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchWarning", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The reported duration of the chapters on the source media
+ ///and the duration of chapters in the input file differ drastically.
+ ///It is very likely that this chapter file was produced from a different source media.
+ ///
+ ///Are you sure you want to import the chapter names?.
+ /// </summary>
+ public static string ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchMsg {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchMsg", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Chapter duration doesn't match between source and input file.
+ /// </summary>
+ public static string ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchWarning {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchWarning", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid chapter information for source media.
+ /// </summary>
+ public static string ChaptersViewModel_ValidationFailedWarning {
+ get {
+ return ResourceManager.GetString("ChaptersViewModel_ValidationFailedWarning", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Confirm.
/// </summary>
public static string Confirm {
diff --git a/win/CS/HandBrakeWPF/Properties/Resources.resx b/win/CS/HandBrakeWPF/Properties/Resources.resx index a8b2cb469..304b8f961 100644 --- a/win/CS/HandBrakeWPF/Properties/Resources.resx +++ b/win/CS/HandBrakeWPF/Properties/Resources.resx @@ -751,4 +751,38 @@ Your old presets file was archived to:</value> <data name="ChaptersViewModel_UnableToImportChaptersFirstColumnMustContainOnlyIntegerNumber" xml:space="preserve">
<value>First column in chapters file must only contain a integer number value higher than zero (0)</value>
</data>
+ <data name="ChaptersViewModel_UnsupportedFileFormatMsg" xml:space="preserve">
+ <value>Chapter files of type '{0}' are not currently supported.</value>
+ </data>
+ <data name="ChaptersViewModel_UnsupportedFileFormatWarning" xml:space="preserve">
+ <value>Unsupported chapter file type</value>
+ </data>
+ <data name="ChaptersViewModel_ValidationFailedWarning" xml:space="preserve">
+ <value>Invalid chapter information for source media</value>
+ </data>
+ <data name="ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatch" xml:space="preserve">
+ <value>The number of chapters on the source media
+and the number of chapters in the input file do not match ({0} vs {1}).
+
+Do you still want to import the chapter names?</value>
+ </data>
+ <data name="ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchMsg" xml:space="preserve">
+ <value>The number of chapters on the source media
+and the number of chapters in the input file do not match ({0} vs {1}).
+
+Do you still want to import the chapter names?</value>
+ </data>
+ <data name="ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchWarning" xml:space="preserve">
+ <value>Chapter count doesn't match between source and input file</value>
+ </data>
+ <data name="ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchMsg" xml:space="preserve">
+ <value>The reported duration of the chapters on the source media
+and the duration of chapters in the input file differ drastically.
+It is very likely that this chapter file was produced from a different source media.
+
+Are you sure you want to import the chapter names?</value>
+ </data>
+ <data name="ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchWarning" xml:space="preserve">
+ <value>Chapter duration doesn't match between source and input file</value>
+ </data>
</root>
\ No newline at end of file diff --git a/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterCsv.cs b/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterCsv.cs new file mode 100644 index 000000000..906eae510 --- /dev/null +++ b/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterCsv.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HandBrakeWPF.Utilities.Input +{ + using HandBrakeWPF.Exceptions; + using HandBrakeWPF.Properties; + + using Microsoft.VisualBasic.FileIO; + + /// <summary> + /// Handles the importing of Chapter information from CSV files + /// </summary> + internal class ChapterImporterCsv + { + public static string FileFilter => "CSV files (*.csv;*.tsv)|*.csv;*.tsv"; + + public static void Import(string filename, ref Dictionary<int, Tuple<string, TimeSpan>> importedChapters) + { + using (TextFieldParser csv = new TextFieldParser(filename) + { + CommentTokens = new[] { "#" }, // Comment lines + Delimiters = new[] { ",", "\t", ";", ":" }, // Support all of these common delimeter types + HasFieldsEnclosedInQuotes = true, // Assume that our data will be properly escaped + TextFieldType = FieldType.Delimited, + TrimWhiteSpace = true // Remove excess whitespace from ends of imported values + }) + { + while (!csv.EndOfData) + { + try + { + // Only read the first two columns, anything else will be ignored but will not raise an error + var row = csv.ReadFields(); + if (row == null || row.Length < 2) // null condition happens if the file is somehow corrupt during reading + throw new MalformedLineException(Resources.ChaptersViewModel_UnableToImportChaptersLineDoesNotHaveAtLeastTwoColumns, csv.LineNumber); + + int chapterNumber; + if (!int.TryParse(row[0], out chapterNumber)) + throw new MalformedLineException(Resources.ChaptersViewModel_UnableToImportChaptersFirstColumnMustContainOnlyIntegerNumber, csv.LineNumber); + + // Store the chapter name at the correct index + importedChapters[chapterNumber] = new Tuple<string, TimeSpan>(row[1]?.Trim(), TimeSpan.Zero); + } + catch (MalformedLineException mlex) + { + throw new GeneralApplicationException( + Resources.ChaptersViewModel_UnableToImportChaptersWarning, + string.Format(Resources.ChaptersViewModel_UnableToImportChaptersMalformedLineMsg, mlex.LineNumber), + mlex); + } + } + } + } + } +} diff --git a/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterTxt.cs b/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterTxt.cs new file mode 100644 index 000000000..678abb20a --- /dev/null +++ b/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterTxt.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HandBrakeWPF.Utilities.Input +{ + using System.IO; + + using HandBrakeWPF.Helpers; + + public class ChapterImporterTxt + { + public static string FileFilter => "Text files (*.txt)|*.txt"; + + public static void Import(string filename, ref Dictionary<int, Tuple<string, TimeSpan>> chapterMap) + { + using (var file = new StreamReader(filename)) + { + // Indexing is 1-based + int chapterMapIdx = 1; + TimeSpan prevChapterStart = TimeSpan.Zero; + + while (!file.EndOfStream) + { + // Read the lines in pairs, the duration is always first then the chapter name + var chapterStartRaw = file.ReadLine(); + var chapterName = file.ReadLine(); + + // If either of the values is null then the end of the file has been reached and we need to terminate + if (chapterName == null || chapterStartRaw == null) + break; + + // Split the values on '=' and take the left side + chapterName = chapterName.Split(new []{ '=' }, 2).LastOrDefault(); + chapterStartRaw = chapterStartRaw.Split(new[] { '=' }, 2).LastOrDefault(); + + // Parse the time + if(!string.IsNullOrWhiteSpace(chapterStartRaw)) + { + var chapterStart = TimeSpanHelper.ParseChapterTimeStart(chapterStartRaw); + + // If we're past the first chapter in the file then calculate the duration for the previous chapter + if (chapterMapIdx > 1) + { + var old = chapterMap[chapterMapIdx - 1]; + chapterMap[chapterMapIdx - 1] = new Tuple<string, TimeSpan>(old.Item1, chapterStart - prevChapterStart); + } + + prevChapterStart = chapterStart; + } + + // Save the chapter info, we calculate the duration in the next iteration (look back) + chapterMap[chapterMapIdx++] = new Tuple<string, TimeSpan>(chapterName, TimeSpan.Zero); + } + } + } + } +} diff --git a/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterXml.cs b/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterXml.cs new file mode 100644 index 000000000..565b49e3c --- /dev/null +++ b/win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterXml.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HandBrakeWPF.Utilities.Input +{ + using System.Globalization; + using System.IO; + using System.Xml; + using System.Xml.Linq; + using System.Xml.XPath; + + using HandBrakeWPF.Helpers; + + /// <summary> + /// Imports chapter markers in the ChaptersDb.org XML format + /// More info: http://www.chapterdb.org/docs + /// </summary> + internal class ChapterImporterXml + { + /// <summary> + /// The file filter value for the OpenFileDialog + /// </summary> + public static string FileFilter => "XML files (*.xml)|*.xml"; + + /// <summary> + /// Imports all chapter information from the given <see cref="filename"/> into the <see cref="chapterMap"/> dictionary. + /// </summary> + /// <param name="filename">The full path and filename of the chapter marker file to import</param> + /// <param name="chapterMap">The dictionary that should be populated with parsed chapter markers</param> + public static void Import(string filename, ref Dictionary<int, Tuple<string, TimeSpan>> chapterMap) + { + XDocument xDoc = XDocument.Load(new StreamReader(filename)); + var xRoot = xDoc.Root; + if (xRoot == null) + return; + + // Indexing is 1-based + int chapterMapIdx = 1; + + // Get all chapters in the document + var chapters = xRoot.XPathSelectElements("/Chapters/EditionEntry/ChapterAtom"); + TimeSpan prevChapterStart = TimeSpan.Zero; + + foreach (XElement chapter in chapters) + { + // Extract and clean up any special XML escape characters + var chapterName = chapter.XPathSelectElement("ChapterDisplay/ChapterString")?.Value; + if (!string.IsNullOrWhiteSpace(chapterName)) + { + chapterName = XmlConvert.DecodeName(chapterName); + } + + var chapterStartRaw = chapter.XPathSelectElement("ChapterTimeStart")?.Value; + if(!string.IsNullOrWhiteSpace(chapterStartRaw)) + { + //Format: 02:35:05 and 02:35:05.2957333 + var chapterStart = TimeSpanHelper.ParseChapterTimeStart(chapterStartRaw); + + // If we're past the first chapter in the file then calculate the duration for the previous chapter + if (chapterMapIdx > 1) + { + var old = chapterMap[chapterMapIdx - 1]; + chapterMap[chapterMapIdx-1] = new Tuple<string, TimeSpan>(old.Item1, chapterStart - prevChapterStart); + } + + prevChapterStart = chapterStart; + } + + // Save the chapter info, we calculate the duration in the next iteration (look back) + chapterMap[chapterMapIdx++] = new Tuple<string, TimeSpan>(chapterName, TimeSpan.Zero); + } + } + } +} diff --git a/win/CS/HandBrakeWPF/ViewModels/ChaptersViewModel.cs b/win/CS/HandBrakeWPF/ViewModels/ChaptersViewModel.cs index 4815de20d..e92656f97 100644 --- a/win/CS/HandBrakeWPF/ViewModels/ChaptersViewModel.cs +++ b/win/CS/HandBrakeWPF/ViewModels/ChaptersViewModel.cs @@ -12,8 +12,10 @@ namespace HandBrakeWPF.ViewModels using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text;
+ using System.Linq;
using System.Windows.Forms;
using Caliburn.Micro;
@@ -22,6 +24,7 @@ namespace HandBrakeWPF.ViewModels 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;
@@ -98,7 +101,7 @@ namespace HandBrakeWPF.ViewModels /// Export the Chapter Markers to a CSV file
/// </summary>
/// <exception cref="GeneralApplicationException">
- /// Thrown when exporting fails.
+ /// Thrown if exporting fails.
/// </exception>
public void Export()
{
@@ -139,12 +142,21 @@ namespace HandBrakeWPF.ViewModels }
/// <summary>
- /// Import a CSV file
+ /// Imports a Chapter marker file
/// </summary>
+ /// <exception cref="GeneralApplicationException">
+ /// Thrown if importing fails.
+ /// </exception>
public void Import()
{
string filename = null;
- using (var dialog = new OpenFileDialog() { Filter = "CSV files (*.csv)|*.csv", CheckFileExists = true })
+ string fileExtension = null;
+ using (var dialog = new OpenFileDialog()
+ {
+ Filter = string.Join("|", 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;
@@ -152,57 +164,54 @@ namespace HandBrakeWPF.ViewModels // 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 chapterMap = new Dictionary<int, string>();
+ var importedChapters = new Dictionary<int, Tuple<string, TimeSpan>>();
- using (TextFieldParser csv = new TextFieldParser(filename)
- { CommentTokens = new[] { "#" }, // Comment lines
- Delimiters = new[] { ",", "\t", ";", ":" }, // Support all of these common delimeter types
- HasFieldsEnclosedInQuotes = true, // Assume that our data will be properly escaped
- TextFieldType = FieldType.Delimited,
- TrimWhiteSpace = true // Remove excess whitespace from ends of imported values
- })
+ // Execute the importer based on the file extension
+ switch (fileExtension)
{
- while (!csv.EndOfData)
- {
- try
- {
- // Only read the first two columns, anything else will be ignored but will not raise an error
- var row = csv.ReadFields();
- if (row == null || row.Length < 2) // null condition happens if the file is somehow corrupt during reading
- throw new MalformedLineException(Resources.ChaptersViewModel_UnableToImportChaptersLineDoesNotHaveAtLeastTwoColumns, csv.LineNumber);
-
- int chapterNumber;
- if (!int.TryParse(row[0], out chapterNumber))
- throw new MalformedLineException(Resources.ChaptersViewModel_UnableToImportChaptersFirstColumnMustContainOnlyIntegerNumber, csv.LineNumber);
-
- // Store the chapter name at the correct index
- chapterMap[chapterNumber] = row[1]?.Trim();
- }
- catch (MalformedLineException mlex)
- {
- throw new GeneralApplicationException(
- Resources.ChaptersViewModel_UnableToImportChaptersWarning,
- string.Format(Resources.ChaptersViewModel_UnableToImportChaptersMalformedLineMsg, mlex.LineNumber),
- mlex);
- }
- }
+ case ".csv":
+ 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 (chapterMap.Count <= 0)
+ if (importedChapters == null || importedChapters.Count <= 0)
return;
+ // Validate the chaptermap against the current chapter list extracted from the source
+ string validationErrorMessage;
+ if (!this.ValidateImportedChapters(importedChapters, out validationErrorMessage))
+ {
+ if( !string.IsNullOrEmpty(validationErrorMessage))
+ throw new GeneralApplicationException(Resources.ChaptersViewModel_ValidationFailedWarning, validationErrorMessage);
+ }
+
// Now iterate over each chatper we have, and set it's name
foreach (ChapterMarker item in this.Task.ChapterNames)
{
- string chapterName;
-
// If we don't have a chapter name for this chapter then
// fallback to the value that is already set for the chapter
- if (!chapterMap.TryGetValue(item.ChapterNumber, out chapterName))
- chapterName = item.ChapterName;
+ string chapterName = item.ChapterName;
+
+ // Attempt to retrieve the imported chapter name
+ Tuple<string, TimeSpan> chapterInfo;
+ if (importedChapters.TryGetValue(item.ChapterNumber, out chapterInfo))
+ chapterName = chapterInfo.Item1;
// Assign the chapter name unless the name is not set or only whitespace charaters
item.ChapterName = !string.IsNullOrWhiteSpace(chapterName) ? chapterName : string.Empty;
@@ -210,6 +219,57 @@ namespace HandBrakeWPF.ViewModels }
/// <summary>
+ /// 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 <see cref="validationErrorMessage"/>
+ /// </summary>
+ /// <param name="importedChapters">The list of imported chapter information</param>
+ /// <param name="validationErrorMessage">In case of a validation error this variable will hold
+ /// a detailed message that can be presented to the user</param>
+ /// <returns>True if there are no errors with imported chapters, false otherwise</returns>
+ private bool ValidateImportedChapters(Dictionary<int, Tuple<string, TimeSpan>> importedChapters, out string validationErrorMessage)
+ {
+ validationErrorMessage = null;
+
+ // If the number of chapters don't match, prompt for confirmation
+ if (importedChapters.Count != this.Task.ChapterNames.Count)
+ {
+ if (DialogResult.Yes !=
+ MessageBox.Show(
+ string.Format(Resources.ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchMsg, this.Task.ChapterNames.Count, importedChapters.Count),
+ Resources.ChaptersViewModel_ValidateImportedChapters_ChapterCountMismatchWarning,
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question,
+ MessageBoxDefaultButton.Button2))
+ {
+ 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)
+ var diffs = importedChapters.Zip(this.Task.ChapterNames, (import, source) => source.Duration - import.Value.Item2);
+ if (diffs.Count(diff => Math.Abs(diff.TotalSeconds) > 15) > 2)
+ {
+ if (DialogResult.Yes !=
+ MessageBox.Show(
+ Resources.ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchMsg,
+ Resources.ChaptersViewModel_ValidateImportedChapters_ChapterDurationMismatchWarning,
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question,
+ MessageBoxDefaultButton.Button2))
+ {
+ return false;
+ }
+ }
+
+ // All is well, we should import chapters
+ return true;
+ }
+
+ /// <summary>
/// Setup this window for a new source
/// </summary>
/// <param name="source">
|