1#! /usr/bin/env python
2
3"""Tkinter-based GUI for websucker.
4
5Easy use: type or paste source URL and destination directory in
6their respective text boxes, click GO or hit return, and presto.
7"""
8
9from Tkinter import *
10import websucker
11import os
12import threading
13import Queue
14import time
15
16VERBOSE = 2
17
18
19try:
20    class Canceled(Exception):
21        "Exception used to cancel run()."
22except (NameError, TypeError):
23    Canceled = __name__ + ".Canceled"
24
25
26class SuckerThread(websucker.Sucker):
27
28    stopit = 0
29    savedir = None
30    rootdir = None
31
32    def __init__(self, msgq):
33        self.msgq = msgq
34        websucker.Sucker.__init__(self)
35        self.setflags(verbose=VERBOSE)
36        self.urlopener.addheaders = [
37            ('User-agent', 'websucker/%s' % websucker.__version__),
38        ]
39
40    def message(self, format, *args):
41        if args:
42            format = format%args
43        ##print format
44        self.msgq.put(format)
45
46    def run1(self, url):
47        try:
48            try:
49                self.reset()
50                self.addroot(url)
51                self.run()
52            except Canceled:
53                self.message("[canceled]")
54            else:
55                self.message("[done]")
56        finally:
57            self.msgq.put(None)
58
59    def savefile(self, text, path):
60        if self.stopit:
61            raise Canceled
62        websucker.Sucker.savefile(self, text, path)
63
64    def getpage(self, url):
65        if self.stopit:
66            raise Canceled
67        return websucker.Sucker.getpage(self, url)
68
69    def savefilename(self, url):
70        path = websucker.Sucker.savefilename(self, url)
71        if self.savedir:
72            n = len(self.rootdir)
73            if path[:n] == self.rootdir:
74                path = path[n:]
75                while path[:1] == os.sep:
76                    path = path[1:]
77                path = os.path.join(self.savedir, path)
78        return path
79
80    def XXXaddrobot(self, *args):
81        pass
82
83    def XXXisallowed(self, *args):
84        return 1
85
86
87class App:
88
89    sucker = None
90    msgq = None
91
92    def __init__(self, top):
93        self.top = top
94        top.columnconfigure(99, weight=1)
95        self.url_label = Label(top, text="URL:")
96        self.url_label.grid(row=0, column=0, sticky='e')
97        self.url_entry = Entry(top, width=60, exportselection=0)
98        self.url_entry.grid(row=0, column=1, sticky='we',
99                    columnspan=99)
100        self.url_entry.focus_set()
101        self.url_entry.bind("<Key-Return>", self.go)
102        self.dir_label = Label(top, text="Directory:")
103        self.dir_label.grid(row=1, column=0, sticky='e')
104        self.dir_entry = Entry(top)
105        self.dir_entry.grid(row=1, column=1, sticky='we',
106                    columnspan=99)
107        self.go_button = Button(top, text="Go", command=self.go)
108        self.go_button.grid(row=2, column=1, sticky='w')
109        self.cancel_button = Button(top, text="Cancel",
110                        command=self.cancel,
111                                    state=DISABLED)
112        self.cancel_button.grid(row=2, column=2, sticky='w')
113        self.auto_button = Button(top, text="Paste+Go",
114                      command=self.auto)
115        self.auto_button.grid(row=2, column=3, sticky='w')
116        self.status_label = Label(top, text="[idle]")
117        self.status_label.grid(row=2, column=4, sticky='w')
118        self.top.update_idletasks()
119        self.top.grid_propagate(0)
120
121    def message(self, text, *args):
122        if args:
123            text = text % args
124        self.status_label.config(text=text)
125
126    def check_msgq(self):
127        while not self.msgq.empty():
128            msg = self.msgq.get()
129            if msg is None:
130                self.go_button.configure(state=NORMAL)
131                self.auto_button.configure(state=NORMAL)
132                self.cancel_button.configure(state=DISABLED)
133                if self.sucker:
134                    self.sucker.stopit = 0
135                self.top.bell()
136            else:
137                self.message(msg)
138        self.top.after(100, self.check_msgq)
139
140    def go(self, event=None):
141        if not self.msgq:
142            self.msgq = Queue.Queue(0)
143            self.check_msgq()
144        if not self.sucker:
145            self.sucker = SuckerThread(self.msgq)
146        if self.sucker.stopit:
147            return
148        self.url_entry.selection_range(0, END)
149        url = self.url_entry.get()
150        url = url.strip()
151        if not url:
152            self.top.bell()
153            self.message("[Error: No URL entered]")
154            return
155        self.rooturl = url
156        dir = self.dir_entry.get().strip()
157        if not dir:
158            self.sucker.savedir = None
159        else:
160            self.sucker.savedir = dir
161            self.sucker.rootdir = os.path.dirname(
162                websucker.Sucker.savefilename(self.sucker, url))
163        self.go_button.configure(state=DISABLED)
164        self.auto_button.configure(state=DISABLED)
165        self.cancel_button.configure(state=NORMAL)
166        self.message( '[running...]')
167        self.sucker.stopit = 0
168        t = threading.Thread(target=self.sucker.run1, args=(url,))
169        t.start()
170
171    def cancel(self):
172        if self.sucker:
173            self.sucker.stopit = 1
174        self.message("[canceling...]")
175
176    def auto(self):
177        tries = ['PRIMARY', 'CLIPBOARD']
178        text = ""
179        for t in tries:
180            try:
181                text = self.top.selection_get(selection=t)
182            except TclError:
183                continue
184            text = text.strip()
185            if text:
186                break
187        if not text:
188            self.top.bell()
189            self.message("[Error: clipboard is empty]")
190            return
191        self.url_entry.delete(0, END)
192        self.url_entry.insert(0, text)
193        self.go()
194
195
196class AppArray:
197
198    def __init__(self, top=None):
199        if not top:
200            top = Tk()
201            top.title("websucker GUI")
202            top.iconname("wsgui")
203            top.wm_protocol('WM_DELETE_WINDOW', self.exit)
204        self.top = top
205        self.appframe = Frame(self.top)
206        self.appframe.pack(fill='both')
207        self.applist = []
208        self.exit_button = Button(top, text="Exit", command=self.exit)
209        self.exit_button.pack(side=RIGHT)
210        self.new_button = Button(top, text="New", command=self.addsucker)
211        self.new_button.pack(side=LEFT)
212        self.addsucker()
213        ##self.applist[0].url_entry.insert(END, "http://www.python.org/doc/essays/")
214
215    def addsucker(self):
216        self.top.geometry("")
217        frame = Frame(self.appframe, borderwidth=2, relief=GROOVE)
218        frame.pack(fill='x')
219        app = App(frame)
220        self.applist.append(app)
221
222    done = 0
223
224    def mainloop(self):
225        while not self.done:
226            time.sleep(0.1)
227            self.top.update()
228
229    def exit(self):
230        for app in self.applist:
231            app.cancel()
232            app.message("[exiting...]")
233        self.done = 1
234
235
236def main():
237    AppArray().mainloop()
238
239if __name__ == '__main__':
240    main()
241