uprclsearch.py 6.74 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
20
import re

21
22
23
24
import uprclfolders
from uprclutils import *
from recoll import recoll

Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

39
40
41
42
43
44
45
46
47
48
49
50
# 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
51
    escape = False
52
    instring = False
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
53
    for j in range(i, len(s)):
54
55
56
57
58
59
60
61
62
63
64
65
66
        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
67
68

        else:
69
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
            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
103
def upnpsearchtorecoll(s):
104
    uplog("upnpsearchtorecoll:in: <%s>" % s)
Jean-Francois Dockes's avatar
uprcl    
Jean-Francois Dockes committed
105
106

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

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

        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

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

            if w == 'contains':
                out.append(':')
148
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
149
150
151
152
153
            elif w == 'doesNotContain':
                if len(out) < 1:
                    raise Exception("doesNotContain can't be the first word")
                out.insert(-1, "-")
                out.append(':')
154
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
155
156
157
            elif w == 'derivedFrom':
                hadDerived = True
                out.append(':')
158
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
159
160
            elif w == 'true':
                out.append('*')
161
                oper = ""
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
162
163
164
165
            elif w == 'false':
                out.append('xxxjanzocsduochterrrrm')
            elif w == 'exists':
                out.append(':')
166
                oper = ':'
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
167
168
169
170
171
172
173
            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
174
                # use parentheses
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
175
176
                out.append('OR')
            else:
Jean-Francois Dockes's avatar
ckpt    
Jean-Francois Dockes committed
177
178
179
180
                try:
                    field = upnp2rclfields[w]
                except:
                    field = w
181
182
                out.append(field)
                oper = ""
Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
183
184
185
186

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


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

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

    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)
219
220
221
222
            if e:
                entries.append(e)
        if (maxcnt > 0 and len(entries) >= maxcnt) or \
               len(docs) != rclq.arraysize:
223
            break
224
    uplog("Search retrieved %d docs" % (len(entries),))
225

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



Jean-Francois Dockes's avatar
none    
Jean-Francois Dockes committed
230
231
232
233
234
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)