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

View File

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

View File

@@ -1,13 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?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"> <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"> <entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/> <attribute name="httpEtag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="date" optional="YES" attributeType="String" syncable="YES"/> <attribute name="httpModified" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="icon" optional="YES" attributeType="Binary" customClassName="NSImage" syncable="YES"/> <attribute name="icon" optional="YES" attributeType="Binary" customClassName="NSImage" syncable="YES"/>
<attribute name="link" optional="YES" attributeType="String" 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="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="title" 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"/> <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="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="refreshNum" optional="YES" attributeType="Integer 32" usesScalarValueType="YES" 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="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="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="type" optional="YES" attributeType="Integer 16" usesScalarValueType="YES" syncable="YES"/>
<attribute name="url" optional="YES" attributeType="String" 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"/> <relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedConfig" inverseName="children" inverseEntity="FeedConfig" syncable="YES"/>
</entity> </entity>
<entity name="FeedItem" representedClassName="FeedItem" syncable="YES" codeGenerationType="class"> <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="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="link" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="published" optional="YES" attributeType="Transformable" customClassName="NSArray" syncable="YES"/> <attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="summary" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/> <attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" 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="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> </entity>
<elements> <elements>
<element name="Feed" positionX="-209" positionY="-3" width="128" height="210"/> <element name="Feed" positionX="-209" positionY="-3" width="128" height="165"/>
<element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="180"/> <element name="FeedConfig" positionX="-20" positionY="-126" width="128" height="195"/>
<element name="FeedItem" positionX="-20" positionY="81" width="128" height="180"/> <element name="FeedItem" positionX="-20" positionY="81" width="128" height="180"/>
<element name="FeedTag" positionX="187" positionY="171" width="128" height="75"/>
</elements> </elements>
</model> </model>

View File

@@ -21,7 +21,8 @@
// SOFTWARE. // SOFTWARE.
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@interface FeedDownload : NSObject @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 @end

View File

