summaryrefslogtreecommitdiffstats
path: root/win
diff options
context:
space:
mode:
authorSverrir Sigmundarson <[email protected]>2015-11-20 01:00:13 +0100
committerSverrir Sigmundarson <[email protected]>2015-11-23 14:39:32 +0100
commit4137a887b8fad760eb0c90d3ca90303aa3674740 (patch)
treeea381f573d587170d3664b716e67b232e0c92703 /win
parentec2474b1e9024d053915f9d91cf2da84f1f4fafe (diff)
Adding support for ChapterDb.org input formats (XML and TXT) files. Minor refactorings to accomodate the parsing of the new input formats.
Diffstat (limited to 'win')
-rw-r--r--win/CS/HandBrakeWPF/Exceptions/GeneralApplicationException.cs12
-rw-r--r--win/CS/HandBrakeWPF/HandBrakeWPF.csproj5
-rw-r--r--win/CS/HandBrakeWPF/Helpers/TimeSpanHelper.cs31
-rw-r--r--win/CS/HandBrakeWPF/Properties/Resources.Designer.cs82
-rw-r--r--win/CS/HandBrakeWPF/Properties/Resources.resx34
-rw-r--r--win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterCsv.cs59
-rw-r--r--win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterTxt.cs60
-rw-r--r--win/CS/HandBrakeWPF/Utilities/Input/ChapterImporterXml.cs77
-rw-r--r--win/CS/HandBrakeWPF/ViewModels/ChaptersViewModel.cs142
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 &apos;{0}&apos; 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&apos;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&apos;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">