├── .gitignore
├── Gruntfile.js
├── Pinboard-Actions.png
├── Pinboard.lbext
└── Contents
│ ├── Info.plist
│ └── Resources
│ └── Actions
│ ├── Pinboard Log In.lbaction
│ └── Contents
│ │ ├── Info.plist
│ │ ├── Resources
│ │ ├── BookmarkTemplate.icns
│ │ ├── PinboardTemplate.icns
│ │ ├── TagTemplate.icns
│ │ └── pinboard.jpg
│ │ └── Scripts
│ │ └── pinboard-login.js
│ ├── Pinboard Recent.lbaction
│ └── Contents
│ │ ├── Info.plist
│ │ ├── Resources
│ │ ├── BookmarkTemplate.icns
│ │ ├── PinboardTemplate.icns
│ │ └── TagTemplate.icns
│ │ └── Scripts
│ │ ├── pinboard-recent.js
│ │ └── shared.js
│ ├── Pinboard Search.lbaction
│ └── Contents
│ │ ├── Info.plist
│ │ ├── Resources
│ │ ├── BookmarkTemplate.icns
│ │ ├── PinboardTemplate.icns
│ │ └── TagTemplate.icns
│ │ └── Scripts
│ │ ├── pinboard-search.js
│ │ ├── search.js
│ │ └── shared.js
│ ├── Pinboard Tags.lbaction
│ └── Contents
│ │ ├── Info.plist
│ │ ├── Resources
│ │ ├── BookmarkTemplate.icns
│ │ ├── PinboardTemplate.icns
│ │ └── TagTemplate.icns
│ │ └── Scripts
│ │ ├── pinboard-tags.js
│ │ └── shared.js
│ └── Pinboard Unread.lbaction
│ └── Contents
│ ├── Info.plist
│ ├── Resources
│ ├── BookmarkTemplate.icns
│ ├── PinboardTemplate.icns
│ └── TagTemplate.icns
│ └── Scripts
│ ├── pinboard-unread.js
│ └── shared.js
├── README.md
├── launchbar-pinboard.sublime-project
├── package.json
├── shared
└── Contents
│ ├── Resources
│ ├── BookmarkTemplate.icns
│ ├── PinboardTemplate.icns
│ ├── TagTemplate.icns
│ ├── bookmark.acorn
│ ├── icons.iconsproj
│ ├── icons.sketch
│ │ ├── Data
│ │ ├── metadata
│ │ └── version
│ ├── pinboard.acorn
│ └── tag.acorn
│ └── Scripts
│ └── shared.js
└── test
└── test-searching.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.sublime-workspace
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | var ACTIONS_DIR = 'Pinboard.lbext/Contents/Resources/Actions/';
4 |
5 |
6 | module.exports = function(grunt) {
7 | grunt.initConfig({
8 |
9 | clean: {
10 | build: '**/shared+*.js'
11 | },
12 |
13 | copy: {
14 | sharedResources: {
15 | files: [
16 | {
17 | expand: true,
18 | cwd: 'shared',
19 | src: ['Contents/Resources/**/*.icns'],
20 | dest: ACTIONS_DIR + 'Pinboard Log In.lbaction/'
21 | },
22 | {
23 | expand: true,
24 | cwd: 'shared',
25 | src: ['Contents/Resources/**/*.icns', 'Contents/Scripts/*.js'],
26 | dest: ACTIONS_DIR + 'Pinboard Recent.lbaction/'
27 | },
28 | {
29 | expand: true,
30 | cwd: 'shared',
31 | src: ['Contents/Resources/**/*.icns', 'Contents/Scripts/*.js'],
32 | dest: ACTIONS_DIR + 'Pinboard Unread.lbaction/'
33 | },
34 | {
35 | expand: true,
36 | cwd: 'shared',
37 | src: ['Contents/Resources/**/*.icns', 'Contents/Scripts/*.js'],
38 | dest: ACTIONS_DIR + 'Pinboard Tags.lbaction/'
39 | },
40 | {
41 | expand: true,
42 | cwd: 'shared',
43 | src: ['Contents/Resources/**/*.icns', 'Contents/Scripts/*.js'],
44 | dest: ACTIONS_DIR + 'Pinboard Search.lbaction/'
45 | }
46 | ]
47 | },
48 |
49 | installActions: {
50 | cwd: 'Pinboard.lbext/Contents/Resources/Actions',
51 | expand: true,
52 | src: [
53 | 'Pinboard Unread.lbaction/**',
54 | 'Pinboard Recent.lbaction/**',
55 | 'Pinboard Log In.lbaction/**',
56 | 'Pinboard Tags.lbaction/**',
57 | 'Pinboard Search.lbaction/**'],
58 | dest: path.join(
59 | process.env.HOME || process.env.USERPROFILE,
60 | 'Library/Application Support/LaunchBar/Actions/')
61 | }
62 | },
63 |
64 | watch: {
65 | install: {
66 | files: ['**/*.js', 'shared/**/*.icns', '*/*/Info.plist', '!node_modules/**/*.js'],
67 | tasks: ['default']
68 | }
69 | },
70 |
71 | jshint: {
72 | options: {
73 | laxbreak: true
74 | },
75 | scripts: ['Gruntfile.js', 'shared/**/*.js', '*.lbaction/**/*.js']
76 | },
77 |
78 | mochaTest: {
79 | test: {
80 | src: 'test/*.js'
81 | }
82 | }
83 | });
84 |
85 | require('load-grunt-tasks')(grunt);
86 |
87 | grunt.registerTask('default', ['jshint', 'copy:sharedResources', 'copy:installActions']);
88 |
89 | };
90 |
--------------------------------------------------------------------------------
/Pinboard-Actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard-Actions.png
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleName
6 | Pinboard for LaunchBar
7 | CFBundleVersion
8 | 1.5.3
9 | LBDescription
10 |
11 | LBAuthor
12 | Jay Gillibrand
13 | LBSummary
14 | Searches and lists your Pinboard.in bookmarks.
15 | LBWebsite
16 | https://github.com/gillibrand/launchbar-pinboard
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | gillibrand.jay.pinboard.login
7 | CFBundleName
8 | Pinboard: Log In
9 | LBTextInputTitle
10 | API Token
11 | CFBundleIconFile
12 | PinboardTemplate.icns
13 | LBAbbreviation
14 | pbl
15 | LBDebugLogEnabled
16 |
17 | CFBundleVersion
18 | 1.4.3
19 | LBScripts
20 |
21 | LBDefaultScript
22 |
23 | LBAcceptedArgumentTypes
24 |
25 | string
26 |
27 | LBScriptName
28 | pinboard-login.js
29 | LBReturnsResult
30 |
31 | LBRequiresArgument
32 |
33 | LBLiveFeedbackEnabled
34 |
35 |
36 |
37 | LBDescription
38 |
39 | LBAuthor
40 | Jay Gillibrand
41 | LBWebsite
42 | https://github.com/gillibrand/launchbar-pinboard
43 | LBSummary
44 | Required to log in to your Pinboard account. Used by other Pinboard actions.
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/BookmarkTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/BookmarkTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/PinboardTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/PinboardTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/TagTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/TagTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/pinboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Resources/pinboard.jpg
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Log In.lbaction/Contents/Scripts/pinboard-login.js:
--------------------------------------------------------------------------------
1 | function run(apiToken) {
2 | var results = [];
3 |
4 | results.push({
5 | title: 'Enter your Pinboard API token',
6 | subtitle: 'Enter you API token here to access your Pinboard account from LaunchBar',
7 | icon: 'PinboardTemplate.icns',
8 | action: 'saveApiToken',
9 | actionArgument: apiToken,
10 | });
11 |
12 | results.push({
13 | title: 'Look up your API token',
14 | subtitle: 'View your API token on Pinboard',
15 | url: 'https://pinboard.in/settings/password'
16 | });
17 |
18 | if (File.exists(apiTokenPath())) {
19 | results.push({
20 | title: 'Log Out',
21 | subtitle: 'Delete your saved API token',
22 | action: 'deleteApiToken'
23 | });
24 | }
25 |
26 | return results;
27 | }
28 |
29 | function apiTokenPath() {
30 | return Action.supportPath + '/api-token.txt';
31 | }
32 |
33 | function saveApiToken(apiToken) {
34 | // Test with a known Pinboard URL. Any non-error means it's good.
35 | var result = HTTP.getJSON('https://api.pinboard.in/v1/user/secret?format=json&auth_token=' + apiToken);
36 |
37 | if (result.response.status !== 200) {
38 | LaunchBar.alert(
39 | 'Unable to log in to Pinboard.',
40 | 'The API token you entered was not accepted. Try copying and pasting the API token from your Pinboard settings and try again. \n\nPinboard said: ' + result.response.status + ' ' + result.response.localizedStatus);
41 | LaunchBar.performAction('Pinboard: Log In');
42 | return;
43 | }
44 |
45 | // Multiple actions want to access this, so save in a common location instead of the support path just for this action.
46 | File.writeText(apiToken, apiTokenPath());
47 |
48 | LaunchBar.displayNotification({
49 | string: 'Successfully logged in to Pinboard'
50 | });
51 | }
52 |
53 | function deleteApiToken() {
54 | try {
55 | LaunchBar.execute('/bin/bash', '-c', 'rm \'' + apiTokenPath() + '\'');
56 | }
57 | catch (e) {
58 | LaunchBar.log('Log Out failed: ' + e);
59 | LaunchBar.alert(
60 | 'Could not log out.',
61 | 'Your API token file was not deleted. You can try manually deleting the file at ' + apiTokenPath());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | gillibrand.jay.pinboard-recent
7 | CFBundleIconFile
8 | PinboardTemplate.icns
9 | CFBundleName
10 | Pinboard: Recent Bookmarks
11 | LBAbbreviation
12 | pbr
13 | LBDebugLogEnabled
14 |
15 | CFBundleVersion
16 | 1.4.3
17 | LBScripts
18 |
19 | LBDefaultScript
20 |
21 | LBAcceptedArgumentTypes
22 |
23 | string
24 |
25 | LBScriptName
26 | pinboard-recent.js
27 | LBReturnsResult
28 |
29 | LBRequiresArgument
30 |
31 | LBLiveFeedbackEnabled
32 |
33 |
34 |
35 | LBDescription
36 |
37 | LBAuthor
38 | Jay Gillibrand
39 | LBWebsite
40 | https://github.com/gillibrand/launchbar-pinboard
41 | LBSummary
42 | Lists you most recent Pinboard bookmarks.
43 | LBResult
44 | Up to 25 recent bookmarks.
45 | LBRequirements
46 | "Pinboard Log In" action.
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Resources/BookmarkTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Resources/BookmarkTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Resources/PinboardTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Resources/PinboardTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Resources/TagTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Resources/TagTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Scripts/pinboard-recent.js:
--------------------------------------------------------------------------------
1 | include('shared.js');
2 |
3 | function run() {
4 | if (!loadApiToken()) return loginErrorAsListResults();
5 |
6 | var data = getUrl('https://api.pinboard.in/v1/posts/recent', {
7 | count: 25
8 | });
9 | if (!data) return;
10 |
11 | return postsAsListResults(data.posts);
12 | }
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Recent.lbaction/Contents/Scripts/shared.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This script is copied (grunt) to each action directory so they can share
3 | * common log in and HTTP request behaviour. It is included in each action with
4 | * the LaunchBar specific `include` function, but this seems to require the
5 | * shared script to be in the same directory as the calling script.
6 | */
7 |
8 | /**
9 | * Just globally store the API token after reading from the file.
10 | */
11 | var apiToken_;
12 |
13 | /**
14 | * Reads the saved API token from a support file.
15 | * If the file cannot be read, prompts the user for
16 | * the API token to save.
17 | *
18 | * @return {boolean} true if the token was loaded (user is logged in).
19 | */
20 | function loadApiToken() {
21 | var file = Action.supportPath;
22 | // LaunchBar 6.1 added a trailing slash to this path. Try to work with either.
23 | var tailRe = /Action Support\/?/;
24 | var m = tailRe.exec(file);
25 | var tailIndex = m.index;
26 | var tailLength = m[0].length;
27 | // The file is save in the support folder for the Pinboard Log In action.
28 | // Different actions can't share a support folder, so just hard-code where it is.
29 | file = file.slice(0, tailIndex + tailLength) + 'gillibrand.jay.pinboard.login/api-token.txt';
30 |
31 | try {
32 | LaunchBar.log('Will read API toke file at: ' + file);
33 | apiToken_ = File.readText(file);
34 | return true;
35 | }
36 | catch (e) {
37 | LaunchBar.log('Failed to read log in token. ' + e);
38 | return false;
39 | }
40 | }
41 |
42 | /**
43 | * Return a single action result to defers to the login in action.
44 | * Used from other actions that can't get the API token (not logged in yet).
45 | *
46 | * @return {array} single action to log in.
47 | */
48 | function loginErrorAsListResults() {
49 | return [{
50 | title: 'Log In to Pinboard',
51 | subtitle: 'Continue to log in with LaunchBar.',
52 | actionBundleIdentifier: 'gillibrand.jay.pinboard.login'
53 | }];
54 | }
55 |
56 | /**
57 | * HTTP GET a give Pinboard. Automatically requests JSON format
58 | * and include the API token (which must be loaded with loadApiToken first).
59 | *
60 | * Will automatically prompt for a new API token if hits a 401.
61 | *
62 | * @param {string} url URL to load.
63 | * @param {object} params optional hash of parameter names to values.
64 | * @return {object} the JSON result. null if not loaded.
65 | */
66 | function getUrl(url, params) {
67 | url = url + '?format=json&auth_token=' + apiToken_;
68 |
69 | if (params) {
70 | for (var name in params) {
71 | url += '&' + name + '=' + params[name];
72 | }
73 | }
74 |
75 | LaunchBar.debugLog('GET ' + url);
76 |
77 | var result = HTTP.getJSON(url);
78 |
79 | if (result.data) return result.data;
80 |
81 | if (result.response.status === 401) {
82 | LaunchBar.alert(
83 | 'You are no longer logged in to Pinboard through LaunchBar.',
84 | 'Your API token is wrong or has expired. You must enter it again.');
85 | LaunchBar.performAction('Pinboard: Log In');
86 | return null;
87 | }
88 | else {
89 | LaunchBar.alert(
90 | 'Could not contact Pinboard.',
91 | 'This may be a temporary error. Try again later.\n\nPinboard said: ' + result.response.status + ' ' + result.response.localizedStatus);
92 | return null;
93 | }
94 | }
95 |
96 | /**
97 | * Converts Piboard post (bookmark) JSON objects to list results
98 | * to present in LaunchBar.
99 | * @param {array} posts the Pinboard post objects as returned from HTTP JSON requests.
100 | * @return {array} LaunchBar results.
101 | */
102 | function postsAsListResults(posts) {
103 | return posts.map(function(post) {
104 | var result = {
105 | url: post.href,
106 | icon: 'BookmarkTemplate.icns'
107 | };
108 |
109 | result.title = post.description
110 | ? post.description
111 | : post.href;
112 |
113 | result.subtitle = post.extended
114 | ? post.extended
115 | : post.href;
116 |
117 | if (post.tags) {
118 | result.title = result.title + ' (' + post.tags + ')';
119 | }
120 |
121 | return result;
122 | });
123 | }
124 |
125 | function clearCachedAllPosts() {
126 | Action.preferences.lastCacheTime = null;
127 | LaunchBar.displayNotification({
128 | subtitle: 'Pinboard for LaunchBar',
129 | string: 'Cleared cached Pinboard bookmarks.'
130 | });
131 | }
132 |
133 | /**
134 | * Gets potentially all posts. Reads them from the cache file if available and
135 | * it's less than 5 minutes old (as recommended by Pinboard.in for be-nice rate
136 | * limiting).
137 | *
138 | * Unfortunately the cache is per-action since this shared script is actually
139 | * copied to each action, and there is no trivial way to share preferences and
140 | * support directories between actions.
141 | *
142 | * @param {object} params extra params to append to the "all" request.
143 | * If the results are already cached, this is ignored (ugh).
144 | * @return {array} all posts, possibly from the cache. May be filtered
145 | * if params were passed.
146 | */
147 | function getAllPosts(params) {
148 | var posts = getCachedAllPosts();
149 |
150 | if (!posts) {
151 | posts = cacheAllPosts(params);
152 | }
153 |
154 | return posts;
155 | }
156 |
157 |
158 | // duplicated from search.js since that's not "shared" will all actions.
159 | // TODO: clean up. Probably put all search.js into shared for simplicity.
160 | function indexText(text) {
161 | return text.toLowerCase();
162 | }
163 |
164 | var ALL_POSTS_FILE = Action.supportPath + '/all-posts.json';
165 |
166 | /**
167 | * Gets all posts and caches in the files system. Updates
168 | * a "last cache" time in the preferences of the current action.
169 | * @param {object} params extra params to pass to the "all" request.
170 | * @return {array} all posts.
171 | */
172 | function cacheAllPosts(params) {
173 | LaunchBar.log('Caching all bookmarks for local searching.');
174 | var allPosts = getUrl('https://api.pinboard.in/v1/posts/all', params);
175 |
176 | var simplePosts = allPosts.map(function(post) {
177 | var indexedText = indexText(post.description + ' ' + post.extended + ' ' + post.tags);
178 | post.indexedText = indexedText;
179 | return post;
180 | });
181 |
182 | File.writeJSON({
183 | simplePosts: simplePosts
184 | }, ALL_POSTS_FILE);
185 |
186 | Action.preferences.lastCacheTime = Date.now();
187 |
188 | return simplePosts;
189 | }
190 |
191 | /**
192 | * Gets all the cached posts or null if there are none
193 | * or the cache time expired.
194 | * @return {array} null or array of cached posts.
195 | */
196 | function getCachedAllPosts() {
197 | var lastCacheTime = Action.preferences.lastCacheTime;
198 |
199 | if (!lastCacheTime) {
200 | LaunchBar.log('List of all my posts was never cached.');
201 | return null;
202 | }
203 |
204 | var diffMillis = Date.now() - lastCacheTime;
205 |
206 | if (diffMillis / 1000.0 / 60.0 >= 5) {
207 | LaunchBar.log('List of all my posts is older than 5 mins. May re-cache all posts...');
208 |
209 | var lastUpdateIso = Action.preferences.lastUpdateIso;
210 |
211 | var updateIso = getUrl('https://api.pinboard.in/v1/posts/update').update_time;
212 | LaunchBar.debugLog('Last update time: ' + updateIso);
213 | Action.preferences.lastUpdateIso = updateIso;
214 |
215 | if (!lastUpdateIso) {
216 | LaunchBar.log('No last update time was found. Will re-cache.');
217 | return null;
218 | }
219 |
220 | if (updateIso !== lastUpdateIso) {
221 | LaunchBar.log('Bookmarks were updated. Will re-cache.');
222 | return null;
223 | }
224 | }
225 |
226 | try {
227 | var simplePosts = File.readJSON(ALL_POSTS_FILE).simplePosts;
228 | LaunchBar.log('Using cached list of all my posts.');
229 | return simplePosts;
230 | }
231 | catch (e) {
232 | LaunchBar.log('No cached all-posts files.');
233 | return null;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | gillibrand.jay.pinboard-search
7 | CFBundleIconFile
8 | PinboardTemplate.icns
9 | LBTextInputTitle
10 | Search
11 | CFBundleName
12 | Pinboard: Search Bookmarks
13 | LBAbbreviation
14 | pbs
15 | LBDebugLogEnabled
16 |
17 | CFBundleVersion
18 | 1.4.3
19 | LBScripts
20 |
21 | LBDefaultScript
22 |
23 | LBAcceptedArgumentTypes
24 |
25 | string
26 |
27 | LBScriptName
28 | pinboard-search.js
29 | LBReturnsResult
30 |
31 | LBLiveFeedbackEnabled
32 |
33 | LBRequiresArgument
34 |
35 |
36 |
37 | LBDescription
38 |
39 | LBAuthor
40 | Jay Gillibrand
41 | LBWebsite
42 | https://github.com/gillibrand/launchbar-pinboard
43 | LBSummary
44 | Searches all your bookmarks.
45 | LBResult
46 | Your Pinboard bookmarks that have a matching title, description, tag, or URL.
47 | LBRequirements
48 | "Pinboard Log In" action.
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Resources/BookmarkTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Resources/BookmarkTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Resources/PinboardTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Resources/PinboardTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Resources/TagTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Resources/TagTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Scripts/pinboard-search.js:
--------------------------------------------------------------------------------
1 | include('shared.js');
2 | include('search.js');
3 |
4 | function runWithString(query) {
5 |
6 | if (!query) {
7 | return [];
8 | }
9 |
10 | if (!loadApiToken()) {
11 |
12 | return loginErrorAsListResults();
13 | }
14 |
15 | var posts = getAllPosts();
16 |
17 | var matchingPosts = searchPosts(posts, query);
18 | var results = postsAsListResults(matchingPosts);
19 |
20 | if (query.toLowerCase() === 'refresh') {
21 | results.unshift({
22 | title: 'Refresh your out-of-date bookmarks',
23 | subtitle: 'Try this if your most recent bookmarks are missing from search results',
24 | action: 'clearCachedAllPosts'
25 | });
26 | }
27 |
28 | return results;
29 | }
30 |
31 | function searchPosts(simplePosts, query) {
32 | query = query.toLowerCase().trim();
33 | if (query.length < 2) return [];
34 |
35 | return search.searchObjectsWithIndexedText(simplePosts, query);
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Scripts/search.js:
--------------------------------------------------------------------------------
1 | var search = (function() {
2 | var WHITESPACE = /\s+/;
3 |
4 | /**
5 | * Assignes a score (higher is better) to an indexed text
6 | * string based on how well it matches the search words.
7 | * @param {array} searchWords words to search for--the text the user
8 | * searches for.
9 | * @param {string} indexedText
10 | * @return {number} score for how well the indexed text
11 | * matches the search words.
12 | */
13 | function scoreIndexedText(searchWords, indexedText) {
14 | var score = 0;
15 |
16 | for (var i = 0; i < searchWords.length; i++) {
17 | var searchWord = searchWords[i];
18 |
19 | // The first time a word is found, increase the score a lot.
20 | // Repeats of the same word score much lower. This ranks multiple
21 | // word matches very high, but gives some weight to repeated words
22 | // too.
23 | var scoreWeight = 10;
24 |
25 | var atIndex = 0;
26 | while (true) {
27 | atIndex = indexedText.indexOf(searchWord, atIndex);
28 | if (atIndex === -1) break;
29 |
30 | score += scoreWeight;
31 | scoreWeight = 1;
32 | atIndex += 1;
33 | }
34 | }
35 |
36 | return score;
37 | }
38 |
39 | /**
40 | * Given objects with a .indexedText property, filters out any that don't
41 | * match the search string and sorts the results with closest matching
42 | * first.
43 | * @param {array} objects objects with a .indexedText property.
44 | * @param {string} searchString space separated string of words to search
45 | * for.
46 | * @return {array} sorted, matching objects.
47 | */
48 | function searchObjectsWithIndexedText(objects, searchString) {
49 | var searchWords = searchString.toLowerCase().split(WHITESPACE);
50 | var matchingObjects = [];
51 |
52 | objects.forEach(function(object) {
53 | var score = scoreIndexedText(searchWords, object.indexedText);
54 | if (score > 0) {
55 | object.__score__ = score;
56 | matchingObjects.push(object);
57 | }
58 | });
59 |
60 | matchingObjects.sort(function(left, right) {
61 | return right.__score__ - left.__score__;
62 | });
63 |
64 | matchingObjects.forEach(function(m) {
65 | delete m.__score__;
66 | });
67 |
68 | return matchingObjects;
69 | }
70 |
71 | /**
72 | * Given a text string, creates a indexed version of that text that can be
73 | * used for a full text search later. Currenlty this just lowercases the
74 | * text, but could strip stop-words or repeates in the future.
75 | * @param {string} text text to index.
76 | * @return {string} indexed text to use with other functions in this
77 | * module.
78 | */
79 | function indexText(text) {
80 | return text.toLowerCase();
81 | }
82 |
83 | return {
84 | indexText: indexText,
85 | searchObjectsWithIndexedText: searchObjectsWithIndexedText
86 | };
87 | })();
88 |
89 | try {
90 | module.exports = search;
91 | }
92 | catch (e) {
93 | // not in a node.js
94 | }
95 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Search.lbaction/Contents/Scripts/shared.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This script is copied (grunt) to each action directory so they can share
3 | * common log in and HTTP request behaviour. It is included in each action with
4 | * the LaunchBar specific `include` function, but this seems to require the
5 | * shared script to be in the same directory as the calling script.
6 | */
7 |
8 | /**
9 | * Just globally store the API token after reading from the file.
10 | */
11 | var apiToken_;
12 |
13 | /**
14 | * Reads the saved API token from a support file.
15 | * If the file cannot be read, prompts the user for
16 | * the API token to save.
17 | *
18 | * @return {boolean} true if the token was loaded (user is logged in).
19 | */
20 | function loadApiToken() {
21 | var file = Action.supportPath;
22 | // LaunchBar 6.1 added a trailing slash to this path. Try to work with either.
23 | var tailRe = /Action Support\/?/;
24 | var m = tailRe.exec(file);
25 | var tailIndex = m.index;
26 | var tailLength = m[0].length;
27 | // The file is save in the support folder for the Pinboard Log In action.
28 | // Different actions can't share a support folder, so just hard-code where it is.
29 | file = file.slice(0, tailIndex + tailLength) + 'gillibrand.jay.pinboard.login/api-token.txt';
30 |
31 | try {
32 | LaunchBar.log('Will read API toke file at: ' + file);
33 | apiToken_ = File.readText(file);
34 | return true;
35 | }
36 | catch (e) {
37 | LaunchBar.log('Failed to read log in token. ' + e);
38 | return false;
39 | }
40 | }
41 |
42 | /**
43 | * Return a single action result to defers to the login in action.
44 | * Used from other actions that can't get the API token (not logged in yet).
45 | *
46 | * @return {array} single action to log in.
47 | */
48 | function loginErrorAsListResults() {
49 | return [{
50 | title: 'Log In to Pinboard',
51 | subtitle: 'Continue to log in with LaunchBar.',
52 | actionBundleIdentifier: 'gillibrand.jay.pinboard.login'
53 | }];
54 | }
55 |
56 | /**
57 | * HTTP GET a give Pinboard. Automatically requests JSON format
58 | * and include the API token (which must be loaded with loadApiToken first).
59 | *
60 | * Will automatically prompt for a new API token if hits a 401.
61 | *
62 | * @param {string} url URL to load.
63 | * @param {object} params optional hash of parameter names to values.
64 | * @return {object} the JSON result. null if not loaded.
65 | */
66 | function getUrl(url, params) {
67 | url = url + '?format=json&auth_token=' + apiToken_;
68 |
69 | if (params) {
70 | for (var name in params) {
71 | url += '&' + name + '=' + params[name];
72 | }
73 | }
74 |
75 | LaunchBar.debugLog('GET ' + url);
76 |
77 | var result = HTTP.getJSON(url);
78 |
79 | if (result.data) return result.data;
80 |
81 | if (result.response.status === 401) {
82 | LaunchBar.alert(
83 | 'You are no longer logged in to Pinboard through LaunchBar.',
84 | 'Your API token is wrong or has expired. You must enter it again.');
85 | LaunchBar.performAction('Pinboard: Log In');
86 | return null;
87 | }
88 | else {
89 | LaunchBar.alert(
90 | 'Could not contact Pinboard.',
91 | 'This may be a temporary error. Try again later.\n\nPinboard said: ' + result.response.status + ' ' + result.response.localizedStatus);
92 | return null;
93 | }
94 | }
95 |
96 | /**
97 | * Converts Piboard post (bookmark) JSON objects to list results
98 | * to present in LaunchBar.
99 | * @param {array} posts the Pinboard post objects as returned from HTTP JSON requests.
100 | * @return {array} LaunchBar results.
101 | */
102 | function postsAsListResults(posts) {
103 | return posts.map(function(post) {
104 | var result = {
105 | url: post.href,
106 | icon: 'BookmarkTemplate.icns'
107 | };
108 |
109 | result.title = post.description
110 | ? post.description
111 | : post.href;
112 |
113 | result.subtitle = post.extended
114 | ? post.extended
115 | : post.href;
116 |
117 | if (post.tags) {
118 | result.title = result.title + ' (' + post.tags + ')';
119 | }
120 |
121 | return result;
122 | });
123 | }
124 |
125 | function clearCachedAllPosts() {
126 | Action.preferences.lastCacheTime = null;
127 | LaunchBar.displayNotification({
128 | subtitle: 'Pinboard for LaunchBar',
129 | string: 'Cleared cached Pinboard bookmarks.'
130 | });
131 | }
132 |
133 | /**
134 | * Gets potentially all posts. Reads them from the cache file if available and
135 | * it's less than 5 minutes old (as recommended by Pinboard.in for be-nice rate
136 | * limiting).
137 | *
138 | * Unfortunately the cache is per-action since this shared script is actually
139 | * copied to each action, and there is no trivial way to share preferences and
140 | * support directories between actions.
141 | *
142 | * @param {object} params extra params to append to the "all" request.
143 | * If the results are already cached, this is ignored (ugh).
144 | * @return {array} all posts, possibly from the cache. May be filtered
145 | * if params were passed.
146 | */
147 | function getAllPosts(params) {
148 | var posts = getCachedAllPosts();
149 |
150 | if (!posts) {
151 | posts = cacheAllPosts(params);
152 | }
153 |
154 | return posts;
155 | }
156 |
157 |
158 | // duplicated from search.js since that's not "shared" will all actions.
159 | // TODO: clean up. Probably put all search.js into shared for simplicity.
160 | function indexText(text) {
161 | return text.toLowerCase();
162 | }
163 |
164 | var ALL_POSTS_FILE = Action.supportPath + '/all-posts.json';
165 |
166 | /**
167 | * Gets all posts and caches in the files system. Updates
168 | * a "last cache" time in the preferences of the current action.
169 | * @param {object} params extra params to pass to the "all" request.
170 | * @return {array} all posts.
171 | */
172 | function cacheAllPosts(params) {
173 | LaunchBar.log('Caching all bookmarks for local searching.');
174 | var allPosts = getUrl('https://api.pinboard.in/v1/posts/all', params);
175 |
176 | var simplePosts = allPosts.map(function(post) {
177 | var indexedText = indexText(post.description + ' ' + post.extended + ' ' + post.tags);
178 | post.indexedText = indexedText;
179 | return post;
180 | });
181 |
182 | File.writeJSON({
183 | simplePosts: simplePosts
184 | }, ALL_POSTS_FILE);
185 |
186 | Action.preferences.lastCacheTime = Date.now();
187 |
188 | return simplePosts;
189 | }
190 |
191 | /**
192 | * Gets all the cached posts or null if there are none
193 | * or the cache time expired.
194 | * @return {array} null or array of cached posts.
195 | */
196 | function getCachedAllPosts() {
197 | var lastCacheTime = Action.preferences.lastCacheTime;
198 |
199 | if (!lastCacheTime) {
200 | LaunchBar.log('List of all my posts was never cached.');
201 | return null;
202 | }
203 |
204 | var diffMillis = Date.now() - lastCacheTime;
205 |
206 | if (diffMillis / 1000.0 / 60.0 >= 5) {
207 | LaunchBar.log('List of all my posts is older than 5 mins. May re-cache all posts...');
208 |
209 | var lastUpdateIso = Action.preferences.lastUpdateIso;
210 |
211 | var updateIso = getUrl('https://api.pinboard.in/v1/posts/update').update_time;
212 | LaunchBar.debugLog('Last update time: ' + updateIso);
213 | Action.preferences.lastUpdateIso = updateIso;
214 |
215 | if (!lastUpdateIso) {
216 | LaunchBar.log('No last update time was found. Will re-cache.');
217 | return null;
218 | }
219 |
220 | if (updateIso !== lastUpdateIso) {
221 | LaunchBar.log('Bookmarks were updated. Will re-cache.');
222 | return null;
223 | }
224 | }
225 |
226 | try {
227 | var simplePosts = File.readJSON(ALL_POSTS_FILE).simplePosts;
228 | LaunchBar.log('Using cached list of all my posts.');
229 | return simplePosts;
230 | }
231 | catch (e) {
232 | LaunchBar.log('No cached all-posts files.');
233 | return null;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | gillibrand.jay.pinboard-tags
7 | CFBundleIconFile
8 | PinboardTemplate.icns
9 | CFBundleName
10 | Pinboard: Tags
11 | LBAbbreviation
12 | pbt
13 | LBDebugLogEnabled
14 |
15 | CFBundleVersion
16 | 1.4.3
17 | LBScripts
18 |
19 | LBDefaultScript
20 |
21 | LBAcceptedArgumentTypes
22 |
23 | string
24 |
25 | LBScriptName
26 | pinboard-tags.js
27 | LBReturnsResult
28 |
29 | LBRequiresArgument
30 |
31 | LBLiveFeedbackEnabled
32 |
33 |
34 |
35 | LBDescription
36 |
37 | LBAuthor
38 | Jay Gillibrand
39 | LBWebsite
40 | https://github.com/gillibrand/launchbar-pinboard
41 | LBSummary
42 | Lists you Pinboard tags.
43 | LBResult
44 | Your Pinboard tags sorted from most used to least. Selecting a tag will list all the bookmarks for that tag.
45 | LBRequirements
46 | "Pinboard Log In" action.
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Resources/BookmarkTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Resources/BookmarkTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Resources/PinboardTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Resources/PinboardTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Resources/TagTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Resources/TagTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Scripts/pinboard-tags.js:
--------------------------------------------------------------------------------
1 | include('shared.js');
2 |
3 | /**
4 | * Lists all tags, sorted by count.
5 | */
6 | function run() {
7 | if (!loadApiToken()) return loginErrorAsListResults();
8 |
9 | var tags = getUrl('https://api.pinboard.in/v1/tags/get');
10 | if (!tags) return;
11 |
12 | var results = [];
13 |
14 | for (var tag in tags) {
15 | var count = tags[tag];
16 |
17 | var result = {
18 | title: tag,
19 | subtitle: count,
20 | action: 'listTag',
21 | actionArgument: tag,
22 | actionReturnsItems: true,
23 | count: count,
24 | icon: 'TagTemplate.icns'
25 | };
26 |
27 | results.push(result);
28 | }
29 |
30 | results.sort(function(left, right) {
31 | return right.count - left.count;
32 | });
33 |
34 | return results;
35 | }
36 |
37 | /**
38 | * List the bookmarks for a given tag.
39 | * @param {string} tag name of the tag.
40 | */
41 | function listTag(tag) {
42 | if (!loadApiToken()) return;
43 |
44 | var posts = getUrl('https://api.pinboard.in/v1/posts/all', {
45 | tag: tag
46 | });
47 |
48 | if (!posts) return;
49 |
50 | return postsAsListResults(posts);
51 | }
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Tags.lbaction/Contents/Scripts/shared.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This script is copied (grunt) to each action directory so they can share
3 | * common log in and HTTP request behaviour. It is included in each action with
4 | * the LaunchBar specific `include` function, but this seems to require the
5 | * shared script to be in the same directory as the calling script.
6 | */
7 |
8 | /**
9 | * Just globally store the API token after reading from the file.
10 | */
11 | var apiToken_;
12 |
13 | /**
14 | * Reads the saved API token from a support file.
15 | * If the file cannot be read, prompts the user for
16 | * the API token to save.
17 | *
18 | * @return {boolean} true if the token was loaded (user is logged in).
19 | */
20 | function loadApiToken() {
21 | var file = Action.supportPath;
22 | // LaunchBar 6.1 added a trailing slash to this path. Try to work with either.
23 | var tailRe = /Action Support\/?/;
24 | var m = tailRe.exec(file);
25 | var tailIndex = m.index;
26 | var tailLength = m[0].length;
27 | // The file is save in the support folder for the Pinboard Log In action.
28 | // Different actions can't share a support folder, so just hard-code where it is.
29 | file = file.slice(0, tailIndex + tailLength) + 'gillibrand.jay.pinboard.login/api-token.txt';
30 |
31 | try {
32 | LaunchBar.log('Will read API toke file at: ' + file);
33 | apiToken_ = File.readText(file);
34 | return true;
35 | }
36 | catch (e) {
37 | LaunchBar.log('Failed to read log in token. ' + e);
38 | return false;
39 | }
40 | }
41 |
42 | /**
43 | * Return a single action result to defers to the login in action.
44 | * Used from other actions that can't get the API token (not logged in yet).
45 | *
46 | * @return {array} single action to log in.
47 | */
48 | function loginErrorAsListResults() {
49 | return [{
50 | title: 'Log In to Pinboard',
51 | subtitle: 'Continue to log in with LaunchBar.',
52 | actionBundleIdentifier: 'gillibrand.jay.pinboard.login'
53 | }];
54 | }
55 |
56 | /**
57 | * HTTP GET a give Pinboard. Automatically requests JSON format
58 | * and include the API token (which must be loaded with loadApiToken first).
59 | *
60 | * Will automatically prompt for a new API token if hits a 401.
61 | *
62 | * @param {string} url URL to load.
63 | * @param {object} params optional hash of parameter names to values.
64 | * @return {object} the JSON result. null if not loaded.
65 | */
66 | function getUrl(url, params) {
67 | url = url + '?format=json&auth_token=' + apiToken_;
68 |
69 | if (params) {
70 | for (var name in params) {
71 | url += '&' + name + '=' + params[name];
72 | }
73 | }
74 |
75 | LaunchBar.debugLog('GET ' + url);
76 |
77 | var result = HTTP.getJSON(url);
78 |
79 | if (result.data) return result.data;
80 |
81 | if (result.response.status === 401) {
82 | LaunchBar.alert(
83 | 'You are no longer logged in to Pinboard through LaunchBar.',
84 | 'Your API token is wrong or has expired. You must enter it again.');
85 | LaunchBar.performAction('Pinboard: Log In');
86 | return null;
87 | }
88 | else {
89 | LaunchBar.alert(
90 | 'Could not contact Pinboard.',
91 | 'This may be a temporary error. Try again later.\n\nPinboard said: ' + result.response.status + ' ' + result.response.localizedStatus);
92 | return null;
93 | }
94 | }
95 |
96 | /**
97 | * Converts Piboard post (bookmark) JSON objects to list results
98 | * to present in LaunchBar.
99 | * @param {array} posts the Pinboard post objects as returned from HTTP JSON requests.
100 | * @return {array} LaunchBar results.
101 | */
102 | function postsAsListResults(posts) {
103 | return posts.map(function(post) {
104 | var result = {
105 | url: post.href,
106 | icon: 'BookmarkTemplate.icns'
107 | };
108 |
109 | result.title = post.description
110 | ? post.description
111 | : post.href;
112 |
113 | result.subtitle = post.extended
114 | ? post.extended
115 | : post.href;
116 |
117 | if (post.tags) {
118 | result.title = result.title + ' (' + post.tags + ')';
119 | }
120 |
121 | return result;
122 | });
123 | }
124 |
125 | function clearCachedAllPosts() {
126 | Action.preferences.lastCacheTime = null;
127 | LaunchBar.displayNotification({
128 | subtitle: 'Pinboard for LaunchBar',
129 | string: 'Cleared cached Pinboard bookmarks.'
130 | });
131 | }
132 |
133 | /**
134 | * Gets potentially all posts. Reads them from the cache file if available and
135 | * it's less than 5 minutes old (as recommended by Pinboard.in for be-nice rate
136 | * limiting).
137 | *
138 | * Unfortunately the cache is per-action since this shared script is actually
139 | * copied to each action, and there is no trivial way to share preferences and
140 | * support directories between actions.
141 | *
142 | * @param {object} params extra params to append to the "all" request.
143 | * If the results are already cached, this is ignored (ugh).
144 | * @return {array} all posts, possibly from the cache. May be filtered
145 | * if params were passed.
146 | */
147 | function getAllPosts(params) {
148 | var posts = getCachedAllPosts();
149 |
150 | if (!posts) {
151 | posts = cacheAllPosts(params);
152 | }
153 |
154 | return posts;
155 | }
156 |
157 |
158 | // duplicated from search.js since that's not "shared" will all actions.
159 | // TODO: clean up. Probably put all search.js into shared for simplicity.
160 | function indexText(text) {
161 | return text.toLowerCase();
162 | }
163 |
164 | var ALL_POSTS_FILE = Action.supportPath + '/all-posts.json';
165 |
166 | /**
167 | * Gets all posts and caches in the files system. Updates
168 | * a "last cache" time in the preferences of the current action.
169 | * @param {object} params extra params to pass to the "all" request.
170 | * @return {array} all posts.
171 | */
172 | function cacheAllPosts(params) {
173 | LaunchBar.log('Caching all bookmarks for local searching.');
174 | var allPosts = getUrl('https://api.pinboard.in/v1/posts/all', params);
175 |
176 | var simplePosts = allPosts.map(function(post) {
177 | var indexedText = indexText(post.description + ' ' + post.extended + ' ' + post.tags);
178 | post.indexedText = indexedText;
179 | return post;
180 | });
181 |
182 | File.writeJSON({
183 | simplePosts: simplePosts
184 | }, ALL_POSTS_FILE);
185 |
186 | Action.preferences.lastCacheTime = Date.now();
187 |
188 | return simplePosts;
189 | }
190 |
191 | /**
192 | * Gets all the cached posts or null if there are none
193 | * or the cache time expired.
194 | * @return {array} null or array of cached posts.
195 | */
196 | function getCachedAllPosts() {
197 | var lastCacheTime = Action.preferences.lastCacheTime;
198 |
199 | if (!lastCacheTime) {
200 | LaunchBar.log('List of all my posts was never cached.');
201 | return null;
202 | }
203 |
204 | var diffMillis = Date.now() - lastCacheTime;
205 |
206 | if (diffMillis / 1000.0 / 60.0 >= 5) {
207 | LaunchBar.log('List of all my posts is older than 5 mins. May re-cache all posts...');
208 |
209 | var lastUpdateIso = Action.preferences.lastUpdateIso;
210 |
211 | var updateIso = getUrl('https://api.pinboard.in/v1/posts/update').update_time;
212 | LaunchBar.debugLog('Last update time: ' + updateIso);
213 | Action.preferences.lastUpdateIso = updateIso;
214 |
215 | if (!lastUpdateIso) {
216 | LaunchBar.log('No last update time was found. Will re-cache.');
217 | return null;
218 | }
219 |
220 | if (updateIso !== lastUpdateIso) {
221 | LaunchBar.log('Bookmarks were updated. Will re-cache.');
222 | return null;
223 | }
224 | }
225 |
226 | try {
227 | var simplePosts = File.readJSON(ALL_POSTS_FILE).simplePosts;
228 | LaunchBar.log('Using cached list of all my posts.');
229 | return simplePosts;
230 | }
231 | catch (e) {
232 | LaunchBar.log('No cached all-posts files.');
233 | return null;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIdentifier
6 | gillibrand.jay.pinboard-unread
7 | CFBundleIconFile
8 | PinboardTemplate.icns
9 | CFBundleName
10 | Pinboard: Unread Bookmarks
11 | LBAbbreviation
12 | pbu
13 | LBDebugLogEnabled
14 |
15 | CFBundleVersion
16 | 1.0.3
17 | LBScripts
18 |
19 | LBDefaultScript
20 |
21 | LBAcceptedArgumentTypes
22 |
23 | string
24 |
25 | LBScriptName
26 | pinboard-unread.js
27 | LBReturnsResult
28 |
29 | LBRequiresArgument
30 |
31 | LBLiveFeedbackEnabled
32 |
33 |
34 |
35 | LBDescription
36 |
37 | LBAuthor
38 | Jay Gillibrand
39 | LBWebsite
40 | https://github.com/gillibrand/launchbar-pinboard
41 | LBSummary
42 | Lists all your unread Pinboard bookmarks.
43 | LBResult
44 | All unread bookmarks.
45 | LBRequirements
46 | "Pinboard Log In" action.
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Resources/BookmarkTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Resources/BookmarkTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Resources/PinboardTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Resources/PinboardTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Resources/TagTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Resources/TagTemplate.icns
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Scripts/pinboard-unread.js:
--------------------------------------------------------------------------------
1 | include('shared.js');
2 |
3 | function run() {
4 | if (!loadApiToken()) return loginErrorAsListResults();
5 |
6 | var posts = getAllPosts({
7 | toread: 'yes'
8 | });
9 |
10 | var unreadPosts = posts.filter(function(post) {
11 | return post.toread == 'yes';
12 | });
13 |
14 | return postsAsListResults(unreadPosts);
15 | }
--------------------------------------------------------------------------------
/Pinboard.lbext/Contents/Resources/Actions/Pinboard Unread.lbaction/Contents/Scripts/shared.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This script is copied (grunt) to each action directory so they can share
3 | * common log in and HTTP request behaviour. It is included in each action with
4 | * the LaunchBar specific `include` function, but this seems to require the
5 | * shared script to be in the same directory as the calling script.
6 | */
7 |
8 | /**
9 | * Just globally store the API token after reading from the file.
10 | */
11 | var apiToken_;
12 |
13 | /**
14 | * Reads the saved API token from a support file.
15 | * If the file cannot be read, prompts the user for
16 | * the API token to save.
17 | *
18 | * @return {boolean} true if the token was loaded (user is logged in).
19 | */
20 | function loadApiToken() {
21 | var file = Action.supportPath;
22 | // LaunchBar 6.1 added a trailing slash to this path. Try to work with either.
23 | var tailRe = /Action Support\/?/;
24 | var m = tailRe.exec(file);
25 | var tailIndex = m.index;
26 | var tailLength = m[0].length;
27 | // The file is save in the support folder for the Pinboard Log In action.
28 | // Different actions can't share a support folder, so just hard-code where it is.
29 | file = file.slice(0, tailIndex + tailLength) + 'gillibrand.jay.pinboard.login/api-token.txt';
30 |
31 | try {
32 | LaunchBar.log('Will read API toke file at: ' + file);
33 | apiToken_ = File.readText(file);
34 | return true;
35 | }
36 | catch (e) {
37 | LaunchBar.log('Failed to read log in token. ' + e);
38 | return false;
39 | }
40 | }
41 |
42 | /**
43 | * Return a single action result to defers to the login in action.
44 | * Used from other actions that can't get the API token (not logged in yet).
45 | *
46 | * @return {array} single action to log in.
47 | */
48 | function loginErrorAsListResults() {
49 | return [{
50 | title: 'Log In to Pinboard',
51 | subtitle: 'Continue to log in with LaunchBar.',
52 | actionBundleIdentifier: 'gillibrand.jay.pinboard.login'
53 | }];
54 | }
55 |
56 | /**
57 | * HTTP GET a give Pinboard. Automatically requests JSON format
58 | * and include the API token (which must be loaded with loadApiToken first).
59 | *
60 | * Will automatically prompt for a new API token if hits a 401.
61 | *
62 | * @param {string} url URL to load.
63 | * @param {object} params optional hash of parameter names to values.
64 | * @return {object} the JSON result. null if not loaded.
65 | */
66 | function getUrl(url, params) {
67 | url = url + '?format=json&auth_token=' + apiToken_;
68 |
69 | if (params) {
70 | for (var name in params) {
71 | url += '&' + name + '=' + params[name];
72 | }
73 | }
74 |
75 | LaunchBar.debugLog('GET ' + url);
76 |
77 | var result = HTTP.getJSON(url);
78 |
79 | if (result.data) return result.data;
80 |
81 | if (result.response.status === 401) {
82 | LaunchBar.alert(
83 | 'You are no longer logged in to Pinboard through LaunchBar.',
84 | 'Your API token is wrong or has expired. You must enter it again.');
85 | LaunchBar.performAction('Pinboard: Log In');
86 | return null;
87 | }
88 | else {
89 | LaunchBar.alert(
90 | 'Could not contact Pinboard.',
91 | 'This may be a temporary error. Try again later.\n\nPinboard said: ' + result.response.status + ' ' + result.response.localizedStatus);
92 | return null;
93 | }
94 | }
95 |
96 | /**
97 | * Converts Piboard post (bookmark) JSON objects to list results
98 | * to present in LaunchBar.
99 | * @param {array} posts the Pinboard post objects as returned from HTTP JSON requests.
100 | * @return {array} LaunchBar results.
101 | */
102 | function postsAsListResults(posts) {
103 | return posts.map(function(post) {
104 | var result = {
105 | url: post.href,
106 | icon: 'BookmarkTemplate.icns'
107 | };
108 |
109 | result.title = post.description
110 | ? post.description
111 | : post.href;
112 |
113 | result.subtitle = post.extended
114 | ? post.extended
115 | : post.href;
116 |
117 | if (post.tags) {
118 | result.title = result.title + ' (' + post.tags + ')';
119 | }
120 |
121 | return result;
122 | });
123 | }
124 |
125 | function clearCachedAllPosts() {
126 | Action.preferences.lastCacheTime = null;
127 | LaunchBar.displayNotification({
128 | subtitle: 'Pinboard for LaunchBar',
129 | string: 'Cleared cached Pinboard bookmarks.'
130 | });
131 | }
132 |
133 | /**
134 | * Gets potentially all posts. Reads them from the cache file if available and
135 | * it's less than 5 minutes old (as recommended by Pinboard.in for be-nice rate
136 | * limiting).
137 | *
138 | * Unfortunately the cache is per-action since this shared script is actually
139 | * copied to each action, and there is no trivial way to share preferences and
140 | * support directories between actions.
141 | *
142 | * @param {object} params extra params to append to the "all" request.
143 | * If the results are already cached, this is ignored (ugh).
144 | * @return {array} all posts, possibly from the cache. May be filtered
145 | * if params were passed.
146 | */
147 | function getAllPosts(params) {
148 | var posts = getCachedAllPosts();
149 |
150 | if (!posts) {
151 | posts = cacheAllPosts(params);
152 | }
153 |
154 | return posts;
155 | }
156 |
157 |
158 | // duplicated from search.js since that's not "shared" will all actions.
159 | // TODO: clean up. Probably put all search.js into shared for simplicity.
160 | function indexText(text) {
161 | return text.toLowerCase();
162 | }
163 |
164 | var ALL_POSTS_FILE = Action.supportPath + '/all-posts.json';
165 |
166 | /**
167 | * Gets all posts and caches in the files system. Updates
168 | * a "last cache" time in the preferences of the current action.
169 | * @param {object} params extra params to pass to the "all" request.
170 | * @return {array} all posts.
171 | */
172 | function cacheAllPosts(params) {
173 | LaunchBar.log('Caching all bookmarks for local searching.');
174 | var allPosts = getUrl('https://api.pinboard.in/v1/posts/all', params);
175 |
176 | var simplePosts = allPosts.map(function(post) {
177 | var indexedText = indexText(post.description + ' ' + post.extended + ' ' + post.tags);
178 | post.indexedText = indexedText;
179 | return post;
180 | });
181 |
182 | File.writeJSON({
183 | simplePosts: simplePosts
184 | }, ALL_POSTS_FILE);
185 |
186 | Action.preferences.lastCacheTime = Date.now();
187 |
188 | return simplePosts;
189 | }
190 |
191 | /**
192 | * Gets all the cached posts or null if there are none
193 | * or the cache time expired.
194 | * @return {array} null or array of cached posts.
195 | */
196 | function getCachedAllPosts() {
197 | var lastCacheTime = Action.preferences.lastCacheTime;
198 |
199 | if (!lastCacheTime) {
200 | LaunchBar.log('List of all my posts was never cached.');
201 | return null;
202 | }
203 |
204 | var diffMillis = Date.now() - lastCacheTime;
205 |
206 | if (diffMillis / 1000.0 / 60.0 >= 5) {
207 | LaunchBar.log('List of all my posts is older than 5 mins. May re-cache all posts...');
208 |
209 | var lastUpdateIso = Action.preferences.lastUpdateIso;
210 |
211 | var updateIso = getUrl('https://api.pinboard.in/v1/posts/update').update_time;
212 | LaunchBar.debugLog('Last update time: ' + updateIso);
213 | Action.preferences.lastUpdateIso = updateIso;
214 |
215 | if (!lastUpdateIso) {
216 | LaunchBar.log('No last update time was found. Will re-cache.');
217 | return null;
218 | }
219 |
220 | if (updateIso !== lastUpdateIso) {
221 | LaunchBar.log('Bookmarks were updated. Will re-cache.');
222 | return null;
223 | }
224 | }
225 |
226 | try {
227 | var simplePosts = File.readJSON(ALL_POSTS_FILE).simplePosts;
228 | LaunchBar.log('Using cached list of all my posts.');
229 | return simplePosts;
230 | }
231 | catch (e) {
232 | LaunchBar.log('No cached all-posts files.');
233 | return null;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pinboard for LaunchBar
2 |
3 | A suite of custom actions for [LaunchBar 6](http://www.obdev.at/products/launchbar) that provide access to [Pinboard](https://pinboard.in) bookmarks.
4 |
5 | 
6 |
7 | # Installation
8 |
9 | [Download the ZIP](https://github.com/gillibrand/launchbar-pinboard/archive/master.zip) and unzip. Double-click `Pinboard.lbext` to install all the actions.
10 |
11 | # Actions
12 |
13 | All actions require that you log in to Pinboard using your API token first. Actions will automatically prompt you for this if needed, but the automatic prompting has broken several times through the LaunchBar betas, so you may want to initially run `Pinboard: Log In` yourself.
14 |
15 | ## Pinboard: Recent Bookmarks
16 |
17 | Lists your 25 most recent bookmarks.
18 |
19 | ## Pinboard: Tags
20 |
21 | Lists your Pinboard tags sorted from most-used to least. Selecting a tag lists all the bookmarks for that tag.
22 |
23 | ## Pinboard: Search
24 |
25 | Searches the titles, descriptions, and tags of all your bookmarks.
26 |
27 | Search results can be up to five minutes out-of-date. This is done to improve performance if you have a large number of bookmarks. If you notice your results are out-of-date, you can try searching for `refresh` with this action and choosing the `Refresh your out-of-date bookmarks` action.
28 |
29 | ## Pinboard: Unread Bookmarks
30 |
31 | List all you unread bookmarks.
32 |
33 | ## Pinboard: Log In
34 |
35 | This action is used by all the other actions in order to access your Pinboard account. Prompts for and saves your Pinboard API token. Also provides a quick link to your API token if you are already logged in to the Pinboard web site.
36 |
37 | Your API token (not password) is saved as plain-text in the Application Support directory for this action. You can delete it with the `Log Out` sub-action.
38 |
39 | # Building
40 |
41 | If you want edit or customize these actions for yourself, be aware that `Grunt` is used to copy a shared script to each of the individual action scripts. See `shared.js` for more details.
42 |
43 | # Version History
44 |
45 | ### 4/27/2018
46 |
47 | - Fix for unexpected title characters causing posts not to load courtesy of [Takeshi Suzuki](https://github.com/tockrock).
48 |
49 | ### 8/30/2014
50 |
51 | - Compatibility update for LaunchBar 6.1. `Action.supportPath` now includes a trailing slash.
52 |
53 | ### 7/7/2014
54 |
55 | - Improved search performance when bookmarks haven't changed since the last search.
56 |
57 | ### 6/24/2014
58 |
59 | - Added retina icons courtesy of [Pete Schaffner](https://github.com/peteschaffner).
60 |
61 | ### 6/18/2014
62 |
63 | - Added `Pinboard Unread` action.
64 |
65 | ### 6/11/2014
66 |
67 | - Code clean up. Change how shared scripts are included to use `include` API.
68 |
69 | ### 6/4/2014
70 |
71 | - Auto-defers to Log In action if not logged in (updated for LaunchBar 6.0 beta 7).
72 |
73 | ### 5/24/2014
74 |
75 | - Added `Log Out` sub-action.
76 |
77 | #### 5/2/2014
78 |
79 | - Packaged all actions into a a single `.lbext` file for easy installation.
80 |
81 | #### 4/26/2014
82 |
83 | - Added `Pinboard Search` action.
84 |
85 | #### 4/24/2014
86 |
87 | - Initial release.
--------------------------------------------------------------------------------
/launchbar-pinboard.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":
3 | [
4 | {
5 | "file_exclude_patterns":
6 | [
7 | "*.sublime-workspace"
8 | ],
9 | "folder_exclude_patterns":
10 | [
11 | "node_modules",
12 | "temp"
13 | ],
14 | "follow_symlinks": true,
15 | "path": "."
16 | }
17 | ],
18 | "settings":
19 | {
20 | "non_nls.warn_on_save": false
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "grunt": "^0.4.4",
4 | "grunt-contrib-clean": "~0.5.0",
5 | "grunt-contrib-concat": "~0.4.0",
6 | "grunt-contrib-copy": "~0.5.0",
7 | "grunt-contrib-jshint": "^1.1.0",
8 | "grunt-contrib-watch": "~0.6.1",
9 | "grunt-mocha-test": "^0.10.2",
10 | "load-grunt-tasks": "~0.4.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/shared/Contents/Resources/BookmarkTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/BookmarkTemplate.icns
--------------------------------------------------------------------------------
/shared/Contents/Resources/PinboardTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/PinboardTemplate.icns
--------------------------------------------------------------------------------
/shared/Contents/Resources/TagTemplate.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/TagTemplate.icns
--------------------------------------------------------------------------------
/shared/Contents/Resources/bookmark.acorn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/bookmark.acorn
--------------------------------------------------------------------------------
/shared/Contents/Resources/icons.iconsproj:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/icons.iconsproj
--------------------------------------------------------------------------------
/shared/Contents/Resources/icons.sketch/Data:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/icons.sketch/Data
--------------------------------------------------------------------------------
/shared/Contents/Resources/icons.sketch/metadata:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | app
6 | com.bohemiancoding.sketch3
7 | build
8 | 7891
9 | commit
10 | debc570766a4cc5a2e31258967910f7e5776f485
11 | fonts
12 |
13 | length
14 | 36224
15 | version
16 | 37
17 |
18 |
19 |
--------------------------------------------------------------------------------
/shared/Contents/Resources/icons.sketch/version:
--------------------------------------------------------------------------------
1 | 37
--------------------------------------------------------------------------------
/shared/Contents/Resources/pinboard.acorn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/pinboard.acorn
--------------------------------------------------------------------------------
/shared/Contents/Resources/tag.acorn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gillibrand/launchbar-pinboard/6d6218f85f8532e8a805108c6902f82cb690166f/shared/Contents/Resources/tag.acorn
--------------------------------------------------------------------------------
/shared/Contents/Scripts/shared.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This script is copied (grunt) to each action directory so they can share
3 | * common log in and HTTP request behaviour. It is included in each action with
4 | * the LaunchBar specific `include` function, but this seems to require the
5 | * shared script to be in the same directory as the calling script.
6 | */
7 |
8 | /**
9 | * Just globally store the API token after reading from the file.
10 | */
11 | var apiToken_;
12 |
13 | /**
14 | * Reads the saved API token from a support file.
15 | * If the file cannot be read, prompts the user for
16 | * the API token to save.
17 | *
18 | * @return {boolean} true if the token was loaded (user is logged in).
19 | */
20 | function loadApiToken() {
21 | var file = Action.supportPath;
22 | // LaunchBar 6.1 added a trailing slash to this path. Try to work with either.
23 | var tailRe = /Action Support\/?/;
24 | var m = tailRe.exec(file);
25 | var tailIndex = m.index;
26 | var tailLength = m[0].length;
27 | // The file is save in the support folder for the Pinboard Log In action.
28 | // Different actions can't share a support folder, so just hard-code where it is.
29 | file = file.slice(0, tailIndex + tailLength) + 'gillibrand.jay.pinboard.login/api-token.txt';
30 |
31 | try {
32 | LaunchBar.log('Will read API toke file at: ' + file);
33 | apiToken_ = File.readText(file);
34 | return true;
35 | }
36 | catch (e) {
37 | LaunchBar.log('Failed to read log in token. ' + e);
38 | return false;
39 | }
40 | }
41 |
42 | /**
43 | * Return a single action result to defers to the login in action.
44 | * Used from other actions that can't get the API token (not logged in yet).
45 | *
46 | * @return {array} single action to log in.
47 | */
48 | function loginErrorAsListResults() {
49 | return [{
50 | title: 'Log In to Pinboard',
51 | subtitle: 'Continue to log in with LaunchBar.',
52 | actionBundleIdentifier: 'gillibrand.jay.pinboard.login'
53 | }];
54 | }
55 |
56 | /**
57 | * HTTP GET a give Pinboard. Automatically requests JSON format
58 | * and include the API token (which must be loaded with loadApiToken first).
59 | *
60 | * Will automatically prompt for a new API token if hits a 401.
61 | *
62 | * @param {string} url URL to load.
63 | * @param {object} params optional hash of parameter names to values.
64 | * @return {object} the JSON result. null if not loaded.
65 | */
66 | function getUrl(url, params) {
67 | url = url + '?format=json&auth_token=' + apiToken_;
68 |
69 | if (params) {
70 | for (var name in params) {
71 | url += '&' + name + '=' + params[name];
72 | }
73 | }
74 |
75 | LaunchBar.debugLog('GET ' + url);
76 |
77 | var result = HTTP.getJSON(url);
78 |
79 | if (result.data) return result.data;
80 |
81 | if (result.response.status === 401) {
82 | LaunchBar.alert(
83 | 'You are no longer logged in to Pinboard through LaunchBar.',
84 | 'Your API token is wrong or has expired. You must enter it again.');
85 | LaunchBar.performAction('Pinboard: Log In');
86 | return null;
87 | }
88 | else {
89 | LaunchBar.alert(
90 | 'Could not contact Pinboard.',
91 | 'This may be a temporary error. Try again later.\n\nPinboard said: ' + result.response.status + ' ' + result.response.localizedStatus);
92 | return null;
93 | }
94 | }
95 |
96 | /**
97 | * Converts Piboard post (bookmark) JSON objects to list results
98 | * to present in LaunchBar.
99 | * @param {array} posts the Pinboard post objects as returned from HTTP JSON requests.
100 | * @return {array} LaunchBar results.
101 | */
102 | function postsAsListResults(posts) {
103 | return posts.map(function(post) {
104 | var result = {
105 | url: post.href,
106 | icon: 'BookmarkTemplate.icns'
107 | };
108 |
109 | result.title = post.description
110 | ? post.description
111 | : post.href;
112 |
113 | result.subtitle = post.extended
114 | ? post.extended
115 | : post.href;
116 |
117 | if (post.tags) {
118 | result.title = result.title + ' (' + post.tags + ')';
119 | }
120 |
121 | return result;
122 | });
123 | }
124 |
125 | function clearCachedAllPosts() {
126 | Action.preferences.lastCacheTime = null;
127 | LaunchBar.displayNotification({
128 | subtitle: 'Pinboard for LaunchBar',
129 | string: 'Cleared cached Pinboard bookmarks.'
130 | });
131 | }
132 |
133 | /**
134 | * Gets potentially all posts. Reads them from the cache file if available and
135 | * it's less than 5 minutes old (as recommended by Pinboard.in for be-nice rate
136 | * limiting).
137 | *
138 | * Unfortunately the cache is per-action since this shared script is actually
139 | * copied to each action, and there is no trivial way to share preferences and
140 | * support directories between actions.
141 | *
142 | * @param {object} params extra params to append to the "all" request.
143 | * If the results are already cached, this is ignored (ugh).
144 | * @return {array} all posts, possibly from the cache. May be filtered
145 | * if params were passed.
146 | */
147 | function getAllPosts(params) {
148 | var posts = getCachedAllPosts();
149 |
150 | if (!posts) {
151 | posts = cacheAllPosts(params);
152 | }
153 |
154 | return posts;
155 | }
156 |
157 |
158 | // duplicated from search.js since that's not "shared" will all actions.
159 | // TODO: clean up. Probably put all search.js into shared for simplicity.
160 | function indexText(text) {
161 | return text.toLowerCase();
162 | }
163 |
164 | var ALL_POSTS_FILE = Action.supportPath + '/all-posts.json';
165 |
166 | /**
167 | * Gets all posts and caches in the files system. Updates
168 | * a "last cache" time in the preferences of the current action.
169 | * @param {object} params extra params to pass to the "all" request.
170 | * @return {array} all posts.
171 | */
172 | function cacheAllPosts(params) {
173 | LaunchBar.log('Caching all bookmarks for local searching.');
174 | var allPosts = getUrl('https://api.pinboard.in/v1/posts/all', params);
175 |
176 | var simplePosts = allPosts.map(function(post) {
177 | var indexedText = indexText(post.description + ' ' + post.extended + ' ' + post.tags);
178 | post.indexedText = indexedText;
179 | return post;
180 | });
181 |
182 | File.writeJSON({
183 | simplePosts: simplePosts
184 | }, ALL_POSTS_FILE);
185 |
186 | Action.preferences.lastCacheTime = Date.now();
187 |
188 | return simplePosts;
189 | }
190 |
191 | /**
192 | * Gets all the cached posts or null if there are none
193 | * or the cache time expired.
194 | * @return {array} null or array of cached posts.
195 | */
196 | function getCachedAllPosts() {
197 | var lastCacheTime = Action.preferences.lastCacheTime;
198 |
199 | if (!lastCacheTime) {
200 | LaunchBar.log('List of all my posts was never cached.');
201 | return null;
202 | }
203 |
204 | var diffMillis = Date.now() - lastCacheTime;
205 |
206 | if (diffMillis / 1000.0 / 60.0 >= 5) {
207 | LaunchBar.log('List of all my posts is older than 5 mins. May re-cache all posts...');
208 |
209 | var lastUpdateIso = Action.preferences.lastUpdateIso;
210 |
211 | var updateIso = getUrl('https://api.pinboard.in/v1/posts/update').update_time;
212 | LaunchBar.debugLog('Last update time: ' + updateIso);
213 | Action.preferences.lastUpdateIso = updateIso;
214 |
215 | if (!lastUpdateIso) {
216 | LaunchBar.log('No last update time was found. Will re-cache.');
217 | return null;
218 | }
219 |
220 | if (updateIso !== lastUpdateIso) {
221 | LaunchBar.log('Bookmarks were updated. Will re-cache.');
222 | return null;
223 | }
224 | }
225 |
226 | try {
227 | var simplePosts = File.readJSON(ALL_POSTS_FILE).simplePosts;
228 | LaunchBar.log('Using cached list of all my posts.');
229 | return simplePosts;
230 | }
231 | catch (e) {
232 | LaunchBar.log('No cached all-posts files.');
233 | return null;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/test/test-searching.js:
--------------------------------------------------------------------------------
1 | var ACTIONS_DIR = 'Pinboard.lbext/Contents/Resources/Actions/';
2 |
3 | var assert = require('assert');
4 | var search = require('../' + ACTIONS_DIR + 'Pinboard Search.lbaction/Contents/Scripts/search');
5 |
6 | describe('search', function() {
7 |
8 | describe('searchPosts', function() {
9 |
10 |
11 | it('finds one match', function() {
12 | var all = [
13 | {
14 | id: 0,
15 | indexedText: search.indexText('Red Apple')
16 | },
17 | {
18 | id: 1,
19 | indexedText: search.indexText('Red Cherry')
20 | }
21 | ];
22 |
23 | var matches = search.searchObjectsWithIndexedText(all, 'Apple');
24 | assert.equal(1, matches.length);
25 | assert.equal(0, matches[0].id);
26 | });
27 |
28 |
29 | it('finds no matches', function() {
30 | var all = [
31 | {
32 | id: 0,
33 | indexedText: search.indexText('Red Apple')
34 | },
35 | {
36 | id: 1,
37 | indexedText: search.indexText('Red Cherry')
38 | }
39 | ];
40 |
41 | var matches = search.searchObjectsWithIndexedText(all, 'green');
42 | assert.equal(0, matches.length);
43 | });
44 |
45 |
46 | it('scores more hits in target', function() {
47 | var all = [
48 | {
49 | indexedText: search.indexText('Red Cherry'),
50 | id: 0
51 | },
52 | {
53 | indexedText: search.indexText('Red Cherry Red'),
54 | id: 1
55 | },
56 | {
57 | indexedText: search.indexText('Red Cherry'),
58 | id: 2
59 | }
60 | ];
61 |
62 | var matches = search.searchObjectsWithIndexedText(all, 'red');
63 | assert.equal(3, matches.length);
64 | assert.equal(1, matches[0].id);
65 | });
66 |
67 |
68 | it('scores more search terms higher', function() {
69 | var all = [
70 | {
71 | indexedText: search.indexText('Red Cherry'),
72 | id: 0
73 | },
74 | {
75 | indexedText: search.indexText('Red Apple'),
76 | id: 1
77 | },
78 | {
79 | indexedText: search.indexText('Red Grapes'),
80 | id: 2
81 | }
82 | ];
83 |
84 | var matches = search.searchObjectsWithIndexedText(all, 'red apple');
85 | assert.equal(3, matches.length);
86 | assert.equal(1, matches[0].id);
87 | });
88 |
89 | it('should prefer more search word matches', function() {
90 | var all = [
91 | {
92 | indexedText: search.indexText('book'),
93 | id: 0
94 | },
95 | {
96 | indexedText: search.indexText('book fruit'),
97 | id: 1
98 | },
99 | {
100 | indexedText: search.indexText('fruit book fruit'),
101 | id: 2
102 | },
103 | {
104 | indexedText: search.indexText('fruit'),
105 | id: 3
106 | }
107 | ];
108 |
109 | var matches = search.searchObjectsWithIndexedText(all, 'fruit book');
110 | assert.equal(4, matches.length);
111 | assert.equal(2, matches[0].id);
112 | });
113 |
114 | });
115 | });
--------------------------------------------------------------------------------