summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDamiano Galassi <[email protected]>2016-08-22 14:38:01 +0200
committerDamiano Galassi <[email protected]>2016-08-22 14:38:01 +0200
commitfd3aa29aaeef382eb14575b416cc95d569adb47a (patch)
treee320cb2b66ebf903f4912bfc92dd5d2383a92133
parent403453a0e6d40562824bc3d770dbf73863f8b397 (diff)
MacGui: follow CSV format for imported/exported chapters.
-rw-r--r--macosx/HBChapterTitlesController.m261
1 files changed, 204 insertions, 57 deletions
diff --git a/macosx/HBChapterTitlesController.m b/macosx/HBChapterTitlesController.m
index 6fabfb5bc..cedc1088a 100644
--- a/macosx/HBChapterTitlesController.m
+++ b/macosx/HBChapterTitlesController.m
@@ -9,10 +9,157 @@
@import HandBrakeKit.HBChapter;
@import HandBrakeKit.HBJob;
+@interface NSArray (HBCSVAdditions)
+
++ (nullable NSArray<NSArray<NSString *> *> *)HB_arrayWithContentsOfCSVURL:(NSURL *)url;
+
+@end
+
+@implementation NSArray (HBCSVAdditions)
+
+// CSV parsing examples
+// CSV Record:
+// one,two,three
+// Fields:
+// <one>
+// <two>
+// <three>
+// CSV Record:
+// one, two, three
+// Fields:
+// <one>
+// < two>
+// < three>
+// CSV Record:
+// one,"2,345",three
+// Fields:
+// <one>
+// <2,345>
+// <three>
+// CSV record:
+// one,"John said, ""Hello there.""",three
+// Explanation: inside a quoted field, two double quotes in a row count
+// as an escaped double quote in the field data.
+// Fields:
+// <one>
+// <John said, "Hello there.">
+// <three>
++ (nullable NSArray<NSArray<NSString *> *> *)HB_arrayWithContentsOfCSVURL:(NSURL *)url;
+{
+ NSString *str = [[NSString alloc] initWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL];
+
+ if (str == nil)
+ {
+ return nil;
+ }
+
+ NSMutableString *csvString = [str mutableCopy];
+ [csvString replaceOccurrencesOfString:@"\r\n" withString:@"\n" options:NSLiteralSearch range:NSMakeRange(0, csvString.length)];
+ [csvString replaceOccurrencesOfString:@"\r" withString:@"\n" options:NSLiteralSearch range:NSMakeRange(0, csvString.length)];
+
+ if (!csvString)
+ {
+ return 0;
+ }
+
+ if ([csvString characterAtIndex:0] == 0xFEFF)
+ {
+ [csvString deleteCharactersInRange:NSMakeRange(0,1)];
+ }
+ if ([csvString characterAtIndex:[csvString length]-1] != '\n')
+ {
+ [csvString appendFormat:@"%c",'\n'];
+ }
+
+ NSScanner *sc = [NSScanner scannerWithString:csvString];
+ sc.charactersToBeSkipped = nil;
+ NSMutableArray *csvArray = [NSMutableArray array];
+ [csvArray addObject:[NSMutableArray array]];
+ NSCharacterSet *commaNewlineCS = [NSCharacterSet characterSetWithCharactersInString:@",\n"];
+
+ while (sc.scanLocation < csvString.length)
+ {
+ if ([sc scanString:@"\"" intoString:NULL])
+ {
+ // Quoted field
+ NSMutableString *field = [NSMutableString string];
+ BOOL done = NO;
+ NSString *quotedString;
+ // Scan until we get to the end double quote or the EOF.
+ while (!done && sc.scanLocation < csvString.length)
+ {
+ if ([sc scanUpToString:@"\"" intoString:&quotedString])
+ {
+ [field appendString:quotedString];
+ }
+ if ([sc scanString:@"\"\"" intoString:NULL])
+ {
+ // Escaped double quote inside the quoted string.
+ [field appendString:@"\""];
+ }
+ else
+ {
+ done = YES;
+ }
+ }
+ if (sc.scanLocation < csvString.length)
+ {
+ ++sc.scanLocation;
+ BOOL nextIsNewline = [sc scanString:@"\n" intoString:NULL];
+ BOOL nextIsComma = NO;
+ if (!nextIsNewline)
+ {
+ nextIsComma = [sc scanString:@"," intoString:NULL];
+ }
+ if (nextIsNewline || nextIsComma)
+ {
+ [[csvArray lastObject] addObject:field];
+ if (nextIsNewline && sc.scanLocation < csvString.length)
+ {
+ [csvArray addObject:[NSMutableArray array]];
+ }
+ }
+ else
+ {
+ // Quoted fields must be immediately followed by a comma or newline.
+ return nil;
+ }
+ }
+ else
+ {
+ // No close quote found before EOF, so file is invalid CSV.
+ return nil;
+ }
+ }
+ else
+ {
+ NSString *field;
+ [sc scanUpToCharactersFromSet:commaNewlineCS intoString:&field];
+ BOOL nextIsNewline = [sc scanString:@"\n" intoString:NULL];
+ BOOL nextIsComma = NO;
+ if (!nextIsNewline)
+ {
+ nextIsComma = [sc scanString:@"," intoString:NULL];
+ }
+ if (nextIsNewline || nextIsComma)
+ {
+ [[csvArray lastObject] addObject:field];
+ if (nextIsNewline && sc.scanLocation < csvString.length)
+ {
+ [csvArray addObject:[NSMutableArray array]];
+ }
+ }
+ }
+ }
+ return csvArray;
+}
+
+@end
+
@interface HBChapterTitlesController () <NSTableViewDataSource, NSTableViewDelegate>
@property (weak) IBOutlet NSTableView *table;
-@property (nonatomic, readwrite, strong) NSArray *chapterTitles;
+@property (nonatomic, readwrite, strong) NSArray<HBChapter *> *chapterTitles;
@end
@@ -73,6 +220,44 @@
#pragma mark - Chapter Files Import / Export
+- (BOOL)importChaptersFromURL:(NSURL *)URL error:(NSError **)outError
+{
+ NSArray<NSArray<NSString *> *> *csvData = [NSArray HB_arrayWithContentsOfCSVURL:URL];
+ if (csvData.count == self.chapterTitles.count)
+ {
+ NSUInteger i = 0;
+ for (NSArray<NSString *> *lineFields in csvData)
+ {
+ if (lineFields.count < 2 || [lineFields[0] integerValue] != i + 1)
+ {
+ if (NULL != outError)
+ {
+ *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid chapters CSV file", nil),
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The CSV file is not a valid chapters CSV file.", nil)}];
+ }
+ return NO;
+ }
+ i++;
+ }
+
+ NSUInteger j = 0;
+ for (NSArray<NSString *> *lineFields in csvData)
+ {
+ [self.chapterTitles[j] setTitle:lineFields[1]];
+ j++;
+ }
+ return YES;
+ }
+
+ if (NULL != outError)
+ {
+ *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Incorrect line count", nil),
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The line count in the chapters CSV file does not match the number of chapters in the movie.", nil)}];
+ }
+
+ return NO;
+}
+
- (IBAction)browseForChapterFile:(id)sender
{
// We get the current file name and path from the destination field here
@@ -80,65 +265,17 @@
// Open a panel to let the user choose the file
NSOpenPanel *panel = [NSOpenPanel openPanel];
- panel.allowedFileTypes = @[@"csv"];
+ panel.allowedFileTypes = @[@"csv", @"txt"];
panel.directoryURL = sourceDirectory;
[panel beginSheetModalForWindow:self.view.window completionHandler:^(NSInteger result)
{
if (result == NSFileHandlingPanelOKButton)
{
- NSString *csv = [[NSString alloc] initWithContentsOfURL:panel.URL encoding:NSUTF8StringEncoding error:NULL];
- NSMutableArray *csvArray = [[csv componentsSeparatedByString:@"\n"] mutableCopy];
- NSUInteger count = self.chapterTitles.count;
-
- if (csvArray.count > 0)
- {
- // if last item is empty remove it
- if ([csvArray.lastObject length] == 0)
- {
- [csvArray removeLastObject];
- }
- }
- // if chapters in table is not equal to array count
- if (count != csvArray.count)
- {
- [panel close];
- [[NSAlert alertWithMessageText:NSLocalizedString(@"Unable to load chapter file", nil)
- defaultButton:NSLocalizedString(@"OK", nil)
- alternateButton:NULL
- otherButton:NULL
- informativeTextWithFormat:NSLocalizedString(@"%d chapters expected, %d chapters found in %@", nil),
- count, csvArray.count, panel.URL.lastPathComponent] runModal];
- }
- else
+ NSError *error;
+ if ([self importChaptersFromURL:panel.URL error:&error] == NO)
{
- // otherwise, go ahead and populate table with array
- NSUInteger idx = 0;
- for (NSString *csvLine in csvArray)
- {
- if (csvLine.length > 4)
- {
- // Get the Range.location of the first comma in the line and then put everything after that into chapterTitle
- NSRange firstCommaRange = [csvLine rangeOfString:@","];
- NSString *chapterTitle = [csvLine substringFromIndex:firstCommaRange.location + 1];
- // Since we store our chapterTitle commas as "\," for the cli, we now need to remove the escaping "\" from the title
- chapterTitle = [chapterTitle stringByReplacingOccurrencesOfString:@"\\," withString:@","];
-
- [self.chapterTitles[idx] setTitle:chapterTitle];
- idx++;
- }
- else
- {
- [panel close];
- [[NSAlert alertWithMessageText:NSLocalizedString(@"Unable to load chapter file", nil)
- defaultButton:NSLocalizedString(@"OK", nil)
- alternateButton:NULL
- otherButton:NULL
- informativeTextWithFormat:NSLocalizedString(@"%@ was not formatted as expected.", nil), panel.URL.lastPathComponent] runModal];
- break;
-
- }
- }
+ [self presentError:error];
}
}
}];
@@ -164,12 +301,22 @@
for (HBChapter *chapter in self.chapterTitles)
{
// put each chapter title from the table into the array
- [csv appendFormat:@"%03ld,",idx + 1];
+ [csv appendFormat:@"%ld,",idx + 1];
idx++;
- // Escape any commas in the chapter name with "\,"
- NSString *sanatizedTitle = [chapter.title stringByReplacingOccurrencesOfString:@"," withString:@"\\,"];
- [csv appendString:sanatizedTitle];
+ NSString *sanatizedTitle = [chapter.title stringByReplacingOccurrencesOfString:@"\"" withString:@"\"\""];
+
+ // If the title contains any commas or quotes, add quotes
+ if ([sanatizedTitle containsString:@","] || [sanatizedTitle containsString:@"\""])
+ {
+ [csv appendString:@"\""];
+ [csv appendString:sanatizedTitle];
+ [csv appendString:@"\""];
+ }
+ else
+ {
+ [csv appendString:sanatizedTitle];
+ }
[csv appendString:@"\n"];
}