Changed feed parsing from python lib to RSXML

- Bug fix: Changing url to malformed one will remove all entries
- Bug fix: Add feed without selection resulted in a crash
- Removed FeedTag from database model
This commit is contained in:
relikd
2018-09-12 01:05:39 +02:00
parent 1b118959fd
commit 0c94769700
18 changed files with 135 additions and 4404 deletions

2
.gitignore vendored
View File

@@ -80,7 +80,7 @@ xcuserdata/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Checkouts
Carthage/Build

1
Cartfile Normal file
View File

@@ -0,0 +1 @@
github "relikd/RSXML" "master"

1
Cartfile.resolved Normal file
View File

@@ -0,0 +1 @@
github "relikd/RSXML" "c1b8eca0854aa4d1262dc5dfc054ec8dafb18609"

View File

@@ -7,14 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1968EF7567E06D2A5BB3481A /* PyHandler.m */; };
541A90F221257D77002680A6 /* MenuItemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 541A90F121257D77002680A6 /* MenuItemInfo.m */; };
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
544FBD4521064AEB008A260C /* Python.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 544FBD4421064AEB008A260C /* Python.framework */; };
544FBD4721064B2F008A260C /* getFeed.py in Resources */ = {isa = PBXBuildFile; fileRef = 544FBD4621064B2F008A260C /* getFeed.py */; };
544FBD4921064DF0008A260C /* feedparser521.py in Resources */ = {isa = PBXBuildFile; fileRef = 544FBD4821064DF0008A260C /* feedparser521.py */; };
544DCCB9212A2B4D002DBC46 /* RSXML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 544DCCB8212A2B4D002DBC46 /* RSXML.framework */; };
544DCCBA212A2B4D002DBC46 /* RSXML.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 544DCCB8212A2B4D002DBC46 /* RSXML.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
544DCCBE212A2B6F002DBC46 /* RSXML.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = 544DCCBD212A2B6F002DBC46 /* RSXML.framework.dSYM */; };
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */ = {isa = PBXBuildFile; fileRef = 546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */; };
546FC43F21188C78007CC3A3 /* SettingsFeeds.xib in Resources */ = {isa = PBXBuildFile; fileRef = 546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */; };
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */ = {isa = PBXBuildFile; fileRef = 546FC44121189975007CC3A3 /* SettingsGeneral.m */; };
@@ -32,9 +31,31 @@
54FE73D3212316CD003EAC65 /* BarMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 54FE73D2212316CD003EAC65 /* BarMenu.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
544DCCBB212A2B4D002DBC46 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
544DCCBA212A2B4D002DBC46 /* RSXML.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
544DCCBC212A2B5A002DBC46 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 16;
files = (
544DCCBE212A2B6F002DBC46 /* RSXML.framework.dSYM in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1968E7919BAA36F042FCB717 /* PyHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyHandler.h; sourceTree = "<group>"; };
1968EF7567E06D2A5BB3481A /* PyHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PyHandler.m; sourceTree = "<group>"; };
541A90F021257D77002680A6 /* MenuItemInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuItemInfo.h; sourceTree = "<group>"; };
541A90F121257D77002680A6 /* MenuItemInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuItemInfo.m; sourceTree = "<group>"; };
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
@@ -43,9 +64,8 @@
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
544B011C2114EE9100386E5C /* AppHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppHook.m; sourceTree = "<group>"; };
544FBD4421064AEB008A260C /* Python.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Python.framework; path = System/Library/Frameworks/Python.framework; sourceTree = SDKROOT; };
544FBD4621064B2F008A260C /* getFeed.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = getFeed.py; sourceTree = "<group>"; usesTabs = 0; };
544FBD4821064DF0008A260C /* feedparser521.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = feedparser521.py; sourceTree = "<group>"; usesTabs = 0; };
544DCCB8212A2B4D002DBC46 /* RSXML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RSXML.framework; path = Carthage/Build/Mac/RSXML.framework; sourceTree = "<group>"; };
544DCCBD212A2B6F002DBC46 /* RSXML.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = RSXML.framework.dSYM; path = Carthage/Build/Mac/RSXML.framework.dSYM; sourceTree = "<group>"; };
546FC43B21188AD5007CC3A3 /* SettingsFeeds.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsFeeds.h; sourceTree = "<group>"; };
546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsFeeds.m; sourceTree = "<group>"; };
546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsFeeds.xib; sourceTree = "<group>"; };
@@ -78,7 +98,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
544FBD4521064AEB008A260C /* Python.framework in Frameworks */,
544DCCB9212A2B4D002DBC46 /* RSXML.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -99,7 +119,8 @@
544FBD4321064AEB008A260C /* Frameworks */ = {
isa = PBXGroup;
children = (
544FBD4421064AEB008A260C /* Python.framework */,
544DCCB8212A2B4D002DBC46 /* RSXML.framework */,
544DCCBD212A2B6F002DBC46 /* RSXML.framework.dSYM */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -130,17 +151,6 @@
path = Preferences;
sourceTree = "<group>";
};
549369F421091E6D001AF895 /* python */ = {
isa = PBXGroup;
children = (
1968E7919BAA36F042FCB717 /* PyHandler.h */,
1968EF7567E06D2A5BB3481A /* PyHandler.m */,
544FBD4621064B2F008A260C /* getFeed.py */,
544FBD4821064DF0008A260C /* feedparser521.py */,
);
path = python;
sourceTree = "<group>";
};
54ACC27321061B3B0020715F = {
isa = PBXGroup;
children = (
@@ -161,7 +171,6 @@
54ACC27E21061B3B0020715F /* baRSS */ = {
isa = PBXGroup;
children = (
549369F421091E6D001AF895 /* python */,
544B011B2114EE9100386E5C /* AppHook.h */,
544B011C2114EE9100386E5C /* AppHook.m */,
541A90EF21257D4F002680A6 /* Status Bar Menu */,
@@ -203,6 +212,8 @@
54ACC27821061B3B0020715F /* Sources */,
54ACC27921061B3B0020715F /* Frameworks */,
54ACC27A21061B3B0020715F /* Resources */,
544DCCBB212A2B4D002DBC46 /* Embed Frameworks */,
544DCCBC212A2B5A002DBC46 /* CopyFiles */,
);
buildRules = (
);
@@ -264,8 +275,6 @@
54ACC28621061B3C0020715F /* Assets.xcassets in Resources */,
546FC44421189975007CC3A3 /* SettingsGeneral.xib in Resources */,
546FC43F21188C78007CC3A3 /* SettingsFeeds.xib in Resources */,
544FBD4921064DF0008A260C /* feedparser521.py in Resources */,
544FBD4721064B2F008A260C /* getFeed.py in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -287,7 +296,6 @@
54ACC29821061FBA0020715F /* Preferences.m in Sources */,
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */,
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
541A90F221257D77002680A6 /* MenuItemInfo.m in Sources */,
@@ -432,6 +440,7 @@
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
@@ -481,6 +490,7 @@
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/Carthage/Build/Mac",
);
GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES;
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;

View File

@@ -21,7 +21,6 @@
// SOFTWARE.
#import "AppHook.h"
#import "PyHandler.h"
#import "BarMenu.h"
@implementation AppHook
@@ -34,12 +33,12 @@
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
_barMenu = [BarMenu new];
[PyHandler prepare];
printf("up and running\n");
// https://feeds.feedburner.com/simpledesktops
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
[PyHandler shutdown];
}

View File

@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G65" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1">
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="httpEtag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="httpModified" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="Binary" customClassName="NSImage" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="modified" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="published" optional="YES" attributeType="Transformable" customClassName="NSArray" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="config" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="feed" inverseEntity="FeedConfig" syncable="YES"/>
@@ -17,6 +14,7 @@
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refreshNum" optional="YES" attributeType="Integer 32" usesScalarValueType="YES" syncable="YES"/>
<attribute name="refreshUnit" optional="YES" attributeType="Integer 16" usesScalarValueType="YES" customClassName="NSUInteger" syncable="YES"/>
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="type" optional="YES" attributeType="Integer 16" usesScalarValueType="YES" syncable="YES"/>
<attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
@@ -25,24 +23,19 @@
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="children" inverseEntity="FeedConfig" syncable="YES"/>
</entity>
<entity name="FeedItem" representedClassName="FeedItem" syncable="YES" codeGenerationType="class">
<attribute name="abstract" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="guid" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="published" optional="YES" attributeType="Transformable" customClassName="NSArray" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="summary" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed" syncable="YES"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="FeedTag" inverseName="feedItem" inverseEntity="FeedTag" syncable="YES"/>
</entity>
<entity name="FeedTag" representedClassName="FeedTag" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="feedItem" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedItem" inverseName="tags" inverseEntity="FeedItem" syncable="YES"/>
</entity>
<elements>
<element name="Feed" positionX="-209" positionY="-3" width="128" height="210"/>
<element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="180"/>
<element name="Feed" positionX="-209" positionY="-3" width="128" height="165"/>
<element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="195"/>
<element name="FeedItem" positionX="-20" positionY="81" width="128" height="180"/>
<element name="FeedTag" positionX="187" positionY="171" width="128" height="75"/>
</elements>
</model>

View File

@@ -21,7 +21,8 @@
// SOFTWARE.
#import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@interface FeedDownload : NSObject
+ (void)getFeed:(NSString*)url withBlock:(nullable void (^)(NSDictionary* result, NSError* error))block;
+ (void)getFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block;
@end

View File

@@ -21,19 +21,30 @@
// SOFTWARE.
#import "FeedDownload.h"
#import "PyHandler.h"
@implementation FeedDownload
+ (void)getFeed:(NSString*)url withBlock:(nullable void (^)(NSDictionary* result, NSError* error))block {
[NSThread detachNewThreadWithBlock:^{
NSDictionary *dict = [PyHandler getFeed:url withEtag:nil andModified:nil];
NSError *err = nil;
if (!dict || [dict[@"entries"] count] == 0 ) {
err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotParseResponse userInfo:nil];
+ (void)getFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block {
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
req.timeoutInterval = 30;
req.cachePolicy = NSURLRequestReloadIgnoringCacheData;
// [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"];
// [req setValue:@"wII2pETT9EGmlqyCHBFJpm25/7w" forHTTPHeaderField:@"If-None-Match"]; // ETag
[[[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
// NSString* etag = [httpResponse allHeaderFields][@"Etag"];
// if (etag.length > 5 && [[etag substringFromIndex:etag.length - 5] isEqualToString:@"-gzip"]) {
// etag = [etag substringToIndex:etag.length - 5];
// }
if (error || [httpResponse statusCode] == 304) {
block(nil, error, httpResponse);
return;
}
if (block) block(dict, err);
}];
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:url];
RSParseFeed(xml, ^(RSParsedFeed * _Nullable parsedFeed, NSError * _Nullable err) {
block(parsedFeed, err, httpResponse);
});
}] resume];
}
@end

View File

@@ -30,5 +30,10 @@
<string>Copyright © 2018 relikd. All rights reserved.</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -36,9 +36,10 @@
@property (copy) NSString *previousURL;
@property (strong) NSError *feedError;
@property (strong) NSDictionary *feedResult;
@property (strong) RSParsedFeed *feedResult;
@property (assign) BOOL shouldSaveObject;
@property (assign) BOOL shouldDeletePrevArticles;
@property (assign) BOOL objectNeedsSaving;
@property (assign) BOOL objectIsModified;
@end
@@ -51,6 +52,7 @@
self.previousURL = @"";
self.refreshNum.intValue = 30;
self.shouldSaveObject = NO;
self.shouldDeletePrevArticles = NO;
self.objectNeedsSaving = NO;
self.objectIsModified = NO;
@@ -104,12 +106,12 @@
if (item.refreshUnit != self.refreshUnit.indexOfSelectedItem)
item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem;
if (self.feedResult) {
if (self.shouldDeletePrevArticles) {
[item.managedObjectContext performBlockAndWait:^{
Feed *rss = [StoreCoordinator createFeedFromDictionary:self.feedResult inContext:item.managedObjectContext];
if (item.feed)
[item.managedObjectContext deleteObject:(NSManagedObject*)item.feed];
item.feed = rss;
if (self.feedResult)
item.feed = [StoreCoordinator createFeedFrom:self.feedResult inContext:item.managedObjectContext];
}];
}
if ([item.managedObjectContext hasChanges]) {
@@ -138,16 +140,19 @@
- (void)controlTextDidEndEditing:(NSNotification *)obj {
if (obj.object == self.url && [self urlHasChanged]) {
self.shouldDeletePrevArticles = YES;
self.previousURL = self.url.stringValue;
self.feedResult = nil;
self.feedError = nil;
[self.spinnerURL startAnimation:nil];
[self.spinnerName startAnimation:nil];
[FeedDownload getFeed:self.previousURL withBlock:^(NSDictionary *result, NSError *error) {
[FeedDownload getFeed:self.previousURL block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) {
self.feedResult = result;
self.feedError = error; // warning indicator .hidden is bound to feedError
// TODO: play error sound?
// [httpResponse allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
// [httpResponse allHeaderFields][@"Etag"];
dispatch_async(dispatch_get_main_queue(), ^{
// TODO: play error sound?
self.feedError = error; // warning indicator .hidden is bound to feedError
self.objectNeedsSaving = YES; // stays YES if this block runs after updateRepresentedObject:
[self setTitleFromFeed];
[self.spinnerURL stopAnimation:nil];
@@ -159,7 +164,7 @@
- (void)setTitleFromFeed {
if ([self.name.stringValue isEqualToString:@""]) {
self.name.objectValue = self.feedResult[@"feed"][@"title"];
self.name.objectValue = self.feedResult.title;
}
}

View File

@@ -144,10 +144,12 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
- (FeedConfig*)insertSortedItemAtSelection {
NSIndexPath *selectedIndex = [self.dataStore selectionIndexPath];
if (selectedIndex == NULL)
selectedIndex = [NSIndexPath new];
NSIndexPath *insertIndex = selectedIndex;
FeedConfig *selected = [[[self.dataStore arrangedObjects] descendantNodeAtIndexPath:selectedIndex] representedObject];
NSUInteger lastIndex = selected.children.count;
NSUInteger lastIndex = (selected ? selected.children.count : self.dataStore.arrangedObjects.childNodes.count);
BOOL groupSelected = (selected.typ == GROUP);
if (!groupSelected) {

View File

@@ -74,7 +74,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
sleep(1);
[self performSelectorInBackground:@selector(donothing) withObject:nil];
}
// TODO: remove debugging stuff
- (void)printUnreadRecurisve:(NSMenu*)menu str:(NSString*)prefix {
for (NSMenuItem *item in menu.itemArray) {
MenuItemInfo *info = item.representedObject;
@@ -103,8 +103,8 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
self.barItem.image = [[RSSIcon templateIcon:16 tint:nil] image];
self.barItem.image.template = YES;
}
NSLog(@"==> %d", self.unreadCountTotal);
[self printUnreadRecurisve:self.barItem.menu str:@""];
// NSLog(@"==> %d", self.unreadCountTotal);
// [self printUnreadRecurisve:self.barItem.menu str:@""];
}
@@ -123,9 +123,11 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
[self defaultHeaderForMenu:menu scope:ScopeGlobal];
self.unreadCountTotal = 0;
@autoreleasepool {
for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) {
[menu addItem:[self menuItemForFeedConfig:fc unread:&_unreadCountTotal]];
}
}
[self updateBarIcon];
[menu addItem:[NSMenuItem separatorItem]];
@@ -214,7 +216,12 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""];
mi.target = self;
mi.representedObject = [MenuItemInfo withID:item.objectID unread:(item.unread ? 1 : 0)];
mi.toolTip = item.subtitle;
//mi.toolTip = item.abstract;
// TODO: Do regex during save, not during display. Its here for testing purposes ...
if (item.abstract.length > 0) {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"<[^>]*>" options:kNilOptions error:nil];
mi.toolTip = [regex stringByReplacingMatchesInString:item.abstract options:kNilOptions range:NSMakeRange(0, item.abstract.length) withTemplate:@""];
}
mi.enabled = (item.link.length > 0);
mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff);
mi.tag = ScopeLocal;
@@ -278,17 +285,17 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
self.prefWindow.window.title = [NSString stringWithFormat:@"%@ %@", NSProcessInfo.processInfo.processName,
NSLocalizedString(@"Preferences", nil)];
// one time token to set reference to nil, which will release window
NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
__block id token = [center addObserverForName:NSWindowWillCloseNotification object:self.prefWindow.window queue:nil usingBlock:^(NSNotification *note) {
self.prefWindow = nil;
[center removeObserver:token];
}];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferencesClosed:) name:NSWindowWillCloseNotification object:self.prefWindow.window];
}
[NSApp activateIgnoringOtherApps:YES];
[self.prefWindow showWindow:nil];
}
- (void)preferencesClosed:(id)sender {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:self.prefWindow.window];
self.prefWindow = nil;
}
- (void)pauseUpdates:(NSMenuItem*)sender {
NSLog(@"1pause");
@@ -441,12 +448,14 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
[[self requestFeedConfigForMenuItem:sender.parentItem] descendantFeedItems:block];
} else {
// Sadly we can't just fetch the list of FeedItems since it is not ordered (in case open 10 at a time)
@autoreleasepool {
for (FeedConfig *config in [StoreCoordinator sortedFeedConfigItems]) {
if ([config descendantFeedItems:block] == NO)
break;
}
}
}
}
/**
Recursively update all parent's unread count and total unread count.

View File

@@ -24,11 +24,12 @@
#import "DBv1+CoreDataModel.h"
#import "FeedConfig+Ext.h"
@class RSParsedFeed;
@interface StoreCoordinator : NSObject
+ (void)saveContext:(NSManagedObjectContext*)context;
+ (void)deleteUnreferencedFeeds;
+ (NSArray<FeedConfig*>*)sortedFeedConfigItems;
+ (id)objectWithID:(NSManagedObjectID*)objID;
+ (Feed*)createFeedFromDictionary:(NSDictionary*)obj inContext:(NSManagedObjectContext*)context;
+ (Feed*)createFeedFrom:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context;
@end

View File

@@ -22,6 +22,7 @@
#import "StoreCoordinator.h"
#import "AppHook.h"
#import <RSXML/RSXML.h>
@implementation StoreCoordinator
@@ -66,30 +67,23 @@
return [[self getContext] objectWithID:objID];
}
+ (Feed*)createFeedFromDictionary:(NSDictionary*)obj inContext:(NSManagedObjectContext*)context {
+ (Feed*)createFeedFrom:(RSParsedFeed*)obj inContext:(NSManagedObjectContext*)context {
Feed *a = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:context];
a.title = obj[@"feed"][@"title"];
a.subtitle = obj[@"feed"][@"subtitle"];
a.author = obj[@"feed"][@"author"];
a.link = obj[@"feed"][@"link"];
a.published = obj[@"feed"][@"published"];
a.icon = obj[@"feed"][@"icon"];
a.etag = obj[@"header"][@"etag"];
a.date = obj[@"header"][@"date"];
a.modified = obj[@"header"][@"modified"];
for (NSDictionary *entry in obj[@"entries"]) {
a.title = obj.title;
a.subtitle = obj.subtitle;
a.link = obj.link;
for (RSParsedArticle *entry in obj.articles) {
FeedItem *b = [[FeedItem alloc] initWithEntity:FeedItem.entity insertIntoManagedObjectContext:context];
b.title = entry[@"title"];
b.subtitle = entry[@"subtitle"];
b.author = entry[@"author"];
b.link = entry[@"link"];
b.published = entry[@"published"];
b.summary = entry[@"summary"];
for (NSString *tag in entry[@"tags"]) {
FeedTag *c = [[FeedTag alloc] initWithEntity:FeedTag.entity insertIntoManagedObjectContext:context];
c.name = tag;
[b addTagsObject:c];
}
b.guid = entry.guid;
b.title = entry.title;
b.abstract = entry.abstract;
b.body = entry.body;
b.author = entry.author;
b.link = entry.link;
b.published = entry.datePublished;
// TODO: remove NSLog()
if (!entry.datePublished)
NSLog(@"No date for feed '%@'", obj.urlString);
[a addItemsObject:b];
}
return a;

View File

@@ -1,29 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Foundation/Foundation.h>
@interface PyHandler : NSObject
+ (void)prepare; // must be called before getFeed
+ (void)shutdown;
+ (NSDictionary *)getFeed:(NSString *)url withEtag:(NSString *)etag andModified:(NSString *)modified;
@end

View File

@@ -1,156 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "PyHandler.h"
#import <Python/Python.h>
static PyObject *parseFeedModule;
@implementation PyHandler
+ (PyObject*)appBundlePath {
CFBundleRef mainBundle = CFBundleGetMainBundle();
CFURLRef appPath = CFBundleCopyResourcesDirectoryURL(mainBundle);
CFURLRef absolutePath = CFURLCopyAbsoluteURL(appPath);
CFStringRef path = CFURLCopyFileSystemPath(absolutePath, kCFURLPOSIXPathStyle);
PyObject * pyStr = PyString_FromString(CFStringGetCStringPtr(path, CFStringGetSystemEncoding()));
// const char *resourcePath = [[[NSBundle mainBundle] resourcePath] UTF8String];
CFRelease(path);
CFRelease(absolutePath);
CFRelease(appPath);
return pyStr;
}
+ (void)prepare {
Py_Initialize();
PyObject *sys = PyImport_Import(PyString_FromString("sys"));
PyObject *sys_path_append = PyObject_GetAttrString(PyObject_GetAttrString(sys, "path"), "append");
PyObject *resourcePath = PyTuple_New(1);
PyTuple_SetItem(resourcePath, 0, [self appBundlePath]);
PyObject_CallObject(sys_path_append, resourcePath);
// import MyModule # this is in my project folder
PyObject *myModule = PyImport_Import(PyString_FromString("getFeed"));
parseFeedModule = PyObject_GetAttrString(myModule, "parse");
}
+ (void)shutdown {
PyObject_Free(parseFeedModule);
Py_Finalize();
}
+ (char*)run:(PyObject*)args {
if (parseFeedModule && PyCallable_Check(parseFeedModule)) {
PyObject *result = PyObject_CallObject(parseFeedModule, args);
if (result != NULL && PyObject_TypeCheck(result, &PyString_Type))
return PyString_AsString(result);
}
return NULL;
}
+ (char *)run:(const char *)url withEtag:(const char *)etag andDateString:(const char *)date {
if (!Py_IsInitialized())
return NULL;
return [self run:Py_BuildValue("(z z z)", url, etag, date)];
}
+ (char *)run:(const char *)url withEtag:(const char *)etag andDateArray:(int *)d {
if (!Py_IsInitialized())
return NULL;
if (d == NULL || abs(d[8]) > 1) { // d[8] == tm_isdst (between -1 and 1). Array size must be 9
return [self run:Py_BuildValue("(z z z)", url, etag, NULL)];
}
return [self run:Py_BuildValue("(z z [iiiiiiiii])", url, etag,
d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8])];
}
+ (NSDictionary *)getFeed:(NSString *)url withEtag:(NSString *)etag andModified:(NSString *)modified {
const char* u = NULL;
const char* e = NULL;
const char* m = NULL;
if (url && url.length > 0)
u = [url UTF8String];
if (etag && etag.length > 0)
e = [etag UTF8String];
if (modified && modified.length > 0)
m = [modified UTF8String];
char *data = [self run:u withEtag:e andDateString:m];
printf("JSON result:\n%s\n\n", data);
if (data == NULL) return nil;
NSError *error = nil;
id object = [NSJSONSerialization JSONObjectWithData:
[[NSString stringWithUTF8String:data] dataUsingEncoding:NSUTF8StringEncoding]
options:0 error:&error];
if (error || !object || ![object isKindOfClass:[NSDictionary class]]) {
return nil;
}
return object;
}
// @see: https://docs.python.org/3/c-api/index.html
/* PyObject *ObjcToPyObject(id object)
{
if (object == nil) {
// This technically doesn't need to be an extra case,
// but you may want to differentiate it for error checking
return NULL;
} else if ([object isKindOfClass:[NSString class]]) {
return PyString_FromString([object UTF8String]);
} else if ([object isKindOfClass:[NSNumber class]]) {
// You could probably do some extra checking here if you need to
// with the -objCType method.
return PyLong_FromLong([object longValue]);
} else if ([object isKindOfClass:[NSArray class]]) {
// You may want to differentiate between NSArray (analagous to tuples)
// and NSMutableArray (analagous to lists) here.
Py_ssize_t i, len = [object count];
PyObject *list = PyList_New(len);
for (i = 0; i < len; ++i) {
PyObject *item = ObjcToPyObject([object objectAtIndex:i]);
NSCAssert(item != NULL, @"Can't add NULL item to Python List");
// Note that PyList_SetItem() "steals" the reference to the passed item.
// (i.e., you do not need to release it)
PyList_SetItem(list, i, item);
}
return list;
} else if ([object isKindOfClass:[NSDictionary class]]) {
PyObject *dict = PyDict_New();
for (id key in object) {
PyObject *pyKey = ObjcToPyObject(key);
NSCAssert(pyKey != NULL, @"Can't add NULL key to Python Dictionary");
PyObject *pyItem = ObjcToPyObject([object objectForKey:key]);
NSCAssert(pyItem != NULL, @"Can't add NULL item to Python Dictionary");
PyDict_SetItem(dict, pyKey, pyItem);
Py_DECREF(pyKey);
Py_DECREF(pyItem);
}
return dict;
} else {
NSLog(@"ObjcToPyObject() could not convert Obj-C object to PyObject.");
return NULL;
}
}*/
@end

File diff suppressed because it is too large Load Diff

View File

@@ -1,108 +0,0 @@
#!/usr/bin/env python
__license__ = """
The MIT License (MIT)
Copyright (c) 2018 Oleg Geier
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import feedparser521 as fp
import json
import time
COPY_ENTRY_TAGS = False
COPY_ENTRY_SUMMARY = False
def valueFormatter(key, obj):
if isinstance(obj, time.struct_time):
return list(obj)
if key == "etag":
# stupid server convention to append but not consider changed etag
# some servers append '-gzip' if gzip header is sent
return obj.replace("-gzip", "")
return obj
def copyIfExists(source, source_path, target, target_path):
src = source
trgt = target
try:
srcPTH = source_path.split("/")
trgtPTH = target_path.split("/")
for x in srcPTH[:-1]:
src = src[x]
for x in trgtPTH[:-1]:
trgt = trgt[x]
key = srcPTH[-1]
trgt[trgtPTH[-1]] = valueFormatter(key, src[key])
except Exception:
pass
def prepareResult(obj):
r = {"header": dict(), "feed": dict(), "entries": list()}
try:
if obj.debug_message.startswith("The feed has not changed since"):
obj.status = 304
except Exception:
pass
try:
r["header"]["status"] = obj.status
if obj.status == 304 or len(obj.entries) == 0:
return r
except Exception:
return r
copyIfExists(obj, "etag", r, "header/etag")
copyIfExists(obj, "modified", r, "header/modified")
copyIfExists(obj, "headers/date", r, "header/date")
copyIfExists(obj, "feed/title", r, "feed/title")
copyIfExists(obj, "feed/subtitle", r, "feed/subtitle")
copyIfExists(obj, "feed/author", r, "feed/author")
copyIfExists(obj, "feed/link", r, "feed/link")
copyIfExists(obj, "feed/image/href", r, "feed/icon")
copyIfExists(obj, "feed/published_parsed", r, "feed/published")
for entry in obj.entries:
e = dict()
copyIfExists(entry, "title", e, "title")
copyIfExists(entry, "subtitle", e, "subtitle")
copyIfExists(entry, "author", e, "author")
copyIfExists(entry, "link", e, "link")
copyIfExists(entry, "published_parsed", e, "published")
if COPY_ENTRY_SUMMARY:
copyIfExists(entry, "summary", e, "summary")
if COPY_ENTRY_TAGS:
try:
e["tags"] = list()
for tag in entry.tags:
e["tags"].append(tag.term)
except Exception:
pass
r["entries"].append(e)
return r
def parse(url, etag=None, modified=None):
if isinstance(modified, list):
modified = time.struct_time(modified)
d = fp.parse(url, etag=etag, modified=modified)
return json.dumps(prepareResult(d), separators=(',', ':'))