uprclsearch.py 6.8 KB
Newer Older
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
1
#!/usr/bin/env python
Jean-Francois Dockes's avatar
Jean-Francois Dockes committed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#
# Copyright (C) 2017 J.F.Dockes
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
17
18
from __future__ import print_function

Jean-Francois Dockes's avatar
uprcl    
Jean-Francois Dockes committed
19
import re
20
21
22
import uprclfolders
from recoll import recoll

23
24
from upmplgutils import uplog
from uprclutils import stringToStrings, rcldoctoentry, cmpentries
25

Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def _getchar(s, i):
    if i < len(s):
        return i+1,s[i]
    else:
        return i,None

def _readword(s, i):
    w = ''
    for j in range(i, len(s)):
        if s[j].isspace():
            return j,w
        w += s[j]
    return j,w

40
41
42
43
44
45
46
47
48
49
50
51
# Called with '"' already read.

# Upnp search term strings are double quoted, but we should not take
# them as recoll phrases. We separate parts which are internally
# quoted, and become phrases, and lists of words which we interpret as
# an and search (comma-separated). Internal quotes come backslash-escaped
def _parsestring(s, i=0):
    uplog("parseString: input: <%s>" % s[i:])
    # First change '''"hello \"one phrase\"''' world" into
    #  '''hello "one phrase" world'''
    # Note that we can't handle quoted dquotes inside string
    str = ''
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
52
    escape = False
53
    instring = False
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
54
    for j in range(i, len(s)):
55
56
57
58
59
60
61
62
63
64
65
66
67
        if instring:
            if escape:
                if s[j] == '"':
                    str += '"'
                    instring = False
                else:
                    str += '\\' + s[j]
                escape = False
            else:
                if s[j] == '\\':
                    escape = True
                else:
                    str += s[j]
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
68
69

        else:
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
            if escape:
                str += s[j]
                escape = False
                if s[j] == '"':
                    instring = True
            else:
                if s[j] == '\\':
                    escape = True
                elif s[j] == '"':
                    j += 2
                    break
                else:
                    str += s[j]
                
    tokens = stringToStrings(str)
    return j, tokens

def _appendterms(out, v, field, oper):
    uplog("_appendterms: v %s field <%s> oper <%s>" % (v,field,oper))
    swords = ""
    phrases = []
    for w in v:
        if len(w.split()) == 1:
            if swords:
                swords += ","
            swords += w
        else:
            phrases.append(w)
    out.append(swords)
    for ph in phrases:
        out.append(field)
        out.append(oper)
        out.append('"' + ph + '"')
            
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
104
def upnpsearchtorecoll(s):
105
    uplog("upnpsearchtorecoll:in: <%s>" % s)
Jean-Francois Dockes's avatar
uprcl    
Jean-Francois Dockes committed
106
107

    s = re.sub('[\t\n\r\f ]+', ' ', s)
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
108
109
110
111

    out = []
    hadDerived = False
    i = 0
112
113
    field = ""
    oper = ""
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
114
115
116
117
    while True:
        i,c = _getchar(s, i)
        if not c:
            break
Jean-Francois Dockes's avatar
ckpt    
Jean-Francois Dockes committed
118
        #uplog("upnpsearchtorecoll: nextchar: <%s>" % c)
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
119
120
121
122
123
124
125
126
127
128
129

        if c.isspace():
            continue

        if c == "*":
            if (len(out) > 1 or (len(out) == 1 and not out[-1].isspace())) or \
                   (len(s[i:]) and not s[i:].isspace()):
                raise Exception("If * is used it must be the only input")
            out = ["mime:*"]
            break

130
        if c == '(' or c == ')': 
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
131
            out.append(c)
132
133
        elif c == '>' or c == '<' or c == '=':
            oper += c
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
134
135
        else:
            if c == '"':
136
137
138
139
140
141
                i,v = _parsestring(s, i)
                uplog("_parsestring ret: %s" % v)
                _appendterms(out, v, field, oper)
                oper = ""
                field = ""
                continue
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
142
143
144
            else:
                i -= 1
                i,w = _readword(s, i)
Jean-Francois Dockes's avatar
ckpt    
Jean-Francois Dockes committed
145
                #uplog("_readword returned <%s>" % w)
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
146
147
148

            if w == 'contains':
                out.append(':')
149
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
150
151
152
153
154
            elif w == 'doesNotContain':
                if len(out) < 1:
                    raise Exception("doesNotContain can't be the first word")
                out.insert(-1, "-")
                out.append(':')
155
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
156
157
158
            elif w == 'derivedFrom':
                hadDerived = True
                out.append(':')
159
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
160
161
            elif w == 'true':
                out.append('*')
162
                oper = ""
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
163
164
165
166
            elif w == 'false':
                out.append('xxxjanzocsduochterrrrm')
            elif w == 'exists':
                out.append(':')
167
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
168
169
170
171
172
173
174
            elif w == 'and':
                # Recoll has implied AND, but see next
                pass
            elif w == 'or':
                # Does not work because OR/AND priorities are reversed
                # between recoll and upnp. This would be very
                # difficult to correct, let's hope that the callers
175
                # use parentheses
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
176
177
                out.append('OR')
            else:
Jean-Francois Dockes's avatar
ckpt    
Jean-Francois Dockes committed
178
179
180
181
                try:
                    field = upnp2rclfields[w]
                except:
                    field = w
182
183
                out.append(field)
                oper = ""
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
184
185
186
187

    ostr = ""
    for tok in out:
        ostr += tok + " "
188
    uplog("upnpsearchtorecoll:out: <%s>" % ostr)
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
189
190
191
    return ostr


192
193
194
195
def search(rclconfdir, objid, upnps, idprefix, httphp, pathprefix):
    rcls = upnpsearchtorecoll(upnps)

    filterdir = uprclfolders.dirpath(objid)
196
197
    if filterdir and filterdir != "/":
        rcls += " dir:\"" + filterdir + "\""
198
    
199
    uplog("Search: recoll search: <%s>" % rcls)
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

    rcldb = recoll.connect(confdir=rclconfdir)
    try:
        rclq = rcldb.query()
        rclq.execute(rcls)
    except Exception as e:
        uplog("Search: recoll query raised: %s" % e)
        return []
    
    uplog("Estimated query results: %d" % (rclq.rowcount))
    if rclq.rowcount == 0:
        return []
    
    entries = []
    maxcnt = 0
    while True:
        docs = rclq.fetchmany()
        for doc in docs:
            id = idprefix + '$' + 'seeyoulater'
            e = rcldoctoentry(id, objid, httphp, pathprefix, doc)
220
221
222
223
            if e:
                entries.append(e)
        if (maxcnt > 0 and len(entries) >= maxcnt) or \
               len(docs) != rclq.arraysize:
224
            break
225
    uplog("Search retrieved %d docs" % (len(entries),))
226

227
    return sorted(entries, cmp=cmpentries)
228
229
230



Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
231
232
233
234
235
if __name__ == '__main__':
    s = '(upnp:artist derivedFrom  "abc\\"def\\g") or (dc:title:xxx) '
    print("INPUT: %s" % s)
    o = upnpsearchtorecoll(s)
    print("OUTPUT: %s" % o)