chiark / gitweb /
5062747003865c35b8a94163fd8bfb2292e8c961
[disorder] / disobedience / disobedience.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2006, 2007, 2008 Richard Kettlewell
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 /** @file disobedience/disobedience.c
19  * @brief Main Disobedience program
20  */
21
22 #include "disobedience.h"
23 #include "version.h"
24
25 #include <getopt.h>
26 #include <locale.h>
27 #include <pcre.h>
28 #include <gcrypt.h>
29
30 /* Apologies for the numerous de-consting casts, but GLib et al do not seem to
31  * have heard of const. */
32
33 /* Variables --------------------------------------------------------------- */
34
35 /** @brief Event loop */
36 GMainLoop *mainloop;
37
38 /** @brief Top-level window */
39 GtkWidget *toplevel;
40
41 /** @brief Label for progress indicator */
42 GtkWidget *report_label;
43
44 /** @brief Main tab group */
45 GtkWidget *tabs;
46
47 /** @brief Main client */
48 disorder_eclient *client;
49
50 /** @brief Log client */
51 disorder_eclient *logclient;
52
53 /** @brief Last reported state
54  *
55  * This is updated by log_state().
56  */
57 unsigned long last_state;
58
59 /** @brief True if some track is playing
60  *
61  * This ought to be removed in favour of last_state & DISORDER_PLAYING
62  */
63 int playing;
64
65 /** @brief Left channel volume */
66 int volume_l;
67
68 /** @brief Right channel volume */
69 int volume_r;
70
71 /** @brief Audio backend */
72 const struct uaudio *backend;
73
74 double goesupto = 10;                   /* volume upper bound */
75
76 /** @brief True if a NOP is in flight */
77 static int nop_in_flight;
78
79 /** @brief True if an rtp-address command is in flight */
80 static int rtp_address_in_flight;
81
82 /** @brief True if a rights lookup is in flight */
83 static int rights_lookup_in_flight;
84
85 /** @brief Current rights bitmap */
86 rights_type last_rights;
87
88 /** @brief Global tooltip group */
89 GtkTooltips *tips;
90
91 /** @brief True if RTP play is available
92  *
93  * This is a bit of a bodge...
94  */
95 int rtp_supported;
96
97 /** @brief True if RTP play is enabled */
98 int rtp_is_running;
99
100 /** @brief Server version */
101 const char *server_version;
102
103 /** @brief Parsed server version */
104 long server_version_bytes;
105
106 static void check_rtp_address(const char *event,
107                               void *eventdata,
108                               void *callbackdata);
109
110 /* Window creation --------------------------------------------------------- */
111
112 /* Note that all the client operations kicked off from here will only complete
113  * after we've entered the event loop. */
114
115 /** @brief Called when main window is deleted
116  *
117  * Terminates the program.
118  */
119 static gboolean delete_event(GtkWidget attribute((unused)) *widget,
120                              GdkEvent attribute((unused)) *event,
121                              gpointer attribute((unused)) data) {
122   D(("delete_event"));
123   exit(0);                              /* die immediately */
124 }
125
126 /** @brief Called when the current tab is switched
127  *
128  * Updates the menu settings to correspond to the new page.
129  */
130 static void tab_switched(GtkNotebook *notebook,
131                          GtkNotebookPage attribute((unused)) *page,
132                          guint page_num,
133                          gpointer attribute((unused)) user_data) {
134   GtkWidget *const tab = gtk_notebook_get_nth_page(notebook, page_num);
135   const struct tabtype *const t = g_object_get_data(G_OBJECT(tab), "type");
136   assert(t != 0);
137   if(t->selected)
138     t->selected();
139 }
140
141 /** @brief Create the report box */
142 static GtkWidget *report_box(void) {
143   GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
144
145   report_label = gtk_label_new("");
146   gtk_label_set_ellipsize(GTK_LABEL(report_label), PANGO_ELLIPSIZE_END);
147   gtk_misc_set_alignment(GTK_MISC(report_label), 0, 0);
148   gtk_container_add(GTK_CONTAINER(vbox), gtk_hseparator_new());
149   gtk_container_add(GTK_CONTAINER(vbox), report_label);
150   return vbox;
151 }
152
153 /** @brief Create and populate the main tab group */
154 static GtkWidget *notebook(void) {
155   tabs = gtk_notebook_new();
156   /* The current tab is _NORMAL, the rest are _ACTIVE, which is bizarre but
157    * produces not too dreadful appearance */
158   gtk_widget_set_style(tabs, tool_style);
159   g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0);
160   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue_widget(),
161                            gtk_label_new("Queue"));
162   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(),
163                            gtk_label_new("Recent"));
164   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), choose_widget(),
165                            gtk_label_new("Choose"));
166   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), added_widget(),
167                            gtk_label_new("Added"));
168   return tabs;
169 }
170
171 /** @brief Create and populate the main window */
172 static void make_toplevel_window(void) {
173   GtkWidget *const vbox = gtk_vbox_new(FALSE, 1);
174   GtkWidget *const rb = report_box();
175
176   D(("top_window"));
177   toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL);
178   /* default size is too small */
179   gtk_window_set_default_size(GTK_WINDOW(toplevel), 640, 480);
180   /* terminate on close */
181   g_signal_connect(G_OBJECT(toplevel), "delete_event",
182                    G_CALLBACK(delete_event), NULL);
183   /* lay out the window */
184   gtk_window_set_title(GTK_WINDOW(toplevel), "Disobedience");
185   gtk_container_add(GTK_CONTAINER(toplevel), vbox);
186   /* lay out the vbox */
187   gtk_box_pack_start(GTK_BOX(vbox),
188                      menubar(toplevel),
189                      FALSE,             /* expand */
190                      FALSE,             /* fill */
191                      0);
192   gtk_box_pack_start(GTK_BOX(vbox),
193                      control_widget(),
194                      FALSE,             /* expand */
195                      FALSE,             /* fill */
196                      0);
197   gtk_container_add(GTK_CONTAINER(vbox), notebook());
198   gtk_box_pack_end(GTK_BOX(vbox),
199                    rb,
200                    FALSE,             /* expand */
201                    FALSE,             /* fill */
202                    0);
203   gtk_widget_set_style(toplevel, tool_style);
204 }
205
206 static void userinfo_rights_completed(void attribute((unused)) *v,
207                                       const char *err,
208                                       const char *value) {
209   rights_type r;
210
211   if(err) {
212     popup_protocol_error(0, err);
213     r = 0;
214   } else {
215     if(parse_rights(value, &r, 0))
216       r = 0;
217   }
218   /* If rights have changed, signal everything that cares */
219   if(r != last_rights) {
220     last_rights = r;
221     ++suppress_actions;
222     event_raise("rights-changed", 0);
223     --suppress_actions;
224   }
225   rights_lookup_in_flight = 0;
226 }
227
228 static void check_rights(void) {
229   if(!rights_lookup_in_flight) {
230     rights_lookup_in_flight = 1;
231     disorder_eclient_userinfo(client,
232                               userinfo_rights_completed,
233                               config->username, "rights",
234                               0);
235   }
236 }
237
238 /** @brief Called occasionally */
239 static gboolean periodic_slow(gpointer attribute((unused)) data) {
240   D(("periodic_slow"));
241   /* Expire cached data */
242   cache_expire();
243   /* Update everything to be sure that the connection to the server hasn't
244    * mysteriously gone stale on us. */
245   all_update();
246   /* Recheck RTP status too */
247   check_rtp_address(0, 0, 0);
248   return TRUE;                          /* don't remove me */
249 }
250
251 /** @brief Called frequently */
252 static gboolean periodic_fast(gpointer attribute((unused)) data) {
253 #if 0                                   /* debugging hack */
254   static struct timeval last;
255   struct timeval now;
256   double delta;
257
258   xgettimeofday(&now, 0);
259   if(last.tv_sec) {
260     delta = (now.tv_sec + now.tv_sec / 1.0E6) 
261       - (last.tv_sec + last.tv_sec / 1.0E6);
262     if(delta >= 1.0625)
263       fprintf(stderr, "%f: %fs between 1s heartbeats\n", 
264               now.tv_sec + now.tv_sec / 1.0E6,
265               delta);
266   }
267   last = now;
268 #endif
269   if(rtp_supported && backend && backend->get_volume) {
270     int nl, nr;
271     backend->get_volume(&nl, &nr);
272     if(nl != volume_l || nr != volume_r) {
273       volume_l = nl;
274       volume_r = nr;
275       event_raise("volume-changed", 0);
276     }
277   }
278   /* Periodically check what our rights are */
279   int recheck_rights = 1;
280   if(server_version_bytes >= 0x04010000)
281     /* Server versions after 4.1 will send updates */
282     recheck_rights = 0;
283   if((server_version_bytes & 0xFF) == 0x01)
284     /* Development servers might do regardless of their version number */
285     recheck_rights = 0;
286   if(recheck_rights)
287     check_rights();
288   return TRUE;
289 }
290
291 /** @brief Called when a NOP completes */
292 static void nop_completed(void attribute((unused)) *v,
293                           const char attribute((unused)) *err) {
294   /* TODO report the error somewhere */
295   nop_in_flight = 0;
296 }
297
298 /** @brief Called from time to time to arrange for a NOP to be sent
299  *
300  * At most one NOP remains in flight at any moment.  If the client is not
301  * currently connected then no NOP is sent.
302  */
303 static gboolean maybe_send_nop(gpointer attribute((unused)) data) {
304   if(!nop_in_flight && (disorder_eclient_state(client) & DISORDER_CONNECTED)) {
305     nop_in_flight = 1;
306     disorder_eclient_nop(client, nop_completed, 0);
307   }
308   if(rtp_supported) {
309     const int rtp_was_running = rtp_is_running;
310     rtp_is_running = rtp_running();
311     if(rtp_was_running != rtp_is_running)
312       event_raise("rtp-changed", 0);
313   }
314   return TRUE;                          /* keep call me please */
315 }
316
317 /** @brief Called when a rtp-address command succeeds */
318 static void got_rtp_address(void attribute((unused)) *v,
319                             const char *err,
320                             int attribute((unused)) nvec,
321                             char attribute((unused)) **vec) {
322   const int rtp_was_supported = rtp_supported;
323   const int rtp_was_running = rtp_is_running;
324
325   ++suppress_actions;
326   rtp_address_in_flight = 0;
327   if(err) {
328     /* An error just means that we're not using network play */
329     rtp_supported = 0;
330     rtp_is_running = 0;
331   } else {
332     rtp_supported = 1;
333     rtp_is_running = rtp_running();
334   }
335   /*fprintf(stderr, "rtp supported->%d, running->%d\n",
336           rtp_supported, rtp_is_running);*/
337   if(rtp_supported != rtp_was_supported
338      || rtp_is_running != rtp_was_running)
339     event_raise("rtp-changed", 0);
340   --suppress_actions;
341 }
342
343 /** @brief Called to check whether RTP play is available */
344 static void check_rtp_address(const char attribute((unused)) *event,
345                               void attribute((unused)) *eventdata,
346                               void attribute((unused)) *callbackdata) {
347   if(!rtp_address_in_flight) {
348     //fprintf(stderr, "checking rtp\n");
349     disorder_eclient_rtp_address(client, got_rtp_address, NULL);
350   }
351 }
352
353 /* main -------------------------------------------------------------------- */
354
355 static const struct option options[] = {
356   { "help", no_argument, 0, 'h' },
357   { "version", no_argument, 0, 'V' },
358   { "config", required_argument, 0, 'c' },
359   { "tufnel", no_argument, 0, 't' },
360   { "debug", no_argument, 0, 'd' },
361   { 0, 0, 0, 0 }
362 };
363
364 /* display usage message and terminate */
365 static void help(void) {
366   xprintf("Disobedience - GUI client for DisOrder\n"
367           "\n"
368           "Usage:\n"
369           "  disobedience [OPTIONS]\n"
370           "Options:\n"
371           "  --help, -h              Display usage message\n"
372           "  --version, -V           Display version number\n"
373           "  --config PATH, -c PATH  Set configuration file\n"
374           "  --debug, -d             Turn on debugging\n"
375           "\n"
376           "Also GTK+ options will work.\n");
377   xfclose(stdout);
378   exit(0);
379 }
380
381 static void version_completed(void attribute((unused)) *v,
382                               const char attribute((unused)) *err,
383                               const char *ver) {
384   long major, minor, patch, dev;
385
386   if(!ver) {
387     server_version = 0;
388     server_version_bytes = 0;
389     return;
390   }
391   server_version = ver;
392   server_version_bytes = 0;
393   major = strtol(ver, (char **)&ver, 10);
394   if(*ver != '.')
395     return;
396   ++ver;
397   minor = strtol(ver, (char **)&ver, 10);
398   if(*ver == '.') {
399     ++ver;
400     patch = strtol(ver, (char **)&ver, 10);
401   } else
402     patch = 0;
403   if(*ver) {
404     if(*ver == '+') {
405       dev = 1;
406       ++ver;
407     }
408     if(*ver)
409       dev = 2;
410   } else
411     dev = 0;
412   server_version_bytes = (major << 24) + (minor << 16) + (patch << 8) + dev;
413 }
414
415 void logged_in(void) {
416   /* reset the clients */
417   disorder_eclient_close(client);
418   disorder_eclient_close(logclient);
419   rtp_supported = 0;
420   event_raise("logged-in", 0);
421   /* Force the periodic checks */
422   periodic_slow(0);
423   periodic_fast(0);
424   /* Recheck server version */
425   disorder_eclient_version(client, version_completed, 0);
426   disorder_eclient_enable_connect(client);
427   disorder_eclient_enable_connect(logclient);
428 }
429
430 int main(int argc, char **argv) {
431   int n;
432   gboolean gtkok;
433
434   mem_init();
435   /* garbage-collect PCRE's memory */
436   pcre_malloc = xmalloc;
437   pcre_free = xfree;
438   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
439   gtkok = gtk_init_check(&argc, &argv);
440   while((n = getopt_long(argc, argv, "hVc:dtHC", options, 0)) >= 0) {
441     switch(n) {
442     case 'h': help();
443     case 'V': version("disobedience");
444     case 'c': configfile = optarg; break;
445     case 'd': debugging = 1; break;
446     case 't': goesupto = 11; break;
447     default: fatal(0, "invalid option");
448     }
449   }
450   if(!gtkok)
451     fatal(0, "failed to initialize GTK+");
452   /* gcrypt initialization */
453   if(!gcry_check_version(NULL))
454     disorder_fatal(0, "gcry_check_version failed");
455   gcry_control(GCRYCTL_INIT_SECMEM, 0);
456   gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
457   signal(SIGPIPE, SIG_IGN);
458   init_styles();
459   load_settings();
460   /* create the event loop */
461   D(("create main loop"));
462   mainloop = g_main_loop_new(0, 0);
463   if(config_read(0, NULL)) fatal(0, "cannot read configuration");
464   /* we'll need mixer support */
465   backend = uaudio_apis[0];
466   if(backend->configure)
467     backend->configure();
468   if(backend->open_mixer)
469     backend->open_mixer();
470   /* create the clients */
471   if(!(client = gtkclient())
472      || !(logclient = gtkclient()))
473     return 1;                           /* already reported an error */
474   /* periodic operations (e.g. expiring the cache, checking local volume) */
475   g_timeout_add(600000/*milliseconds*/, periodic_slow, 0);
476   g_timeout_add(1000/*milliseconds*/, periodic_fast, 0);
477   /* global tooltips */
478   tips = gtk_tooltips_new();
479   make_toplevel_window();
480   /* reset styles now everything has its name */
481   gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default()));
482   gtk_widget_show_all(toplevel);
483   /* issue a NOP every so often */
484   g_timeout_add_full(G_PRIORITY_LOW,
485                      2000/*interval, ms*/,
486                      maybe_send_nop,
487                      0/*data*/,
488                      0/*notify*/);
489   /* Start monitoring the log */
490   disorder_eclient_log(logclient, &log_callbacks, 0);
491   /* Initiate all the checks */
492   periodic_fast(0);
493   disorder_eclient_version(client, version_completed, 0);
494   event_register("log-connected", check_rtp_address, 0);
495   suppress_actions = 0;
496   /* If no password is set yet pop up a login box */
497   if(!config->password)
498     login_box();
499   D(("enter main loop"));
500   g_main_loop_run(mainloop);
501   return 0;
502 }
503
504 /*
505 Local Variables:
506 c-basic-offset:2
507 comment-column:40
508 fill-column:79
509 indent-tabs-mode:nil
510 End:
511 */