wplayer

Check-in [9d0528b2df]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add '/' to go straight to top of tree. Add search index & search function.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:9d0528b2df9ae4a25c5e264992c75851d4a68e93ebe81274bc8cd2ff85695551
User & Date: ajv-899-334-8894@vsta.org 2017-03-13 13:25:58
Context
2017-03-13
13:27
Note on search check-in: 8d3bd7470b user: ajv-899-334-8894@vsta.org tags: master, trunk
13:25
Add '/' to go straight to top of tree. Add search index & search function. check-in: 9d0528b2df user: ajv-899-334-8894@vsta.org tags: master, trunk
2016-11-24
17:26
Update example config check-in: dab09cff5c user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to get.py.

5
6
7
8
9
10
11



12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
..
43
44
45
46
47
48
49








50
51
52
53
54
55
56
..
59
60
61
62
63
64
65
66
67
68
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
























































































# Structure of paths in the server:
#  /
#	Main UI.  Top part is playlist, bottom is file/dir browser
#  /media/<prefix>/...
#	For each prefix of files, its contents is served by
#	way of its path under here.
import os, stat, urllib




# The GET part of our handling
class GET_mixin(object):

    # Configure our WPlayer GET treatment
    def __init__(self):

	# GET handlers
	self.dispatchers.append( ("GET", self.send_path) )


    # "/"; main UI
    def send_top(self):
	return self.send_files("html/top.html")

    # Provide a directory listing.  It's JSON with lists
    #  of files and directories (sorted!) in the named path.
    def _send_dir(self, path):
	res = {}
	for path,dirs,files in os.walk(path):
	    break
	else:
	    # If the walk got nothing, provide empty lists
	    files = dirs = ()

	# Normalize to UTF-8
................................................................................

	# Put into place
	res["files"] = files
	res["dirs"] = dirs

	# Back it goes, as a JSON-encoded buffer
	return self.send_json(res)









    # GET dispatcher
    # See if it's a configured path, and serve file or dir
    def send_path(self):
	app = self.server
	webroot = app.approot
	cpaths = webroot.config["files"]
................................................................................
	# We serve /media/prefix/blah/blah...
	if pp[0] != "media":
	    return False,None
	del pp[0]

	# Root; list configured prefixes themselves
	if not pp:
	    res = {"files": (),
		"dirs": sorted(tup[0] for tup in cpaths)}
	    buf = self.send_json(res)
	    return True,buf

	# Find the next path element
	for tup in cpaths:
	    if tup[0] == pp[0]:
		break
	else:
	    # Nope, we don't dispatch this path
	    return False,None

	# Their requested path, starting from here
	prefix = tup[1]["path"]

	if len(pp) > 1:
	    path = os.path.join(prefix, os.path.join(*pp[1:]))
	else:
	    path = prefix

	# File?  Or dir?
	st = os.stat(path)
	if stat.S_ISDIR(st.st_mode):
	    print "GET dir", path
	    buf = self._send_dir(path)
	else:
	    print "GET file", path
	    buf = self.send_files(path)
	return True,buf































































































>
>
>









>







|
|







 







>
>
>
>
>
>
>
>







 







|





|
|
<
<




|
>









|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
..
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
..
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# Structure of paths in the server:
#  /
#	Main UI.  Top part is playlist, bottom is file/dir browser
#  /media/<prefix>/...
#	For each prefix of files, its contents is served by
#	way of its path under here.
import os, stat, urllib

# Max search results when filtering
MAXSEARCH=20

# The GET part of our handling
class GET_mixin(object):

    # Configure our WPlayer GET treatment
    def __init__(self):

	# GET handlers
	self.dispatchers.append( ("GET", self.send_path) )
	self.dispatchers.append( ("GET", self.search) )

    # "/"; main UI
    def send_top(self):
	return self.send_files("html/top.html")

    # Provide a directory listing.  It's JSON with lists
    #  of files and directories (sorted!) in the named path.
    def _send_dir(self, indexed, path):
	res = {"index": indexed}
	for path,dirs,files in os.walk(path):
	    break
	else:
	    # If the walk got nothing, provide empty lists
	    files = dirs = ()

	# Normalize to UTF-8
................................................................................

	# Put into place
	res["files"] = files
	res["dirs"] = dirs

	# Back it goes, as a JSON-encoded buffer
	return self.send_json(res)

    # Look up prefix, get state for this prefix
    def get_cpath(self, elem):
	cpaths = self.server.approot.config["files"]
	for tup in cpaths:
	    if tup[0] == elem:
		return tup[1]
        return None

    # GET dispatcher
    # See if it's a configured path, and serve file or dir
    def send_path(self):
	app = self.server
	webroot = app.approot
	cpaths = webroot.config["files"]