@@ -21,19 +21,30 @@
// SOFTWARE. // SOFTWARE.
#import "FeedDownload.h" #import "FeedDownload.h"
#import "PyHandler.h"
@implementation FeedDownload @implementation FeedDownload
+ (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 {
[NSThread detachNewThreadWithBlock:^{ NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
NSDictionary *dict = [PyHandler getFeed:url withEtag:nil andModified:nil]; req.timeoutInterval = 30;
NSError *err = nil; req.cachePolicy = NSURLRequestReloadIgnoringCacheData;
if (!dict || [dict[@"entries"] count] == 0 ) { // [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"];
err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotParseResponse userInfo:nil]; // [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 @end

View File

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

View File

@@ -36,9 +36,10 @@
@property (copy) NSString *previousURL; @property (copy) NSString *previousURL;
@property (strong) NSError *feedError; @property (strong) NSError *feedError;
@property (strong) NSDictionary *feedResult; @property (strong) RSParsedFeed *feedResult;
@property (assign) BOOL shouldSaveObject; @property (assign) BOOL shouldSaveObject;
@property (assign) BOOL shouldDeletePrevArticles;
@property (assign) BOOL objectNeedsSaving; @property (assign) BOOL objectNeedsSaving;
@property (assign) BOOL objectIsModified; @property (assign) BOOL objectIsModified;
@end @end
@@ -51,6 +52,7 @@
self.previousURL = @""; self.previousURL = @"";
self.refreshNum.intValue = 30; self.refreshNum.intValue = 30;
self.shouldSaveObject = NO; self.shouldSaveObject = NO;
self.shouldDeletePrevArticles = NO;
self.objectNeedsSaving = NO; self.objectNeedsSaving = NO;
self.objectIsModified = NO; self.objectIsModified = NO;
@@ -104,12 +106,12 @@
if (item.refreshUnit != self.refreshUnit.indexOfSelectedItem) if (item.refreshUnit != self.refreshUnit.indexOfSelectedItem)
item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem; item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem;
if (self.feedResult) { if (self.shouldDeletePrevArticles) {
[item.managedObjectContext performBlockAndWait:^{ [item.managedObjectContext performBlockAndWait:^{
Feed *rss = [StoreCoordinator createFeedFromDictionary:self.feedResult inContext:item.managedObjectContext];
if (item.feed) if (item.feed)
[item.managedObjectContext deleteObject:(NSManagedObject*)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]) { if ([item.managedObjectContext hasChanges]) {
@@ -138,16 +140,19 @@
- (void)controlTextDidEndEditing:(NSNotification *)obj { - (void)controlTextDidEndEditing:(NSNotification *)obj {
if (obj.object == self.url && [self urlHasChanged]) { if (obj.object == self.url && [self urlHasChanged]) {
self.shouldDeletePrevArticles = YES;
self.previousURL = self.url.stringValue; self.previousURL = self.url.stringValue;
self.feedResult = nil; self.feedResult = nil;
self.feedError = nil; self.feedError = nil;
[self.spinnerURL startAnimation:nil]; [self.spinnerURL startAnimation:nil];
[self.spinnerName 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.feedResult = result;
self.feedError = error; // warning indicator .hidden is bound to feedError // [httpResponse allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
// TODO: play error sound? // [httpResponse allHeaderFields][@"Etag"];
dispatch_async(dispatch_get_main_queue(), ^{ 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.objectNeedsSaving = YES; // stays YES if this block runs after updateRepresentedObject:
[self setTitleFromFeed]; [self setTitleFromFeed];
[self.spinnerURL stopAnimation:nil]; [self.spinnerURL stopAnimation:nil];
@@ -159,7 +164,7 @@
- (void)setTitleFromFeed { - (void)setTitleFromFeed {
if ([self.name.stringValue isEqualToString:@""]) { 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 { - (FeedConfig*)insertSortedItemAtSelection {
NSIndexPath *selectedIndex = [self.dataStore selectionIndexPath]; NSIndexPath *selectedIndex = [self.dataStore selectionIndexPath];
if (selectedIndex == NULL)
selectedIndex = [NSIndexPath new];
NSIndexPath *insertIndex = selectedIndex; NSIndexPath *insertIndex = selectedIndex;
FeedConfig *selected = [[[self.dataStore arrangedObjects] descendantNodeAtIndexPath:selectedIndex] representedObject]; 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); BOOL groupSelected = (selected.typ == GROUP);
if (!groupSelected) { if (!groupSelected) {

View File

@@ -74,7 +74,7 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
sleep(1); sleep(1);
[self performSelectorInBackground:@selector(donothing) withObject:nil]; [self performSelectorInBackground:@selector(donothing) withObject:nil];
} }
// TODO: remove debugging stuff
- (void)printUnreadRecurisve:(NSMenu*)menu str:(NSString*)prefix { - (void)printUnreadRecurisve:(NSMenu*)menu str:(NSString*)prefix {
for (NSMenuItem *item in menu.itemArray) { for (NSMenuItem *item in menu.itemArray) {
MenuItemInfo *info = item.representedObject; 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 = [[RSSIcon templateIcon:16 tint:nil] image];
self.barItem.image.template = YES; self.barItem.image.template = YES;
} }
NSLog(@"==> %d", self.unreadCountTotal); // NSLog(@"==> %d", self.unreadCountTotal);
[self printUnreadRecurisve:self.barItem.menu str:@""]; // [self printUnreadRecurisve:self.barItem.menu str:@""];
} }
@@ -123,8 +123,10 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
[self defaultHeaderForMenu:menu scope:ScopeGlobal]; [self defaultHeaderForMenu:menu scope:ScopeGlobal];
self.unreadCountTotal = 0; self.unreadCountTotal = 0;
for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) { @autoreleasepool {
[menu addItem:[self menuItemForFeedConfig:fc unread:&_unreadCountTotal]]; for (FeedConfig *fc in [StoreCoordinator sortedFeedConfigItems]) {
[menu addItem:[self menuItemForFeedConfig:fc unread:&_unreadCountTotal]];
}
} }
[self updateBarIcon]; [self updateBarIcon];
@@ -214,7 +216,12 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""]; NSMenuItem *mi = [[NSMenuItem alloc] initWithTitle:item.title action:@selector(openFeedURL:) keyEquivalent:@""];
mi.target = self; mi.target = self;
mi.representedObject = [MenuItemInfo withID:item.objectID unread:(item.unread ? 1 : 0)]; 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.enabled = (item.link.length > 0);
mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff); mi.state = (item.unread ? NSControlStateValueOn : NSControlStateValueOff);
mi.tag = ScopeLocal; mi.tag = ScopeLocal;
@@ -278,17 +285,17 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"]; self.prefWindow = [[Preferences alloc] initWithWindowNibName:@"Preferences"];
self.prefWindow.window.title = [NSString stringWithFormat:@"%@ %@", NSProcessInfo.processInfo.processName, self.prefWindow.window.title = [NSString stringWithFormat:@"%@ %@", NSProcessInfo.processInfo.processName,
NSLocalizedString(@"Preferences", nil)]; NSLocalizedString(@"Preferences", nil)];
// one time token to set reference to nil, which will release window [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferencesClosed:) name:NSWindowWillCloseNotification object:self.prefWindow.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];
}];
} }
[NSApp activateIgnoringOtherApps:YES]; [NSApp activateIgnoringOtherApps:YES];
[self.prefWindow showWindow:nil]; [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 { - (void)pauseUpdates:(NSMenuItem*)sender {
NSLog(@"1pause"); NSLog(@"1pause");
@@ -441,9 +448,11 @@ typedef NS_OPTIONS(NSInteger, MenuItemTag) {
[[self requestFeedConfigForMenuItem:sender.parentItem] descendantFeedItems:block]; [[self requestFeedConfigForMenuItem:sender.parentItem] descendantFeedItems:block];
} else { } else {
// Sadly we can't just fetch the list of FeedItems since it is not ordered (in case open 10 at a time) // Sadly we can't just fetch the list of FeedItems since it is not ordered (in case open 10 at a time)
for (FeedConfig *config in [StoreCoordinator sortedFeedConfigItems]) { @autoreleasepool {
if ([config descendantFeedItems:block] == NO) for (FeedConfig *config in [StoreCoordinator sortedFeedConfigItems]) {
break; if ([config descendantFeedItems:block] == NO)
break;
}
} }
} }
} }

View File

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

View File

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