├── .gitignore
├── LICENSE
├── README.md
├── alfred
├── document.png
├── icon.png
├── info.plist
└── scripts
└── scripts
├── service
├── __init__.py
├── alfred.py
└── cli.py
├── spillo.py
└── spillo
├── __init__.py
├── account.py
├── bookmark.py
├── database.py
├── emitter.py
└── query.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | .idea
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *,cover
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 |
56 | # Sphinx documentation
57 | docs/_build/
58 |
59 | # PyBuilder
60 | target/
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Damien DeVille
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spillo for Alfred
2 |
3 | An Alfred workflow that lets you search your bookmarks in [Spillo](http://bananafishsoftware.com/products/spillo).
4 |
5 | You can download the latest version of the workflow from [here](https://github.com/ddeville/spillo-alfred/releases/latest) or from [Packal](http://www.packal.org/workflow/spillo).
6 |
7 | ## Usage
8 |
9 | You can invoke the workflow by using the `spl` command in Alfred.
10 |
11 | You can search in two different ways:
12 |
13 | ### Global
14 |
15 | Just type some text after the `spl` command and Spillo will search against the title, URL, description and tags of your bookmarks. Simple.
16 |
17 | ```
18 | spl objective-c atomics
19 | ```
20 |
21 | ### Specific
22 |
23 | If you want more control, you can specify various parameters in your search. The following parameters are supported:
24 |
25 | - `-n`/`--name`: The name of the bookmark (text)
26 | - `-u`/`--url`: The URL of the bookmark (text)
27 | - `-d`/`--desc`: The description of the bookmark (text)
28 | - `-t`/`--tags`: The tags of the bookmark (text, separated by space)
29 | - `-un`/`--unread`: Whether the bookmark is unread (`1` or `0`, `true` or `false`, `yes` or `no`)
30 | - `-pu`/`--public`: Whether the bookmark is public (`1` or `0`, `true` or `false`, `yes` or `no`)
31 |
32 | You can use multiple parameters at the same time.
33 |
34 | ```
35 | spl -n nullability -t objc swift -un 1
36 | ```
37 |
38 | #### Unread
39 |
40 | You can also see a list of your unread bookmarks by using the `splunread` command in Alfred.
41 |
42 | ## Hotkeys
43 |
44 | The workflow has some hotkeys setup that you can use to start a search of view your unread bookmarks. These hotkeys are left blank by default but you can easily set them up by editing them in the workflow.
45 |
46 | ## Actions
47 |
48 | When you have found the bookmark you are looking for, you can open it in your default browser. Just hit Return or click on the bookmark in the Alfred search results.
49 |
50 | Alternatively, you can open the bookmark in the background by holding the option key.
51 |
52 | You can also open the bookmark in Spillo by holding the command key.
53 |
54 | ## Notes
55 |
56 | Note that Spillo needs to be installed and authenticated on your machine for the workflow to work.
57 |
--------------------------------------------------------------------------------
/alfred/document.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddeville/spillo-alfred/0f88ad7f90cc8e2773882b12dd2854a603cfab17/alfred/document.png
--------------------------------------------------------------------------------
/alfred/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddeville/spillo-alfred/0f88ad7f90cc8e2773882b12dd2854a603cfab17/alfred/icon.png
--------------------------------------------------------------------------------
/alfred/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bundleid
6 | com.ddeville.spillo.alfred
7 | category
8 | Productivity
9 | connections
10 |
11 | 6C68D0BB-A89A-4FDD-BB99-B5A5B460646E
12 |
13 |
14 | destinationuid
15 | 0EB96C1E-0FCD-4072-BA49-18885F489BE2
16 | modifiers
17 | 0
18 | modifiersubtext
19 |
20 |
21 |
22 | destinationuid
23 | DA1739EB-62FF-4F27-BA0E-1BFBDC136AC2
24 | modifiers
25 | 1048576
26 | modifiersubtext
27 |
28 |
29 |
30 | destinationuid
31 | 47697E2F-6FD5-4B01-8C78-E2E1E5596CAE
32 | modifiers
33 | 524288
34 | modifiersubtext
35 |
36 |
37 |
38 | BB06C0C1-DFC1-4B57-9C0F-E56ED87BC903
39 |
40 |
41 | destinationuid
42 | 0EB96C1E-0FCD-4072-BA49-18885F489BE2
43 | modifiers
44 | 0
45 | modifiersubtext
46 |
47 |
48 |
49 | destinationuid
50 | DA1739EB-62FF-4F27-BA0E-1BFBDC136AC2
51 | modifiers
52 | 1048576
53 | modifiersubtext
54 |
55 |
56 |
57 | destinationuid
58 | 47697E2F-6FD5-4B01-8C78-E2E1E5596CAE
59 | modifiers
60 | 524288
61 | modifiersubtext
62 |
63 |
64 |
65 |
66 | createdby
67 | Bananafish Software
68 | description
69 | Search your bookmarks in Spillo
70 | disabled
71 |
72 | name
73 | Search Bookmarks in Spillo
74 | objects
75 |
76 |
77 | config
78 |
79 | plusspaces
80 |
81 | url
82 | {query}
83 | utf8
84 |
85 |
86 | type
87 | alfred.workflow.action.openurl
88 | uid
89 | 0EB96C1E-0FCD-4072-BA49-18885F489BE2
90 | version
91 | 0
92 |
93 |
94 | config
95 |
96 | argumenttype
97 | 0
98 | escaping
99 | 68
100 | keyword
101 | spl
102 | queuedelaycustom
103 | 3
104 | queuedelayimmediatelyinitially
105 |
106 | queuedelaymode
107 | 0
108 | queuemode
109 | 1
110 | runningsubtext
111 | Please wait, currently searching in Spillo…
112 | script
113 | import os, subprocess, sys
114 |
115 | script = os.path.join(os.path.dirname(os.path.realpath('__file__')), 'scripts/spillo.py')
116 | output = subprocess.check_output([script, '--service', 'alfred', '--query', "{query}"])
117 |
118 | sys.stdout.write(output)
119 | subtext
120 | Enter a query to search bookmarks in Spillo
121 | title
122 | Search in Spillo
123 | type
124 | 3
125 | withspace
126 |
127 |
128 | type
129 | alfred.workflow.input.scriptfilter
130 | uid
131 | 6C68D0BB-A89A-4FDD-BB99-B5A5B460646E
132 | version
133 | 0
134 |
135 |
136 | config
137 |
138 | plusspaces
139 |
140 | url
141 | spillo:///view_post?url={query}
142 | utf8
143 |
144 |
145 | type
146 | alfred.workflow.action.openurl
147 | uid
148 | DA1739EB-62FF-4F27-BA0E-1BFBDC136AC2
149 | version
150 | 0
151 |
152 |
153 | config
154 |
155 | argumenttype
156 | 2
157 | escaping
158 | 102
159 | keyword
160 | splunread
161 | queuedelaycustom
162 | 3
163 | queuedelayimmediatelyinitially
164 |
165 | queuedelaymode
166 | 0
167 | queuemode
168 | 1
169 | runningsubtext
170 | Please wait, currently searching in Spillo…
171 | script
172 | import os, subprocess, sys
173 |
174 | script = os.path.join(os.path.dirname(os.path.realpath('__file__')), 'scripts/spillo.py')
175 | output = subprocess.check_output([script, '--service', 'alfred', '--query', "-un 1"])
176 |
177 | sys.stdout.write(output)
178 | subtext
179 | View a list of unread bookmarks in Spillo
180 | title
181 | View Unread in Spillo
182 | type
183 | 3
184 | withspace
185 |
186 |
187 | type
188 | alfred.workflow.input.scriptfilter
189 | uid
190 | BB06C0C1-DFC1-4B57-9C0F-E56ED87BC903
191 | version
192 | 0
193 |
194 |
195 | config
196 |
197 | concurrently
198 |
199 | escaping
200 | 68
201 | script
202 | import subprocess
203 | from Foundation import NSURL
204 | from AppKit import NSWorkspace
205 |
206 | default_browser_path = NSWorkspace.sharedWorkspace().URLForApplicationToOpenURL_(NSURL.URLWithString_('http://google.com')).path()
207 |
208 | subprocess.check_output(['open', '-a', default_browser_path, '--background', '{query}'])
209 |
210 | type
211 | 3
212 |
213 | type
214 | alfred.workflow.action.script
215 | uid
216 | 47697E2F-6FD5-4B01-8C78-E2E1E5596CAE
217 | version
218 | 0
219 |
220 |
221 | readme
222 |
223 | uidata
224 |
225 | 0EB96C1E-0FCD-4072-BA49-18885F489BE2
226 |
227 | ypos
228 | 10
229 |
230 | 47697E2F-6FD5-4B01-8C78-E2E1E5596CAE
231 |
232 | ypos
233 | 280
234 |
235 | 6C68D0BB-A89A-4FDD-BB99-B5A5B460646E
236 |
237 | ypos
238 | 80
239 |
240 | BB06C0C1-DFC1-4B57-9C0F-E56ED87BC903
241 |
242 | ypos
243 | 190
244 |
245 | DA1739EB-62FF-4F27-BA0E-1BFBDC136AC2
246 |
247 | ypos
248 | 140
249 |
250 |
251 | webaddress
252 | http://bananafishsoftware.com/products/spillo
253 |
254 |
255 |
--------------------------------------------------------------------------------
/alfred/scripts:
--------------------------------------------------------------------------------
1 | ../scripts/
--------------------------------------------------------------------------------
/scripts/service/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddeville/spillo-alfred/0f88ad7f90cc8e2773882b12dd2854a603cfab17/scripts/service/__init__.py
--------------------------------------------------------------------------------
/scripts/service/alfred.py:
--------------------------------------------------------------------------------
1 | from spillo.emitter import Emitter
2 |
3 | from xml.etree.ElementTree import (
4 | Element,
5 | SubElement,
6 | tostring,
7 | )
8 |
9 |
10 | class AlfredEmitter(Emitter):
11 | def generate_empty(self):
12 | return self.generate_output([])
13 |
14 | def generate_output(self, bookmarks):
15 | items_element = Element('items')
16 |
17 | if not bookmarks:
18 | item_element = SubElement(items_element, 'item', {'valid': 'NO'})
19 |
20 | title_element = SubElement(item_element, 'title')
21 | title_element.text = 'No Results'
22 |
23 | subtitle_element = SubElement(item_element, 'subtitle')
24 | subtitle_element.text = 'Could not find any bookmark matching your search query'
25 | else:
26 | for bookmark in bookmarks:
27 | item_element = SubElement(items_element, 'item', {'arg': bookmark.url, 'uid': bookmark.identifier})
28 |
29 | title_element = SubElement(item_element, 'title')
30 | title_element.text = bookmark.title
31 |
32 | subtitle_element = SubElement(item_element, 'subtitle')
33 | subtitle_element.text = bookmark.url
34 |
35 | subtitle_alt_element = SubElement(item_element, 'subtitle', {'mod': 'cmd'})
36 | subtitle_alt_element.text = 'Open bookmark in Spillo'
37 |
38 | subtitle_alt_element = SubElement(item_element, 'subtitle', {'mod': 'alt'})
39 | subtitle_alt_element.text = 'Open URL in the background'
40 |
41 | icon_element = SubElement(item_element, 'icon')
42 | icon_element.text = 'document.png'
43 |
44 | copy_element = SubElement(item_element, 'text', {'type': 'copy'})
45 | copy_element.text = bookmark.url
46 |
47 | largetype_element = SubElement(item_element, 'text', {'type': 'largetype'})
48 | largetype_element.text = bookmark.title
49 |
50 | return tostring(items_element)
51 |
52 | def generate_error(self, error):
53 | items_element = Element('items')
54 |
55 | item_element = SubElement(items_element, 'item', {'valid': 'NO'})
56 |
57 | title_element = SubElement(item_element, 'title')
58 | title_element.text = 'There was an error while querying Spillo'
59 |
60 | subtitle_element = SubElement(item_element, 'subtitle')
61 | subtitle_element.text = error
62 |
63 | return tostring(items_element)
64 |
--------------------------------------------------------------------------------
/scripts/service/cli.py:
--------------------------------------------------------------------------------
1 | from spillo.emitter import Emitter
2 |
3 |
4 | class CLIEmitter(Emitter):
5 | def generate_output(self, bookmarks):
6 | if not bookmarks:
7 | return ""
8 | else:
9 | output = ""
10 | for bookmark in bookmarks:
11 | output += bookmark.title
12 | output += ' - '
13 | output += bookmark.url
14 | output += '\n'
15 | return output
16 |
--------------------------------------------------------------------------------
/scripts/spillo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import getopt
4 | import sys
5 |
6 | from service.alfred import AlfredEmitter
7 | from service.cli import CLIEmitter
8 |
9 | from spillo.account import retrieve_account_identifiers
10 | from spillo.database import Database, DatabaseException
11 | from spillo.query import Query, QueryException
12 |
13 |
14 | def main(argv):
15 | def _emit(output_str):
16 | sys.stdout.write(output_str + '\n')
17 |
18 | def _emit_message_and_exit(message, exit_code=1):
19 | _emit(message)
20 | sys.exit(exit_code)
21 |
22 | # attempt to parse the command line arguments
23 | try:
24 | s, q = _parse_arguments(argv)
25 | except RuntimeError as e:
26 | _emit_message_and_exit(str(e))
27 | return
28 |
29 | # get the right emitter based on the service
30 | if s == 'cli':
31 | emitter = CLIEmitter()
32 | elif s == 'alfred':
33 | emitter = AlfredEmitter()
34 | else:
35 | _emit_message_and_exit('unknown service ' + s)
36 | return
37 |
38 | # parse the query and emit an empty response if there is none
39 | try:
40 | query = Query.parse_query(q)
41 | except QueryException:
42 | _emit_message_and_exit(emitter.generate_empty(), 0)
43 | return
44 |
45 | # retrieve the first account identifier
46 | account_identifiers = retrieve_account_identifiers()
47 | if account_identifiers is None or len(account_identifiers) == 0:
48 | _emit_message_and_exit('No account set up in Spillo')
49 |
50 | # create a database, query it and generate some output via the emitter
51 | try:
52 | database = Database(account_identifiers[0])
53 | output = emitter.generate_output(database.query(query))
54 | except IOError:
55 | output = emitter.generate_error('Cannot find Spillo database, make sure that Spillo is installed')
56 | except DatabaseException:
57 | output = emitter.generate_error('There was an unknown error while querying the database')
58 |
59 | _emit(output)
60 |
61 |
62 | def _parse_arguments(argv):
63 | """Parse the command line arguments and returns the query"""
64 | usage = 'spillo.py -s -q '
65 |
66 | try:
67 | opts, args = getopt.getopt(argv, 's:q:', ['service=', 'query='])
68 | except getopt.GetoptError:
69 | raise RuntimeError(usage)
70 |
71 | query = None
72 | service = None
73 |
74 | for opt, arg in opts:
75 | if opt in ('-s', '--service'):
76 | service = arg
77 | elif opt in ('-q', '--query'):
78 | query = arg
79 |
80 | if service is None or query is None:
81 | raise RuntimeError(usage)
82 |
83 | return service, query
84 |
85 | if __name__ == "__main__":
86 | main(sys.argv[1:])
87 |
--------------------------------------------------------------------------------
/scripts/spillo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddeville/spillo-alfred/0f88ad7f90cc8e2773882b12dd2854a603cfab17/scripts/spillo/__init__.py
--------------------------------------------------------------------------------
/scripts/spillo/account.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from Foundation import (
4 | NSData,
5 | NSLibraryDirectory,
6 | NSPropertyListSerialization,
7 | NSSearchPathForDirectoriesInDomains,
8 | NSUserDomainMask,
9 | )
10 |
11 | def retrieve_account_identifiers():
12 | path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, True).firstObject()
13 | path = os.path.join(path,
14 | 'Group Containers',
15 | 'Q8B696Y8U4.com.ddeville.spillo',
16 | 'Library',
17 | 'Preferences',
18 | 'Q8B696Y8U4.com.ddeville.spillo.plist'
19 | )
20 |
21 | data = NSData.dataWithContentsOfFile_(path)
22 | if data is None:
23 | return None
24 |
25 | defaults = NSPropertyListSerialization.propertyListWithData_options_format_error_(data, 0, None, None)[0]
26 | if defaults is None:
27 | return None
28 |
29 | accounts = defaults.get("accounts")
30 | if accounts is None:
31 | return None
32 |
33 | return accounts.valueForKey_("identifier")
34 |
--------------------------------------------------------------------------------
/scripts/spillo/bookmark.py:
--------------------------------------------------------------------------------
1 | class Bookmark(object):
2 | _url = None
3 | _title = None
4 | _identifier = None
5 | _date = None
6 |
7 | def __init__(self, url, title, identifier, date):
8 | self._url = url
9 | self._title = title
10 | self._identifier = identifier
11 | self._date = date
12 |
13 | @property
14 | def url(self):
15 | return self._url
16 |
17 | @url.setter
18 | def url(self, url):
19 | self._url = url
20 |
21 | @property
22 | def title(self):
23 | return self._title
24 |
25 | @title.setter
26 | def title(self, title):
27 | self._title = title
28 |
29 | @property
30 | def identifier(self):
31 | return self._identifier
32 |
33 | @identifier.setter
34 | def identifier(self, identifier):
35 | self._identifier = identifier
36 |
37 | @property
38 | def date(self):
39 | return self._date
40 |
41 | @date.setter
42 | def date(self, date):
43 | self._date = date
44 |
--------------------------------------------------------------------------------
/scripts/spillo/database.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 |
4 | from query import (
5 | QueryGlobal,
6 | QuerySpecific,
7 | )
8 | from bookmark import Bookmark
9 |
10 | from Foundation import (
11 | NSLibraryDirectory,
12 | NSSearchPathForDirectoriesInDomains,
13 | NSUserDomainMask,
14 | )
15 |
16 |
17 | class Database(object):
18 | def __init__(self, account_identifier):
19 | self.connection = sqlite3.connect(self._retrieve_database_path(account_identifier))
20 |
21 | def query(self, query):
22 | try:
23 | cursor = self.connection.cursor()
24 | if isinstance(query, QueryGlobal):
25 | return self._query_global(cursor, query)
26 | elif isinstance(query, QuerySpecific):
27 | return self._query_specific(cursor, query)
28 | else:
29 | raise DatabaseException('Unexpected query type')
30 | except sqlite3.OperationalError:
31 | raise DatabaseException('There was an unknown error while querying the database')
32 |
33 | # the name or url or desc is like the search term or the tag is exactly the search term
34 | GLOBAL_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND \
35 | ZTITLE LIKE ? OR ZURL LIKE ? OR ZDESC LIKE ? OR Z_PK IN \
36 | (SELECT Z_2POSTS FROM Z_2TAGS WHERE Z_3TAGS == \
37 | (SELECT Z_PK FROM ZPINBOARDTAG WHERE ZTITLE == ? COLLATE NOCASE)) COLLATE NOCASE'
38 |
39 | def _query_global(self, cursor, query):
40 | # construct a search for each word in the query and intersect the queries
41 | queries = []
42 | params = []
43 | for word in query.value.split():
44 | queries.append(Database.GLOBAL_QUERY)
45 | params.append('%' + word + '%') # name
46 | params.append('%' + word + '%') # url
47 | params.append('%' + word + '%') # desc
48 | params.append(word) # tag
49 |
50 | if not queries:
51 | return None
52 |
53 | sql = ' INTERSECT '.join(queries) + ' ORDER BY ZDATE DESC'
54 | cursor.execute(sql, params)
55 | return self._generate_bookmarks(cursor)
56 |
57 | # the name is like the search term
58 | NAME_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND ZTITLE LIKE ? COLLATE NOCASE'
59 | # the url is like the search term
60 | URL_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND ZURL LIKE ? COLLATE NOCASE'
61 | # the desc is like the search term
62 | DESC_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND ZDESC LIKE ? COLLATE NOCASE'
63 | # the tag is exactly the search term
64 | TAG_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND Z_PK IN \
65 | (SELECT Z_2POSTS FROM Z_2TAGS WHERE Z_3TAGS == \
66 | (SELECT Z_PK FROM ZPINBOARDTAG WHERE ZTITLE == ? COLLATE NOCASE))'
67 | # the unread status is 1 or 0
68 | UNREAD_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND ZUNREAD == ?'
69 | # the public status is 1 or 0
70 | PUBLIC_QUERY = 'SELECT ZTITLE, ZURL, ZIDENTIFIER, ZDATE FROM ZPINBOARDPOST WHERE ZDELETING=0 AND ZSHARED == ?'
71 |
72 | def _query_specific(self, cursor, query):
73 | queries = []
74 | params = []
75 |
76 | def create_and_add_query(sql_query, term):
77 | # construct a query for each word in the search term
78 | for word in term.split():
79 | queries.append(sql_query)
80 | params.append('%' + word + '%')
81 |
82 | if query.name:
83 | create_and_add_query(Database.NAME_QUERY, query.name)
84 | if query.url:
85 | create_and_add_query(Database.URL_QUERY, query.url)
86 | if query.desc:
87 | create_and_add_query(Database.DESC_QUERY, query.desc)
88 |
89 | if query.tags:
90 | # construct an intersection of queries for each tag
91 | tag_queries = []
92 | for tag in query.tags:
93 | tag_queries.append(Database.TAG_QUERY)
94 | params.append(tag)
95 | queries.append(' INTERSECT '.join(tag_queries))
96 |
97 | if query.unread is not None: # check for None specifically since it can be 0
98 | queries.append(Database.UNREAD_QUERY)
99 | params.append(query.unread)
100 |
101 | if query.public is not None: # check for None specifically since it can be 0
102 | queries.append(Database.PUBLIC_QUERY)
103 | params.append(query.public)
104 |
105 | if not queries:
106 | return None
107 |
108 | sql = ' INTERSECT '.join(queries) + ' ORDER BY ZDATE DESC'
109 | cursor.execute(sql, params)
110 | return self._generate_bookmarks(cursor)
111 |
112 | @staticmethod
113 | def _retrieve_database_path(account_identifier):
114 | path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, True).firstObject()
115 | path = os.path.join(path,
116 | 'Group Containers',
117 | 'Q8B696Y8U4.com.ddeville.spillo',
118 | 'Library',
119 | 'Application Support',
120 | 'Stores',
121 | account_identifier,
122 | 'Core',
123 | 'Pinboard.sqlite'
124 | )
125 | # attempt to open the file so that we throw if it doesn't exist
126 | with open(path):
127 | pass
128 | return path
129 |
130 | @staticmethod
131 | def _generate_bookmarks(cursor):
132 | bookmarks = []
133 | for row in cursor:
134 | bookmarks.append(Bookmark(title=row[0], url=row[1], identifier=row[2], date=row[3]))
135 | return bookmarks
136 |
137 |
138 | class DatabaseException(Exception):
139 | pass
140 |
--------------------------------------------------------------------------------
/scripts/spillo/emitter.py:
--------------------------------------------------------------------------------
1 | class Emitter(object):
2 | def generate_empty(self):
3 | return ""
4 |
5 | def generate_output(self, bookmarks):
6 | return ""
7 |
8 | def generate_error(self, error):
9 | return error
10 |
--------------------------------------------------------------------------------
/scripts/spillo/query.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import sys
3 | import unicodedata
4 |
5 |
6 | class Query(object):
7 | @staticmethod
8 | def parse_query(query_string):
9 | """Factory method that parses a query string and returns a query
10 | subclass instance."""
11 | parser = _QueryParser()
12 |
13 | parser.add_argument('value', nargs='*')
14 | parser.add_argument('-n', '--name', nargs='+', dest='name')
15 | parser.add_argument('-u', '--url', nargs='+', dest='url')
16 | parser.add_argument('-d', '--desc', nargs='+', dest='desc')
17 | parser.add_argument('-t', '--tags', nargs='+', dest='tags')
18 | parser.add_argument('-un', '--unread', nargs='?', dest='unread')
19 | parser.add_argument('-pu', '--public', nargs='?', dest='public')
20 |
21 | # make sure that the unicode query string is decoded
22 | try:
23 | query_string = unicodedata.normalize('NFC', query_string.decode('utf-8'))
24 | except UnicodeDecodeError:
25 | pass
26 |
27 | # try to parse the arguments, if an exception is thrown it's because some args
28 | # are incomplete and we shouldn't return any result until they are
29 | try:
30 | args = vars(parser.parse_args(query_string.split()))
31 | except:
32 | raise QueryException()
33 |
34 | if args['value']:
35 | return QueryGlobal(_format_string_arg(args, 'value'))
36 |
37 | name = _format_string_arg(args, 'name')
38 | url = _format_string_arg(args, 'url')
39 | desc = _format_string_arg(args, 'desc')
40 | tags = args['tags']
41 | unread = _format_boolean_arg(args, 'unread')
42 | public = _format_boolean_arg(args, 'public')
43 |
44 | return QuerySpecific(name, url, desc, tags, unread, public)
45 |
46 |
47 | def _format_string_arg(args, key):
48 | return ' '.join(args[key]) if args[key] else None
49 |
50 |
51 | def _format_boolean_arg(args, key):
52 | value = args[key]
53 | if value:
54 | if isinstance(value, basestring):
55 | value = value.lower()
56 | if value == 'yes' or value == 'true':
57 | return 1
58 | if value == 'no' or value == 'false':
59 | return 0
60 | try:
61 | return 0 if int(value) == 0 else 1
62 | except (ValueError, TypeError):
63 | pass
64 | return None
65 |
66 |
67 | class QueryGlobal(Query):
68 | _value = None
69 |
70 | def __init__(self, value):
71 | self._value = value
72 |
73 | @property
74 | def value(self):
75 | return self._value
76 |
77 |
78 | class QuerySpecific(Query):
79 | _name = None
80 | _url = None
81 | _desc = None
82 | _tags = None
83 | _unread = None
84 | _public = None
85 |
86 | def __init__(self, name, url, desc, tags, unread, public):
87 | self._name = name
88 | self._url = url
89 | self._desc = desc
90 | self._tags = tags
91 | self._unread = unread
92 | self._public = public
93 |
94 | @property
95 | def name(self):
96 | return self._name
97 |
98 | @property
99 | def url(self):
100 | return self._url
101 |
102 | @property
103 | def desc(self):
104 | return self._desc
105 |
106 | @property
107 | def tags(self):
108 | return self._tags
109 |
110 | @property
111 | def unread(self):
112 | return self._unread
113 |
114 | @property
115 | def public(self):
116 | return self._public
117 |
118 |
119 | class QueryException(Exception):
120 | pass
121 |
122 |
123 | class _QueryParser(argparse.ArgumentParser):
124 | def _get_action_from_name(self, name):
125 | container = self._actions
126 | if name is None:
127 | return None
128 | for action in container:
129 | if '/'.join(action.option_strings) == name:
130 | return action
131 | elif action.metavar == name:
132 | return action
133 | elif action.dest == name:
134 | return action
135 |
136 | def error(self, message):
137 | exc = sys.exc_info()[1]
138 | if exc:
139 | exc.argument = self._get_action_from_name(exc.argument_name)
140 | raise exc
141 | super(_QueryParser, self).error(message)
142 |
--------------------------------------------------------------------------------