comparison mservtk.tcl @ 1:c36994199c5e MSERVTK_0_1

Initial revision
author darius
date Wed, 03 May 2000 12:20:47 +0000
parents
children 4343bc7f829a
comparison
equal deleted inserted replaced
0:c5aa7d041ea8 1:c36994199c5e
1 #!/usr/bin/env wish8.0
2
3 #
4 # This software is copyright Daniel O'Connor (darius@dons.net.au) 2000
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # 2. Redistributions in binary form must reproduce the above copyright
12 # notice, this list of conditions and the following disclaimer in the
13 # documentation and/or other materials provided with the distribution.
14 # 3. Neither the name Daniel O'Connor nor the names of its contributors
15 # may be used to endorse or promote products derived from this software
16 # without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY DANIEL O'CONNOR AND CONTRIBUTORS ``AS IS'' AND
19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
22 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 # SUCH DAMAGE.
29 #
30
31 proc main {} {
32 global argv0 argv state albums songs;
33
34 set state(exit) 0;
35 set state(host) "lot";
36 set state(port) "4444";
37 set state(user) "Darius";
38 set state(pass) "Fnordish";
39 set state(sortmode) "Title";
40 set state(tmpphrase) "";
41
42 gui_build;
43
44 con_mserv;
45
46 con_getalbums albums;
47 con_getsongs songs albums;
48
49 gui_updatesongs;
50 gui_updatequeue;
51
52 update_timer;
53
54 while {1} {
55 vwait state(exit);
56
57 if {$state(exit) == 1} {
58 exit;
59 }
60 }
61 }
62
63 proc quit_now {} {
64 global state;
65
66 set state(exit) 1;
67 }
68
69 proc gui_build {} {
70 # create the toplevel
71 eval destroy [winfo child .];
72 wm title . "MServ-Tk";
73 wm minsize . 600 500;
74 wm geometry . 600x500;
75
76 # Let's have a menubar
77 frame .menubar -relief raised -bd 2;
78 pack .menubar -side top -fill x;
79
80 # Add the File menu
81 menubutton .menubar.file -text "File" -menu .menubar.file.m -underline 0;
82 menu .menubar.file.m -tearoff 0;
83 .menubar.file.m add command -label " Quit" -command "quit_now" \
84 -underline 2 -accelerator "Alt-q";
85 pack .menubar.file -side left;
86
87 # Add the Rate menu
88 menubutton .menubar.rate -text "Rate" -menu .menubar.rate.m -underline 0;
89 menu .menubar.rate.m -tearoff 0;
90 .menubar.rate.m add command -label " Superb" -command "rate_song SUPERB";
91 .menubar.rate.m add command -label " Good" -command "rate_song GOOD";
92 .menubar.rate.m add command -label " Neutral" -command "rate_song NEUTRAL";
93 .menubar.rate.m add command -label " Bad" -command "rate_song BAD";
94 .menubar.rate.m add command -label " Awful" -command "rate_song AWFUL";
95
96 pack .menubar.rate -side left;
97
98 # Add the Control menu
99 menubutton .menubar.control -text "Control" -menu .menubar.control.m -underline 0;
100 menu .menubar.control.m -tearoff 0;
101 .menubar.control.m add command -label " Next" -command "control_player NEXT";
102 .menubar.control.m add command -label " Pause" -command "control_player PAUSE";
103 .menubar.control.m add command -label " Stop" -command "control_player STOP"
104 .menubar.control.m add command -label " Play" -command "control_player PLAY";
105
106 pack .menubar.control -side left;
107
108 # Add the Volume menu
109 menubutton .menubar.vol -text "Volume" -menu .menubar.vol.m -underline 0;
110 menu .menubar.vol.m -tearoff 0;
111 .menubar.vol.m add command -label " Increase" -command "set_vol +3" \
112 -accelerator "+";
113 .menubar.vol.m add command -label " Decrease" -command "set_vol -3" \
114 -accelerator "-";
115 .menubar.vol.m add separator;
116 .menubar.vol.m add command -label " 100%" -command "set_vol 100";
117 .menubar.vol.m add command -label " 90%" -command "set_vol 90";
118 .menubar.vol.m add command -label " 80%" -command "set_vol 80";
119 .menubar.vol.m add command -label " 70%" -command "set_vol 70";
120 .menubar.vol.m add command -label " 60%" -command "set_vol 60";
121 .menubar.vol.m add command -label " 50%" -command "set_vol 50";
122 .menubar.vol.m add command -label " 40%" -command "set_vol 40";
123 .menubar.vol.m add command -label " 30%" -command "set_vol 30";
124 .menubar.vol.m add command -label " 20%" -command "set_vol 20";
125 .menubar.vol.m add command -label " 10%" -command "set_vol 10";
126 .menubar.vol.m add command -label " 0%" -command "set_vol 0";
127
128 pack .menubar.vol -side left;
129
130 # Add the Sort menu
131 menubutton .menubar.sort -text "Sort" -menu .menubar.sort.m -underline 0;
132 menu .menubar.sort.m -tearoff 0;
133 .menubar.sort.m add command -label " Artist" \
134 -command "global state; set state(sortmode) Artist; gui_updatesongs";
135 .menubar.sort.m add command -label " Title" \
136 -command "global state; set state(sortmode) Title; gui_updatesongs";
137 .menubar.sort.m add command -label " Album" \
138 -command "global state; set state(sortmode) Album; gui_updatesongs";
139
140 pack .menubar.sort -side left;
141
142 # Add the Help menu
143 menubutton .menubar.help -text "Help" -menu .menubar.help.m -underline 0;
144 menu .menubar.help.m -tearoff 0;
145 .menubar.help.m add command -label " About" -command "about_box" \
146 -underline 2;
147 pack .menubar.help -side right;
148
149 # Top frame holding tracklist and trackinfo frames
150 frame .top -relief raised -bd 1;
151 pack .top -fill both -expand 1;
152
153 # Tracklist
154 frame .top.tlist -relief raised -bd 1;
155 pack .top.tlist -side left -fill both -expand 1;
156 label .top.tlist.label -text "Track List";
157 pack .top.tlist.label -side top -expand 0;
158 listbox .top.tlist.list -relief raised -borderwidth 2 \
159 -yscrollcommand ".top.tlist.scr set";
160 pack .top.tlist.list -side left -fill both -expand 1;
161 scrollbar .top.tlist.scr -command ".top.tlist.list yview";
162 pack .top.tlist.scr -side right -fill y;
163 bind .top.tlist.list <Double-Button-1> {
164 queue_song [.top.tlist.list curselection];
165 }
166
167 # Trackinfo
168 frame .top.tinfo -relief raised -bd 1;
169 pack .top.tinfo -side right -fill both -expand 0;
170 label .top.tinfo.label -text "Track Info";
171 pack .top.tinfo.label -side top -expand 0;
172 frame .top.tinfo.sub -relief raised -bd 1;
173 pack .top.tinfo.sub -side right -fill both -expand 0;
174 label .top.tinfo.sub.author -text "Author:";
175 pack .top.tinfo.sub.author -side top -expand 0;
176 label .top.tinfo.sub.title -text "Title:";
177 pack .top.tinfo.sub.title -side top -expand 0;
178 label .top.tinfo.sub.length -text "Length:";
179 pack .top.tinfo.sub.length -side top -expand 0;
180 label .top.tinfo.sub.time -text "Time:";
181 pack .top.tinfo.sub.time -side top -expand 0;
182 label .top.tinfo.sub.album -text "Album:";
183 pack .top.tinfo.sub.album -side top -expand 0;
184 label .top.tinfo.sub.trackno -text "Misc:";
185 pack .top.tinfo.sub.trackno -side top -expand 0;
186 label .top.tinfo.sub.vol -text "Volume:";
187 pack .top.tinfo.sub.vol -side top -expand 0;
188
189 # Queue (and the frame holding it)
190 frame .bot -relief raised -bd 1;
191 pack .bot -fill both -expand 1;
192 label .bot.qlabel -text "Queue";
193 pack .bot.qlabel -side top -expand 0;
194 frame .bot.queue;
195 pack .bot.queue -fill both -expand 1;
196 listbox .bot.queue.list -relief raised -borderwidth 2 \
197 -yscrollcommand ".bot.queue.scr set";
198 pack .bot.queue.list -side left -fill both -expand 1;
199 scrollbar .bot.queue.scr -command ".bot.queue.list yview";
200 pack .bot.queue.scr -side right -fill y;
201 bind .bot.queue.list <Double-Button-1> {
202 gui_delqueue [.bot.queue.list curselection];
203 }
204
205 bind . <Destroy> {quit_now};
206 bind all <Alt-q> {quit_now};
207
208 bind all <KP_Add> {set_vol +3};
209 bind all <KP_Subtract> {set_vol -3};
210 bind all <plus> {set_vol +3};
211 bind all <equal> {set_vol +3};
212 bind all <minus> {set_vol -3};
213 bind all <F1> {set_vol 10};
214 bind all <F2> {set_vol 20};
215 bind all <F3> {set_vol 30};
216 bind all <F4> {set_vol 40};
217 bind all <F5> {set_vol 50};
218 bind all <F6> {set_vol 60};
219 bind all <F7> {set_vol 70};
220 bind all <F8> {set_vol 80};
221 bind all <F9> {set_vol 90};
222 bind all <F10> {set_vol 100};
223
224 bind all <Pause> {control_player PAUSE};
225 bind all <End> {control_player NEXT};
226 bind all <Delete> {control_player STOP};
227 bind all <Home> {control_player PLAY};
228
229 update;
230 }
231
232 proc queue_song {id} {
233 global songs state;
234
235 set tmp [lindex [lindex [array get songs *:listid:$id] 1] 0];
236 log "%s" "Queue - '$songs($tmp:name)' by '$songs($tmp:author)' ($tmp)";
237
238 n_write "QUEUE [split $tmp {:}]";
239 n_getrtn rtn;
240
241 if {$rtn(code) != 247} {
242 if {$rtn(code) == 510} {
243 msg_box "Queue" "You can't have the same\nsong in the queue twice!";
244 } else {
245 log "Failed to queue track ($rtn(code) $rtn(data))";
246 }
247 }
248 }
249
250 proc msg_box {title msg} {
251 global state;
252
253 catch {destroy .msg};
254
255 toplevel .msg -class Dialog;
256 wm title .msg $title;
257 wm iconname .msg $title;
258
259 # text region
260 frame .msg.frame;
261 pack .msg.frame -side top -fill both -expand yes;
262 text .msg.frame.text -font fixed -height 10 -width 40 -yscroll ".msg.frame.scroll set" \
263 -wrap none;
264 scrollbar .msg.frame.scroll -command ".msg.frame.text yview";
265 pack .msg.frame.text -side left -expand y -fill both;
266 pack .msg.frame.scroll -side right -fill y;
267
268 # close button
269 button .msg.close -text "Close" -command "destroy .msg";
270 pack .msg.close -side bottom -fill x;
271
272 # read text into the text widget
273 .msg.frame.text insert end $msg;
274 }
275
276 proc about_box {} {
277 global state;
278
279 catch {destroy .about};
280
281 toplevel .about -class Dialog;
282 wm title .about "About...";
283 wm iconname .about "About";
284
285 # text region
286 frame .about.frame;
287 pack .about.frame -side top -fill both -expand yes;
288 text .about.frame.text -font fixed -height 10 -width 40 -yscroll ".about.frame.scroll set" \
289 -wrap none;
290 scrollbar .about.frame.scroll -command ".about.frame.text yview";
291 pack .about.frame.text -side left -expand y;
292 pack .about.frame.scroll -side right -fill y;
293
294 # close button
295 button .about.close -text "Close" -command "destroy .about";
296 pack .about.close -side bottom -fill x;
297
298 # read text into the text widget
299 .about.frame.text insert end "Mserv Client\n";
300 .about.frame.text insert end "Copyright Daniel O'Connor 2000\n";
301 .about.frame.text insert end "\n";
302 .about.frame.text insert end "http://www.dons.net.au/~darius/\n";
303 }
304
305 proc set_vol {vol} {
306 global state;
307
308 n_write "VOLUME $vol"
309 n_getrtn rtn;
310
311 if {$rtn(code) != 255} {
312 log "%s" "Couldn't set volume ($rtn(code) $rtn(data))";
313 }
314 }
315
316 proc rate_song {rate} {
317 global state;
318
319 n_write "RATE $rate";
320 n_getrtn rtn;
321
322 if {$rtn(code) != 270} {
323 log "%s" "Failed to get rate song ($rtn(code) $rtn(data))";
324 }
325
326 }
327
328 proc control_player {cmd} {
329 global state;
330
331 n_write "$cmd";
332 n_getrtn rtn;
333
334 log "%s" "Control Got $rtn(code) $rtn(data)";
335 }
336
337 proc gui_updatesongs {} {
338 global state songs;
339
340 .top.tlist.list delete 0 end;
341
342 set tmp "";
343
344 foreach tag [array names songs "*:id"] {
345 set a $songs($tag);
346 lappend tmp [list $a $songs($a:name) $songs($a:author) $songs($a:albumname)];
347 }
348
349 switch -- $state(sortmode) {
350 "Title" {
351 set idx 1;
352 }
353
354 "Artist" {
355 set idx 2;
356 }
357
358 "Album" {
359 set idx 3;
360 }
361
362 default {
363 set idx 1;
364 }
365 }
366 set tmp [lsort -dictionary -index $idx $tmp];
367
368 foreach a [array names songs *:listid:*] {
369 unset songs($a);
370 }
371
372 set i 0;
373 foreach a $tmp {
374 .top.tlist.list insert end "'[lindex $a 1]' by '[lindex $a 2]' on '[lindex $a 3]'"
375 set songs([lindex $a 0]:listid:$i) $a;
376 incr i;
377 }
378 }
379
380 proc gui_updatequeue {} {
381 global state songs queue;
382
383 if {[info exists state(queuelock)]} {
384 return;
385 }
386
387 set state(queuelock) "";
388
389 log "%s" "Updating queue";
390
391 .bot.queue.list delete 0 end;
392
393 con_getqueue queue;
394
395 foreach tag [lsort [array names queue]] {
396 .bot.queue.list insert end "'$songs($queue($tag):name)' by '$songs($queue($tag):author)' on '$songs($queue($tag):albumname)'";
397 }
398
399 unset state(queuelock);
400 }
401
402 proc gui_delqueue {id} {
403 global queue;
404
405 if {$id == ""} {
406 return;
407 }
408
409 n_write "UNQUEUE [split $queue($id) {:}]";
410 n_getrtn rtn;
411
412 if {$rtn(code) != 254} {
413 log "%s" "Failed to remove $id ($queue($id))";
414 msg_box "Queue" "Failed to dequeue the song";
415 }
416 }
417
418 proc gui_updateinfo {} {
419 global state;
420
421 n_write "VOLUME";
422 n_getrtn rtn;
423
424 if {$rtn(code) != 235} {
425 set vol "??";
426 } else {
427 set vol "[lindex [lindex $rtn(lines) 0] 0]";
428 }
429
430 n_write "INFO";
431 n_getrtn rtn;
432
433 if {$rtn(code) == 246} {
434 set data [split [lindex $rtn(lines) 0] "\t"];
435 set author [lindex $data 4];
436 set title [lindex $data 5];
437 set length [lindex $data 14];
438 set album [lindex $data 3];
439 set tnum [lindex $data 15];
440 } else {
441 set author "N/A";
442 set title "N/A";
443 set length "N/A";
444 set album "N/A";
445 set tnum "N/A";
446 if {$rtn(code) != 401} {
447 log "%s" "Failed to get track info ($rtn(code) $rtn(data))";
448 }
449 }
450
451
452
453 n_write "STATUS";
454 n_getrtn rtn;
455
456 if {$rtn(code) != 222} {
457 set left "x:xx";
458 set played "x:xx";
459 } else {
460 set played [lindex [split [lindex $rtn(lines) 0] "\t"] 8];
461
462 # scan $played "%d:%d" played_m played_s;
463 # set played [expr ($played_m * 60) + $played_s];
464 # scan $length "%d:%f" len_m len_s;
465 # set len [expr ($len_m * 60) + $len_s];
466
467 # set left [expr $len - $played];
468 # set left_m [expr int($left / 60)];
469 # set left [format "%02d:%02d" $left_m [expr int($left - ($left_m * 60))]];
470 }
471
472 .top.tinfo.sub.author configure -text "Author: $author";
473 .top.tinfo.sub.title configure -text "Title: $title";
474 .top.tinfo.sub.length configure -text "Length: $length";
475 .top.tinfo.sub.time configure -text "Time: $played";
476 .top.tinfo.sub.album configure -text "Album: $album";
477 .top.tinfo.sub.trackno configure -text "Misc: $tnum";
478 .top.tinfo.sub.vol configure -text "Volume: $vol";
479 }
480
481 proc con_getqueue {queuevar} {
482 upvar $queuevar queue;
483
484 global state;
485
486 catch {unset queue};
487
488 n_write "QUEUE"
489 n_getrtn rtn;
490
491 if {$rtn(code) == 225} {
492 set i 0;
493 foreach line $rtn(lines) {
494 set foo [split $line \011];
495 set id "[lindex $foo 1]:[lindex $foo 2]";
496
497 set queue($i) $id;
498 incr i;
499 }
500 } elseif {$rtn(code) == 404} {
501 log "%s" "Queue empty";
502 } else {
503 log "%s" "Failed to get queue ($rtn(code) $rtn(data))";
504 }
505 }
506
507 proc con_getsongs {songsvar albumsvar} {
508 upvar $songsvar songs;
509 upvar $albumsvar albums;
510
511 global state;
512
513 catch { unset songs };
514
515 foreach i [array names albums "*:"] {
516 n_write "TRACKS $albums($i)";
517 n_getrtn rtn;
518 if {$rtn(code) != "228"} {
519 error "Got bogus response to track request ($rtn(code) $rtn(data))";
520 }
521
522 foreach trk $rtn(lines) {
523 set foo [split $trk \011];
524 if {[llength $foo] != 6} {
525 continue;
526 }
527
528 set albid [lindex $foo 0];
529 set num [lindex $foo 1]
530 set songs($albid:$num:id) "$albid:$num";
531 set songs($albid:$num:author) [lindex $foo 2];
532 set songs($albid:$num:name) [lindex $foo 3];
533 set songs($albid:$num:rating) [lindex $foo 4];
534 set songs($albid:$num:length) [lindex $foo 5];
535 set songs($albid:$num:albumname) $albums($albid:name);
536 }
537 }
538
539 }
540
541 proc con_getalbums {albumsvar} {
542 upvar $albumsvar albums;
543
544 global state;
545
546 catch {unset albums};
547
548 n_write "ALBUMS";
549 n_getrtn rtn;
550 if {$rtn(code) != 227} {
551 error "Server gave bogus response to album request ($rtn(code) $rtn(data))";
552 }
553
554 foreach alb $rtn(lines) {
555 set foo [split $alb \011];
556 set id [lindex $foo 0];
557 set albums($id:) [lindex $foo 0];
558 set albums($id:author) [lindex $foo 1];
559 set albums($id:name) [lindex $foo 2];
560
561 # log "%s" "Album $i, ID $albums($i:id) called $albums($i:name) by $albums($i:author)";
562 }
563 }
564
565 proc update_timer {} {
566
567 gui_updateinfo;
568
569 after 900 update_timer;
570 }
571
572 proc con_mserv {} {
573 global state;
574
575 # Close old FD
576 catch {close state(serv_fd)};
577 catch {unset state(serv_fd)};
578
579 catch {fileevent $state(serv_fd) readable ""};
580 set state(serv_fd) [ socket $state(host) $state(port) ];
581 set state(pushbuf) "";
582 fileevent $state(serv_fd) readable n_rtinput;
583 fconfigure $state(serv_fd) -blocking 0;
584
585 # Greeting from server
586 n_getrtn rtn;
587 puts $rtn(data);
588 if {$rtn(code) != "200"} {
589 error "Server failed to send greeting";
590 }
591
592 # Login
593 n_write "USER $state(user)"
594 n_getrtn rtn;
595 if {$rtn(code) != "201"} {
596 error "Server failed to send password request";
597 }
598
599 n_write "PASS $state(pass) RTCOMPUTER";
600 n_getrtn rtn;
601 if {$rtn(code) == "507"} {
602 error "Server rejected our credentials";
603 }
604
605 if {$rtn(code) != "202"} {
606 error "Unknown response to PASS command - $rtn(code) $rtn(data)"
607 }
608
609 log "%s" "Logged in";
610 }
611
612 proc n_write {text} {
613 global state;
614
615 puts $state(serv_fd) $text;
616 # log "%s" "Wrote - $text";
617
618 flush $state(serv_fd);
619 }
620
621 proc n_rthandler {code data} {
622 global songs;
623
624 log "%s" "Got RT - $code $data";
625
626 switch -- $code {
627 600 {
628 log "%s" "User '$data' connected";
629 }
630
631 601 {
632 log "%s" "User '$data' disconnected";
633 }
634
635 618 -
636 619 -
637 622 -
638 627 -
639 623 {
640 after idle gui_updatequeue;
641 }
642 }
643 }
644
645 proc n_rtinput {} {
646 global state;
647
648 set rth "";
649
650 set line [gets $state(serv_fd)];
651 # log "%s" "Read - $line";
652
653 # Check for RT text
654 set foo [split $line "\t"];
655 if {[string index $line 0] == "="} {
656 set rth [list [string range [lindex $foo 0] 1 3] [lrange $foo 1 end]];
657 } else {
658 lappend state(tmpphrase) $line
659 if {$line == "."} {
660 set state(pushbuf) [linsert $state(pushbuf) 0 $state(tmpphrase)];
661 set state(tmpphrase) "";
662 }
663 }
664
665 if {$rth != ""} {
666 n_rthandler [lindex $rth 0] [lindex $rth 1];
667 }
668 }
669
670 proc n_getrtn {var} {
671 upvar $var rtn;
672 global state;
673
674 set gotcode 0;
675 catch {unset rtn(code)};
676 catch {unset rtn(data)};
677 catch {unset rtn(lines)}
678
679 while {[llength $state(pushbuf)] == 0} {
680 vwait state(pushbuf);
681 }
682
683 set buf [lindex $state(pushbuf) 0];
684 set state(pushbuf) [lrange $state(pushbuf) 1 end];
685
686 while {1} {
687 if {[llength $buf] == 0} {
688 break;
689 }
690
691 set line [lindex $buf 0];
692 set buf [lrange $buf 1 end];
693
694 if {[string index $line 0] == "."} {
695 break;
696 }
697
698 if {$gotcode == 0} {
699 set rtn(code) [string range $line 0 2];
700 set rtn(data) [string range $line 4 end];
701 set gotcode 1;
702 continue;
703 }
704
705 lappend rtn(lines) $line;
706 }
707
708 if {$gotcode == 0} {
709 log "%s" "Failed to parse phrase (got . before server responce)";
710 }
711 }
712
713 ##################################################################
714 # Log a message to stderr
715 #
716 proc log {format args} {
717 # Extract the calling function's name
718 set fname [lindex [info level -1] 0];
719
720 # Evaluate the supplied format string and arguments
721 if {[catch {set csm [eval format {$format} $args]} msg]} {
722 set csm "bad log message. format='$format' args='$args'";
723 }
724
725 # Emit the message
726 puts stderr "[clock format [clock seconds] -format {%y/%m/%d %H:%M:%S} -gmt yes]:$fname: $csm";
727 flush stderr;
728 }
729
730 main;