diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-09-08 10:38:39 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-09-08 10:38:39 +0200 |
| commit | a5e25cc05e5e33e114331cc826211c89adbcfa6a (patch) | |
| tree | 2ea647b5117e5c3b530ec66d4caafd04969e0fbe /Providers | |
| parent | 0f26c5c3b3e606c89724067d4785323de3fee751 (diff) | |
Reorganize code, WIP save for tag handling
Diffstat (limited to 'Providers')
| -rw-r--r-- | Providers/QSDeliciousAPIProvider.h | 25 | ||||
| -rw-r--r-- | Providers/QSDeliciousAPIProvider.m | 193 | ||||
| -rw-r--r-- | Providers/QSLinkdingProvider.h | 16 | ||||
| -rw-r--r-- | Providers/QSLinkdingProvider.m | 192 |
4 files changed, 426 insertions, 0 deletions
diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h new file mode 100644 index 0000000..0064264 --- /dev/null +++ b/Providers/QSDeliciousAPIProvider.h @@ -0,0 +1,25 @@ +// +// QSDeliciousAPIProvider.h +// QSDeliciousPlugIn +// +// Base provider for Pinboard v1 API (XML-based with Basic Auth) +// Used by Delicious, Magnolia, and Pinboard +// + +#import <Foundation/Foundation.h> +#import "QSBookmarkProvider.h" + +@interface QSDeliciousAPIProvider : NSObject <QSBookmarkProvider, NSXMLParserDelegate> + +@property (nonatomic, strong) NSMutableArray *posts; +@property (nonatomic, assign) SocialSite site; + +- (instancetype)initWithSite:(SocialSite)site; + +// Subclasses can override these methods +- (NSString *)apiURLForSite:(SocialSite)site; +- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host; +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username; + +@end diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m new file mode 100644 index 0000000..ab09f88 --- /dev/null +++ b/Providers/QSDeliciousAPIProvider.m @@ -0,0 +1,193 @@ +// +// QSDeliciousAPIProvider.m +// QSDeliciousPlugIn +// + +#import "QSDeliciousAPIProvider.h" +#import "SocialSite.h" +#import <QSCore/QSCore.h> + +@implementation QSDeliciousAPIProvider + +- (instancetype)initWithSite:(SocialSite)site { + self = [super init]; + if (self) { + _site = site; + _posts = [NSMutableArray array]; + } + return self; +} + +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + return (site == self.site) && + username.length > 0 && + password.length > 0 && + (site != SocialSiteLinkding); // This provider doesn't handle Linkding +} + +- (SocialSite)supportedSite { + return self.site; +} + +- (NSString *)providerName { + return [SocialSiteHelper displayNameForSite:self.site]; +} + +- (NSString *)apiURLForSite:(SocialSite)site { + switch (site) { + case SocialSiteDelicious: + return @"api.del.icio.us/v1"; + case SocialSiteMagnolia: + return @"ma.gnolia.com/api/mirrord/v1"; + case SocialSitePinboard: + return @"api.pinboard.in/v1"; + default: + return nil; + } +} + +- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSString *apiURL = [self apiURLForSite:site]; + if (!apiURL) return nil; + + NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL]; + return [NSURL URLWithString:urlString]; +} + +- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username { + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username { + NSString *siteURL = [SocialSiteHelper siteURLForSite:site]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSString *)tagURLType { + return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { + + // Try cached data first + NSData *data = [self cachedBookmarkDataForSite:site username:username]; + + // If no cached data, fetch from API + if (![data length]) { + NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host]; + if (!requestURL) return @[]; + + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"POST"]; + [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; + + NSError *error; + data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; + + if (error) { + NSLog(@"Error fetching bookmarks: %@", error.localizedDescription); + return @[]; + } + + // Cache the data + [self cacheBookmarkData:data forSite:site username:username]; + } + + // Parse XML data + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *post in self.posts) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSString *tagString = [post objectForKey:@"tag"]; + if (tagString.length > 0) { + [tagSet addObjectsFromArray:[tagString componentsSeparatedByString:@" "]]; + } + } + } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [self providerName], tag]]; + [tagObject setObject:tag forType:[self tagURLType]]; + [tagObject setObject:@(site) forMeta:@"source.site"]; + [tagObject setObject:username forMeta:@"source.username"]; + [tagObject setObject:host forMeta:@"source.host"]; + [tagObject setObject:identifier forMeta:@"source.identifier"]; + [tagObject setName:tag]; + [tagObject setPrimaryType:[self tagURLType]]; + [objects addObject:tagObject]; + } + } + } + + return objects; +} + +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForSite:site username:username]; + if (!data) return @[]; + + NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data]; + [postParser setDelegate:self]; + self.posts = [NSMutableArray arrayWithCapacity:1]; + [postParser parse]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *post in self.posts) { + NSString *postTags = [post objectForKey:@"tag"]; + if ([postTags rangeOfString:tag].location != NSNotFound) { + QSObject *newObject = [self objectForPost:post]; + if (newObject) { + [objects addObject:newObject]; + } + } + } + + return objects; +} + +- (QSObject *)objectForPost:(NSDictionary *)post { + QSObject *newObject = [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]]; + [newObject setObject:[post objectForKey:@"href"] forType:QSURLType]; + [newObject setName:[post objectForKey:@"description"]]; + [newObject setDetails:[post objectForKey:@"extended"]]; + [newObject setPrimaryType:QSURLType]; + return newObject; +} + +#pragma mark - NSXMLParserDelegate + +- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { + if ([elementName isEqualToString:@"post"] && attributeDict) { + [self.posts addObject:attributeDict]; + } +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { + // Implementation if needed +} + +@end diff --git a/Providers/QSLinkdingProvider.h b/Providers/QSLinkdingProvider.h new file mode 100644 index 0000000..28807e8 --- /dev/null +++ b/Providers/QSLinkdingProvider.h @@ -0,0 +1,16 @@ +// +// QSLinkdingProvider.h +// QSDeliciousPlugIn +// +// Provider for Linkding API (JSON-based with API key) +// + +#import <Foundation/Foundation.h> +#import "QSBookmarkProvider.h" + +@interface QSLinkdingProvider : NSObject <QSBookmarkProvider> + +- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username; +- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username; + +@end
\ No newline at end of file diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m new file mode 100644 index 0000000..094f318 --- /dev/null +++ b/Providers/QSLinkdingProvider.m @@ -0,0 +1,192 @@ +// +// QSLinkdingProvider.m +// QSDeliciousPlugIn +// + +#import "QSLinkdingProvider.h" +#import "SocialSite.h" +#import <QSCore/QSCore.h> + +@implementation QSLinkdingProvider + +- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + return (site == SocialSiteLinkding) && + username.length > 0 && + password.length > 0 && // password is API token for Linkding + host.length > 0; +} + +- (SocialSite)supportedSite { + return SocialSiteLinkding; +} + +- (NSString *)providerName { + return @"Linkding"; +} + +- (NSString *)tagURLType { + return @"tag.linkding"; +} + +- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; + return [NSData dataWithContentsOfFile:cachePath]; +} + +- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username { + // Create a safe filename from host + NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"]; + NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]]; + [data writeToFile:cachePath atomically:NO]; +} + +- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags { + + if (![self canHandleSite:site username:username password:password host:host]) { + return @[]; + } + + // Try cached data first + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + + // If no cached data, fetch from API + if (![data length]) { + // Construct Linkding API URL + NSString *baseURL = host; + if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) { + baseURL = [NSString stringWithFormat:@"https://%@", baseURL]; + } + if ([baseURL hasSuffix:@"/"]) { + baseURL = [baseURL substringToIndex:[baseURL length] - 1]; + } + + NSString *urlString = [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL]; + NSURL *requestURL = [NSURL URLWithString:urlString]; + + if (!requestURL) { + NSLog(@"Invalid Linkding host URL: %@", host); + return @[]; + } + + NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + [theRequest setHTTPMethod:@"GET"]; + [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] forHTTPHeaderField:@"Authorization"]; + [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"]; + + NSError *error = nil; + data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error]; + + if (error) { + NSLog(@"Error fetching Linkding bookmarks: %@", error.localizedDescription); + return @[]; + } + + // Cache the data + [self cacheBookmarkData:data forHost:host username:username]; + } + + // Parse JSON data + NSError *jsonError = nil; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) { + NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription); + return @[]; + } + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) { + NSLog(@"Invalid Linkding response format"); + return @[]; + } + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + NSMutableSet *tagSet = [NSMutableSet set]; + + // Create bookmark objects + for (NSDictionary *bookmark in results) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + + // Collect tags if requested + if (includeTags) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]]) { + [tagSet addObjectsFromArray:tags]; + } + } + } + } + + // Create tag objects if requested + if (includeTags) { + for (NSString *tag in tagSet) { + if (tag.length > 0) { + QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]]; + [tagObject setObject:tag forType:[self tagURLType]]; + [tagObject setObject:@(site) forMeta:@"source.site"]; + [tagObject setObject:username forMeta:@"source.username"]; + [tagObject setObject:host forMeta:@"source.host"]; + // We need the identifier to be able to fetch the keychain password + [tagObject setObject:identifier forMeta:@"source.identifier"]; + [tagObject setName:tag]; + [tagObject setPrimaryType:[self tagURLType]]; + [objects addObject:tagObject]; + } + } + } + + return objects; +} + +- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host { + NSData *data = [self cachedBookmarkDataForHost:host username:username]; + if (!data) return @[]; + + NSError *jsonError; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (jsonError) return @[]; + + NSArray *results = [jsonResponse objectForKey:@"results"]; + if (!results || ![results isKindOfClass:[NSArray class]]) return @[]; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1]; + + for (NSDictionary *bookmark in results) { + NSArray *tags = [bookmark objectForKey:@"tag_names"]; + if (tags && [tags isKindOfClass:[NSArray class]] && [tags containsObject:tag]) { + QSObject *newObject = [self objectForLinkdingBookmark:bookmark]; + if (newObject) { + [objects addObject:newObject]; + } + } + } + + return objects; +} + +- (QSObject *)objectForLinkdingBookmark:(NSDictionary *)bookmark { + NSNumber *bookmarkId = [bookmark objectForKey:@"id"]; + NSString *url = [bookmark objectForKey:@"url"]; + NSString *title = [bookmark objectForKey:@"title"]; + NSString *description = [bookmark objectForKey:@"description"]; + + if (!bookmarkId || !url) return nil; + + QSObject *newObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", bookmarkId]]; + [newObject setObject:url forType:QSURLType]; + [newObject setName:title.length > 0 ? title : url]; + [newObject setDetails:description.length > 0 ? description : @""]; + [newObject setPrimaryType:QSURLType]; + + return newObject; +} + +@end |