................................................................................
	# We serve /media/prefix/blah/blah...
	if pp[0] != "media":
	    return False,None
	del pp[0]

	# Root; list configured prefixes themselves
	if not pp:
	    res = {"files": (), "index": False,
		"dirs": sorted(tup[0] for tup in cpaths)}
	    buf = self.send_json(res)
	    return True,buf

	# Find the next path element
        d = self.get_cpath(pp[0])
        if d is None:


	    # Nope, we don't dispatch this path
	    return False,None

	# Their requested path, starting from here
	prefix = d["path"]
        indexed = ('_index' in d)
	if len(pp) > 1:
	    path = os.path.join(prefix, os.path.join(*pp[1:]))
	else:
	    path = prefix

	# File?  Or dir?
	st = os.stat(path)
	if stat.S_ISDIR(st.st_mode):
	    print "GET dir", path
	    buf = self._send_dir(indexed, path)
	else:
	    print "GET file", path
	    buf = self.send_files(path)
	return True,buf

    # A search request
    #
    # If there's an index for this part of the tree, search
    #  for up to MAXSEARCH results and provide links
    def search(self):
        pp = self.paths

        # /search/<path...>?q=<query>
        if (len(pp) < 2) or (pp[0] != "search"):
            return False,None
        s = self.vals.get('q')
        if not s:
            return False,None

        # Which tree are we accessing?  Get its file index
	pp = tuple(urllib.unquote(p) for p in self.paths)
        d = self.get_cpath(pp[1])
        if (d is None) or ('_index' not in d):
            return False,None

        # Walk forward to part of tree which matches our
        #  current position

        # Sweep results
        matches = []
        s = s.lower()
        if len(pp) > 2:
            # Initial part to match, current folder
            prefix = os.path.join(*pp[2:])
            # (+1 to trim separating '/' after prefix)
            plen = len(prefix)+1
            reached = False
        else:
            # Searching from root of this index
            prefix = ''
            plen = 0
            reached = True

        # Walk index entries
        # tup[0] is the actual path name (mixed case), tup[1]
        #  is the version with case flattened to lower only.
        for tup in d['_index']:
            # Still walking forward to reach prefix match
            l = tup[0]
            if not reached:
                if not l.startswith(prefix):
                    continue
                reached = True

            # We're past the part of the index which matches
            #  our prefix
            ll = tup[1]
            if prefix:
                if not l.startswith(prefix):
                    break
                l = l[plen:]
                ll = ll[plen:]

            # See if our search string matches here
            if s in ll:
                matches.append(l)
                if len(matches) >= MAXSEARCH:
                    break
        
        # Here's a list of matching stuff
        # Parcel into file/dir just like a regular directory listing
	res = {"index": True, "files": [], "dirs": []}
        tp = d["path"]
        for bn in matches:
            fn = os.path.join(tp, prefix, bn)
            try:
                st = os.stat(fn)
            except:
                # Ignore broken file paths
                continue
            try:
                bn = urllib.quote(bn.decode('utf-8', 'replace'))
            except:
                # Ignore f*cked encodings
                continue
            if stat.S_ISDIR(st.st_mode):
                res["dirs"].append(bn)
            else:
                res["files"].append(bn)

        # Here's a result in the style of _send_dir() above
        return True,self.send_json(res)

Changes to html/top.html.

28
29
30
31
32
33
34

35
36
37
38
39
40
41

42
43
44
45
   </td>
  </tr>
 </table>
 <hr>
 <button onclick="return go_up();">
  <img src="/imgs/player_eject.png" alt="Go Up" />
 </button>

 <button onclick="return dropFirst();">-First</button>
 <button onclick="return dropLast();">-Last</button>
 <button onclick="return addAll();">+All</button>
 <button onclick="return dropAll();">-All</button>
 <br>
 <textarea rows="1" cols="40" readonly id="trackname"></textarea>
 <br>

 <span id="browser"></span>
 <script src="/js/player.js"></script>
</body>
</html>







>





|
|
>




28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
   </td>
  </tr>
 </table>
 <hr>
 <button onclick="return go_up();">
  <img src="/imgs/player_eject.png" alt="Go Up" />
 </button>
 <button onclick="return go_top();">/</button>
 <button onclick="return dropFirst();">-First</button>
 <button onclick="return dropLast();">-Last</button>
 <button onclick="return addAll();">+All</button>
 <button onclick="return dropAll();">-All</button>
 <br>
 <textarea rows="1" cols="40" readonly id="trackname"></textarea><br>
 <div id="searchbar" style="display: none;">Search: <textarea rows="1" cols="40"
     id="searchval" onkeyup="srch();"></textarea></br></div>
 <span id="browser"></span>
 <script src="/js/player.js"></script>
