chiark / gitweb /
edffbb5d9e5051d8ef4ec3457a2f9df798a519b4
[disorder] / disobedience / disobedience.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2006-2009 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   event_raise("periodic-slow", 0);
247   /* Recheck RTP status too */
248   check_rtp_address(0, 0, 0);
249   return TRUE;                          /* don't remove me */
250 }
251
252 /** @brief Called frequently */
253 static gboolean periodic_fast(gpointer attribute((unused)) data) {
254 #if 0                                   /* debugging hack */
255   static struct timeval last;
256   struct timeval now;
257   double delta;
258
259   xgettimeofday(&now, 0);
260   if(last.tv_sec) {
261     delta = (now.tv_sec + now.tv_sec / 1.0E6) 
262       - (last.tv_sec + last.tv_sec / 1.0E6);
263     if(delta >= 1.0625)
264       fprintf(stderr, "%f: %fs between 1s heartbeats\n", 
265               now.tv_sec + now.tv_sec / 1.0E6,
266               delta);
267   }
268   last = now;
269 #endif
270   if(rtp_supported && backend && backend->get_volume) {
271     int nl, nr;
272     backend->get_volume(&nl, &nr);
273     if(nl != volume_l || nr != volume_r) {
274       volume_l = nl;
275       volume_r = nr;
276       event_raise("volume-changed", 0);
277     }
278   }
279   /* Periodically check what our rights are */
280   int recheck_rights = 1;
281   if(server_version_bytes >= 0x04010000)
282     /* Server versions after 4.1 will send updates */
283     recheck_rights = 0;
284   if((server_version_bytes & 0xFF) == 0x01)
285     /* Development servers might do regardless of their version number */
286     recheck_rights = 0;
287   if(recheck_rights)
288     check_rights();
289   event_raise("periodic-fast", 0);
290   return TRUE;
291 }
292
293 /** @brief Called when a NOP completes */
294 static void nop_completed(void attribute((unused)) *v,
295                           const char attribute((unused)) *err) {
296   /* TODO report the error somewhere */
297   nop_in_flight = 0;
298 }
299
300 /** @brief Called from time to time to arrange for a NOP to be sent
301  *
302  * At most one NOP remains in flight at any moment.  If the client is not
303  * currently connected then no NOP is sent.
304  */
305 static gboolean maybe_send_nop(gpointer attribute((unused)) data) {
306   if(!nop_in_flight && (disorder_eclient_state(client) & DISORDER_CONNECTED)) {
307     nop_in_flight = 1;
308     disorder_eclient_nop(client, nop_completed, 0);
309   }
310   if(rtp_supported) {
311     const int rtp_was_running = rtp_is_running;
312     rtp_is_running = rtp_running();
313     if(rtp_was_running != rtp_is_running)
314       event_raise("rtp-changed", 0);
315   }
316   return TRUE;                          /* keep call me please */
317 }
318
319 /** @brief Called when a rtp-address command succeeds */
320 static void got_rtp_address(void attribute((unused)) *v,
321                             const char *err,
322                             int attribute((unused)) nvec,
323                             char attribute((unused)) **vec) {
324   const int rtp_was_supported = rtp_supported;
325   const int rtp_was_running = rtp_is_running;
326
327   ++suppress_actions;
328   rtp_address_in_flight = 0;
329   if(err) {
330     /* An error just means that we're not using network play */
331     rtp_supported = 0;
332     rtp_is_running = 0;
333   } else {
334     rtp_supported = 1;
335     rtp_is_running = rtp_running();
336   }
337   /*fprintf(stderr, "rtp supported->%d, running->%d\n",
338           rtp_supported, rtp_is_running);*/
339   if(rtp_supported != rtp_was_supported
340      || rtp_is_running != rtp_was_running)
341     event_raise("rtp-changed", 0);
342   --suppress_actions;
343 }
344
345 /** @brief Called to check whether RTP play is available */
346 static void check_rtp_address(const char attribute((unused)) *event,
347                               void attribute((unused)) *eventdata,
348                               void attribute((unused)) *callbackdata) {
349   if(!rtp_address_in_flight) {
350     //fprintf(stderr, "checking rtp\n");
351     disorder_eclient_rtp_address(client, got_rtp_address, NULL);
352   }
353 }
354
355 /* main -------------------------------------------------------------------- */
356
357 static const struct option options[] = {
358   { "help", no_argument, 0, 'h' },
359   { "version", no_argument, 0, 'V' },
360   { "config", required_argument, 0, 'c' },
361   { "tufnel", no_argument, 0, 't' },
362   { "debug", no_argument, 0, 'd' },
363   { 0, 0, 0, 0 }
364 };
365
366 /* display usage message and terminate */
367 static void help(void) {
368   xprintf("Disobedience - GUI client for DisOrder\n"
369           "\n"
370           "Usage:\n"
371           "  disobedience [OPTIONS]\n"
372           "Options:\n"
373           "  --help, -h              Display usage message\n"
374           "  --version, -V           Display version number\n"
375           "  --config PATH, -c PATH  Set configuration file\n"
376           "  --debug, -d             Turn on debugging\n"
377           "\n"
378           "Also GTK+ options will work.\n");
379   xfclose(stdout);
380   exit(0);
381 }
382
383 static void version_completed(void attribute((unused)) *v,
384                               const char attribute((unused)) *err,
385                               const char *ver) {
386   long major, minor, patch, dev;
387
388   if(!ver) {
389     server_version = 0;
390     server_version_bytes = 0;
391     return;
392   }
393   server_version = ver;
394   server_version_bytes = 0;
395   major = strtol(ver, (char **)&ver, 10);
396   if(*ver != '.')
397     return;
398   ++ver;
399   minor = strtol(ver, (char **)&ver, 10);
400   if(*ver == '.') {
401     ++ver;
402     patch = strtol(ver, (char **)&ver, 10);
403   } else
404     patch = 0;
405   if(*ver) {
406     if(*ver == '+') {
407       dev = 1;
408       ++ver;
409     }
410     if(*ver)
411       dev = 2;
412   } else
413     dev = 0;
414   server_version_bytes = (major << 24) + (minor << 16) + (patch << 8) + dev;
415 }
416
417 void logged_in(void) {
418   /* reset the clients */
419   disorder_eclient_close(client);
420   disorder_eclient_close(logclient);
421   rtp_supported = 0;
422   event_raise("logged-in", 0);
423   /* Force the periodic checks */
424   periodic_slow(0);
425   periodic_fast(0);
426   /* Recheck server version */
427   disorder_eclient_version(client, version_completed, 0);
428   disorder_eclient_enable_connect(client);
429   disorder_eclient_enable_connect(logclient);
430 }
431
432 int main(int argc, char **argv) {
433   int n;
434   gboolean gtkok;
435
436   mem_init();
437   /* garbage-collect PCRE's memory */
438   pcre_malloc = xmalloc;
439   pcre_free = xfree;
440   if(!setlocale(LC_CTYPE, "")) disorder_fatal(errno, "error calling setlocale");
441   gtkok = gtk_init_check(&argc, &argv);
442   while((n = getopt_long(argc, argv, "hVc:dtHC", options, 0)) >= 0) {
443     switch(n) {
444     case 'h': help();
445     case 'V': version("disobedience");
446     case 'c': configfile = optarg; break;
447     case 'd': debugging = 1; break;
448     case 't': goesupto = 11; break;
449     default: disorder_fatal(0, "invalid option");
450     }
451   }
452   if(!gtkok)
453     disorder_fatal(0, "failed to initialize GTK+");
454   /* gcrypt initialization */
455   if(!gcry_check_version(NULL))
456     disorder_fatal(0, "gcry_check_version failed");
457   gcry_control(GCRYCTL_INIT_SECMEM, 0);
458   gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
459   signal(SIGPIPE, SIG_IGN);
460   init_styles();
461   load_settings();
462   /* create the event loop */
463   D(("create main loop"));
464   mainloop = g_main_loop_new(0, 0);
465   if(config_read(0, NULL)) disorder_fatal(0, "cannot read configuration");
466   /* we'll need mixer support */
467   backend = uaudio_apis[0];
468   if(backend->configure)
469     backend->configure();
470   if(backend->open_mixer)
471     backend->open_mixer();
472   /* create the clients */
473   if(!(client = gtkclient())
474      || !(logclient = gtkclient()))
475     return 1;                           /* already reported an error */
476   /* periodic operations (e.g. expiring the cache, checking local volume) */
477   g_timeout_add(600000/*milliseconds*/, periodic_slow, 0);
478   g_timeout_add(1000/*milliseconds*/, periodic_fast, 0);
479   /* global tooltips */
480   tips = gtk_tooltips_new();
481   make_toplevel_window();
482   /* reset styles now everything has its name */
483   gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default()));
484   gtk_widget_show_all(toplevel);
485   /* issue a NOP every so often */
486   g_timeout_add_full(G_PRIORITY_LOW,
487                      2000/*interval, ms*/,
488                      maybe_send_nop,
489                      0/*data*/,
490                      0/*notify*/);
491   /* Start monitoring the log */
492   disorder_eclient_log(logclient, &log_callbacks, 0);
493   /* Initiate all the checks */
494   periodic_fast(0);
495   disorder_eclient_version(client, version_completed, 0);
496   event_register("log-connected", check_rtp_address, 0);
497   suppress_actions = 0;
498 #if PLAYLISTS
499   playlists_init();
500 #endif
501   /* If no password is set yet pop up a login box */
502   if(!config->password)
503     login_box();
504   D(("enter main loop"));
505   g_main_loop_run(mainloop);
506   return 0;
507 }
508
509 /*
510 Local Variables:
511 c-basic-offset:2
512 comment-column:40
513 fill-column:79
514 indent-tabs-mode:nil
515 End:
516 */