</body>
</html>

Changes to js/player.js.

21
22
23
24
25
26
27


28
29
30
31
32
33
34
..
48
49
50
51
52
53
54








55
56
57
58
59
60
61
...
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130





131
132
133
134
135
136
137
...
177
178
179
180
181
182
183










184
185
186
187

188
189
190
191
192
193
194
...
232
233
234
235
236
237
238

239
240
241





























242
243
244
245
246
247

// The playlist queue
var playlist = [];

// Current browsing path, and how "deep" we are down into it
var path = "";
var pdepth = 0;


// This will be path's contents; it goes to null while
//  it's getting new content for the current path
var pcontents = null;

// Do we have something on the turntable right now?
var playing = false;

................................................................................
    // Paint file browsing window
    //
    res = ''

    // Name of active track in its own position
    trackname.textContent = do_trim(unescape(path));









    // Directories (if any)
    if (pcontents.dirs.length > 0) {
	res += '<b>Directories</b>:<br>';
	for (var didx = 0; didx < pcontents.dirs.length; didx++) {
	    d = pcontents.dirs[didx];
	    res += '<button onclick="return go_dir(' + didx.toString() + ');">'
	    res += ' <img src="/imgs/player_play.png" alt="enter" />\n'
................................................................................
    for (var fidx = 0; fidx < playlist.length; fidx++) {
	f = do_trim(unescape(playlist[fidx]));
	res += f + '\n';
    }
    pqueue.textContent = res;
}

// For current path, get contents from web server
function curdir() {
    // Flag this op is pending
    pcontents = null;

    // XHR without timeout; get what we can right now
    var req = new XMLHttpRequest();
    req.open("GET", "/media" + path);
    req.onreadystatechange = function() {
	gotDir(req);
    }
    req.send();
}






// Update player position
function seek(delta) {
    var newTime = player.currentTime+delta;
    if (newTime >= 0) {
	player.currentTime = newTime;
    }
................................................................................
}

// Go up a level
function go_up() {
    if ((pcontents == null) || (pdepth == 0)) {
	return false;
    }










    var idx = path.lastIndexOf("/");
    path = path.slice(0, idx);
    pdepth -= 1;
    curdir();

    return false;
}

// Drop first thing on playlist (i.e., next to play)
function dropFirst() {
    if (playlist.length == 0) {
	return false;
................................................................................
function go_dir(didx) {
    // Working on a previous click
    if (pcontents == null) {
	return false;
    }
    path = path + "/" + pcontents.dirs[didx];
    pdepth += 1;

    curdir();
    return false;
}






























// Set up the player
player.addEventListener("ended", playNext);

// Initial browser window update
curdir();







>
>







 







>
>
>
>
>
>
>
>







 







|
|





|





>
>
>
>
>







 







>
>
>
>
>
>
>
>
>
>




>







 







>



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
..
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
...
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
...
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
...
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303

// The playlist queue
var playlist = [];

// Current browsing path, and how "deep" we are down into it
var path = "";
var pdepth = 0;
// Are we within a tree with a file index?
var indexed = false;
// This will be path's contents; it goes to null while
//  it's getting new content for the current path
var pcontents = null;

// Do we have something on the turntable right now?
var playing = false;

................................................................................
    // Paint file browsing window
    //
    res = ''

    // Name of active track in its own position
    trackname.textContent = do_trim(unescape(path));

    // Index available?
    if (indexed != pcontents.index) {
        // Clear old value as we switch modes
        searchval.value = '';
    }
    indexed = pcontents.index;
    searchbar.style.display = indexed ? '' : 'none';

    // Directories (if any)
    if (pcontents.dirs.length > 0) {
	res += '<b>Directories</b>:<br>';
	for (var didx = 0; didx < pcontents.dirs.length; didx++) {
	    d = pcontents.dirs[didx];
	    res += '<button onclick="return go_dir(' + didx.toString() + ');">'
	    res += ' <img src="/imgs/player_play.png" alt="enter" />\n'
................................................................................
    for (var fidx = 0; fidx < playlist.length; fidx++) {
	f = do_trim(unescape(playlist[fidx]));
	res += f + '\n';
    }
    pqueue.textContent = res;
}

// Get a dir/file listing from the URL
function getDir(url) {
    // Flag this op is pending
    pcontents = null;

    // XHR without timeout; get what we can right now
    var req = new XMLHttpRequest();
    req.open("GET", url);
    req.onreadystatechange = function() {
	gotDir(req);
    }
    req.send();
}

// For current path, get contents from web server
function curdir() {
    getDir("/media" + path);
}

// Update player position
function seek(delta) {
    var newTime = player.currentTime+delta;
    if (newTime >= 0) {
	player.currentTime = newTime;
    }
................................................................................
}

// Go up a level
function go_up() {
    if ((pcontents == null) || (pdepth == 0)) {
	return false;
    }

    // If looking at search filter, "up" means back out
    //  of this filtered view
    if (searchval.value != "") {
        searchval.value = "";
        curdir();
        return false;
    }

    // Otherwise pop up a directory level
    var idx = path.lastIndexOf("/");
    path = path.slice(0, idx);
    pdepth -= 1;
    curdir();

    return false;
}

// Drop first thing on playlist (i.e., next to play)
function dropFirst() {
    if (playlist.length == 0) {
	return false;
................................................................................
function go_dir(didx) {
    // Working on a previous click
    if (pcontents == null) {
	return false;
    }
    path = path + "/" + pcontents.dirs[didx];
    pdepth += 1;
    searchval.value = "";
    curdir();
    return false;
}

// Back to top of tree
function go_top() {
    path = "";
    pdepth = 0;
    curdir();
    return false;
}

// Incremental search
function srch() {
    // Action pending
    if (pcontents == null) {
        return;
    }

    // Probably backspace to clear out search field
    let s = searchval.value;
    if (s.length == 0) {
        curdir();
        return;
    }

    // Get a set of options based on the current path
    //  and search value
    const url = "/search" + path + "?q=" + escape(s);
    pdepth += 1;
    getDir(url);
}

// Set up the player
player.addEventListener("ended", playNext);

// Initial browser window update
curdir();

Changes to main.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16











17
18
19
20
21
22
23
..
29
30
31
32
33
34
35
36








37
38
39
40
41
42
43
#
# main.py
#	Main driver for WWW-Usenet interface
#
import sys, time
import chore
from get import GET_mixin
from chore.authen import Authen_mixin, Authen_Server_mixin

# Tie our various handlers together
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
        Authen_mixin):
    def __init__(self, conn, tup, approot):
	chore.handlers.Chore_Handler.__init__(self, conn, tup, approot,
	    (GET_mixin.__init__,
             Authen_mixin.__init__))












# Load our configuration file
#
# This includes configuring our config file elements,
#  then processing the supplied file.
def load_cfg(fn):

................................................................................

    # Here's how our config file looks
    c.onearg.add( ("files",) )
    c.mults.add( ("files",) )
    c.onearg.add( ("files", "path") )

    # Parse the input
    return c.load_cfg(fn)









# Root of our app server
class App(chore.server.Server, Authen_Server_mixin):

    def __init__(self, config):
	# Let Chore handle most things
	chore.server.Server.__init__(self, config, App_Handler);




|











>
>
>
>
>
>
>
>
>
>
>







 







|
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
..
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#
# main.py
#	Main driver for WWW-Usenet interface
#
import sys, time, os
import chore
from get import GET_mixin
from chore.authen import Authen_mixin, Authen_Server_mixin

# Tie our various handlers together
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
        Authen_mixin):
    def __init__(self, conn, tup, approot):
	chore.handlers.Chore_Handler.__init__(self, conn, tup, approot,
	    (GET_mixin.__init__,
             Authen_mixin.__init__))

# Load file index into memory
# This should be a flat text file, sorted, which we will use
#  to provide search
def load_index(fn):
    f = open(fn, 'r')
    # Ugh, we need both the original path, and the case-flattened
    #  variant for loose search matching
    idx = tuple( (l.strip(), l.strip().lower()) for l in f )
    f.close()
    return idx

# Load our configuration file
#
# This includes configuring our config file elements,
#  then processing the supplied file.
def load_cfg(fn):

................................................................................

    # Here's how our config file looks
    c.onearg.add( ("files",) )
    c.mults.add( ("files",) )
    c.onearg.add( ("files", "path") )

    # Parse the input
    cfg = c.load_cfg(fn)

    # Preload any file indices
    for tup in cfg["files"]:
        d = tup[1]["path"]
        fn = d + ".idx"
        if os.access(fn, os.R_OK):
            tup[1]["_index"] = load_index(fn)
    return cfg

# Root of our app server
class App(chore.server.Server, Authen_Server_mixin):

    def __init__(self, config):
	# Let Chore handle most things
	chore.server.Server.__init__(self, config, App_Handler);