1 /*
2 * $Id$
3 *
4 * (c) 2004 by Kalle Wallin <kaw@linux.se>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 *
19 */
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <unistd.h>
24 #include <time.h>
25 #include <string.h>
26 #include <glib.h>
28 #include "config.h"
29 #include "ncmpc.h"
30 #include "support.h"
31 #include "mpdclient.h"
32 #include "options.h"
34 #undef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_ADD /* broken with song id's */
35 #define ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_DELETE
36 #define ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_MOVE
37 #undef ENABLE_SONG_ID
39 #define MPD_ERROR(c) (c==NULL || c->connection==NULL || c->connection->error)
42 /* Error callbacks */
43 static gint
44 error_cb(mpdclient_t *c, gint error, gchar *msg)
45 {
46 GList *list = c->error_callbacks;
48 if( list==NULL )
49 fprintf(stderr, "error [%d]: %s\n", error, msg);
51 while(list)
52 {
53 mpdc_error_cb_t cb = list->data;
54 if( cb )
55 cb(c, error, msg);
56 list=list->next;
57 }
58 mpd_clearError(c->connection);
59 return error;
60 }
62 #ifdef DEBUG
63 #include "strfsong.h"
65 static gchar *
66 get_song_name(mpd_Song *song)
67 {
68 static gchar name[256];
70 strfsong(name, 256, "[%artist% - ]%title%|%file%", song);
71 return name;
72 }
74 #endif
76 /****************************************************************************/
77 /*** mpdclient functions ****************************************************/
78 /****************************************************************************/
80 gint
81 mpdclient_finish_command(mpdclient_t *c)
82 {
83 mpd_finishCommand(c->connection);
85 if( c->connection->error )
86 {
87 gchar *msg = locale_to_utf8(c->connection->errorStr);
88 gint retval = c->connection->error;
90 error_cb(c, c->connection->error, msg);
91 g_free(msg);
92 return retval;
93 }
95 return 0;
96 }
98 mpdclient_t *
99 mpdclient_new(void)
100 {
101 mpdclient_t *c;
103 c = g_malloc0(sizeof(mpdclient_t));
105 return c;
106 }
108 mpdclient_t *
109 mpdclient_free(mpdclient_t *c)
110 {
111 mpdclient_disconnect(c);
112 g_list_free(c->error_callbacks);
113 g_list_free(c->playlist_callbacks);
114 g_list_free(c->browse_callbacks);
115 g_free(c);
117 return NULL;
118 }
120 gint
121 mpdclient_disconnect(mpdclient_t *c)
122 {
123 D("mpdclient_disconnect()...\n");
124 if( c->connection )
125 mpd_closeConnection(c->connection);
126 c->connection = NULL;
128 if( c->status )
129 mpd_freeStatus(c->status);
130 c->status = NULL;
132 if( c->playlist.list )
133 mpdclient_playlist_free(&c->playlist);
135 if( c->song )
136 c->song = NULL;
138 return 0;
139 }
141 gint
142 mpdclient_connect(mpdclient_t *c,
143 gchar *host,
144 gint port,
145 gfloat timeout,
146 gchar *password)
147 {
148 gint retval = 0;
150 /* close any open connection */
151 if( c->connection )
152 mpdclient_disconnect(c);
154 /* connect to MPD */
155 D("mpdclient_connect(%s, %d)...\n", host, port);
156 c->connection = mpd_newConnection(host, port, timeout);
157 if( c->connection->error )
158 return error_cb(c, c->connection->error, c->connection->errorStr);
160 /* send password */
161 if( password )
162 {
163 mpd_sendPasswordCommand(c->connection, password);
164 retval = mpdclient_finish_command(c);
165 }
167 return retval;
168 }
170 gint
171 mpdclient_update(mpdclient_t *c)
172 {
173 gint retval = 0;
175 if( MPD_ERROR(c) )
176 return -1;
178 /* free the old status */
179 if( c->status )
180 mpd_freeStatus(c->status);
182 /* retreive new status */
183 mpd_sendStatusCommand(c->connection);
184 c->status = mpd_getStatus(c->connection);
185 if( (retval=mpdclient_finish_command(c)) )
186 return retval;
187 #ifdef DEBUG
188 if( c->status->error )
189 D("status> %s\n", c->status->error);
190 #endif
192 /* check if the playlist needs an update */
193 if( c->playlist.id != c->status->playlist )
194 {
195 if( c->playlist.list )
196 retval = mpdclient_playlist_update_changes(c);
197 else
198 retval = mpdclient_playlist_update(c);
199 }
201 /* update the current song */
202 if( !c->song || c->status->songid != c->song->id )
203 {
204 c->song = playlist_get_song(c, c->status->song);
205 }
207 c->need_update = FALSE;
209 return retval;
210 }
213 /****************************************************************************/
214 /*** MPD Commands **********************************************************/
215 /****************************************************************************/
217 gint
218 mpdclient_cmd_play(mpdclient_t *c, gint index)
219 {
220 #ifdef ENABLE_SONG_ID
221 mpd_Song *song = playlist_get_song(c, index);
223 if( song )
224 mpd_sendPlayIdCommand(c->connection, song->id);
225 else
226 mpd_sendPlayIdCommand(c->connection, MPD_PLAY_AT_BEGINNING);
227 #else
228 mpd_sendPlayCommand(c->connection, index);
229 #endif
230 c->need_update = TRUE;
231 return mpdclient_finish_command(c);
232 }
234 gint
235 mpdclient_cmd_pause(mpdclient_t *c, gint value)
236 {
237 mpd_sendPauseCommand(c->connection, value);
238 return mpdclient_finish_command(c);
239 }
241 gint
242 mpdclient_cmd_stop(mpdclient_t *c)
243 {
244 mpd_sendStopCommand(c->connection);
245 return mpdclient_finish_command(c);
246 }
248 gint
249 mpdclient_cmd_next(mpdclient_t *c)
250 {
251 mpd_sendNextCommand(c->connection);
252 c->need_update = TRUE;
253 return mpdclient_finish_command(c);
254 }
256 gint
257 mpdclient_cmd_prev(mpdclient_t *c)
258 {
259 mpd_sendPrevCommand(c->connection);
260 c->need_update = TRUE;
261 return mpdclient_finish_command(c);
262 }
264 gint
265 mpdclient_cmd_seek(mpdclient_t *c, gint id, gint pos)
266 {
267 mpd_sendSeekIdCommand(c->connection, id, pos);
268 return mpdclient_finish_command(c);
269 }
271 gint
272 mpdclient_cmd_shuffle(mpdclient_t *c)
273 {
274 mpd_sendShuffleCommand(c->connection);
275 c->need_update = TRUE;
276 return mpdclient_finish_command(c);
277 }
279 gint
280 mpdclient_cmd_clear(mpdclient_t *c)
281 {
282 gint retval = 0;
284 mpd_sendClearCommand(c->connection);
285 retval = mpdclient_finish_command(c);
286 /* call playlist updated callback */
287 mpdclient_playlist_callback(c, PLAYLIST_EVENT_CLEAR, NULL);
288 c->need_update = TRUE;
289 return retval;
290 }
292 gint
293 mpdclient_cmd_repeat(mpdclient_t *c, gint value)
294 {
295 mpd_sendRepeatCommand(c->connection, value);
296 return mpdclient_finish_command(c);
297 }
299 gint
300 mpdclient_cmd_random(mpdclient_t *c, gint value)
301 {
302 mpd_sendRandomCommand(c->connection, value);
303 return mpdclient_finish_command(c);
304 }
306 gint
307 mpdclient_cmd_crossfade(mpdclient_t *c, gint value)
308 {
309 mpd_sendCrossfadeCommand(c->connection, value);
310 return mpdclient_finish_command(c);
311 }
313 gint
314 mpdclient_cmd_db_update(mpdclient_t *c)
315 {
316 mpd_sendUpdateCommand(c->connection);
317 return mpdclient_finish_command(c);
318 }
320 gint
321 mpdclient_cmd_volume(mpdclient_t *c, gint value)
322 {
323 mpd_sendSetvolCommand(c->connection, value);
324 return mpdclient_finish_command(c);
325 }
327 gint
328 mpdclient_cmd_add(mpdclient_t *c, mpd_Song *song)
329 {
330 gint retval = 0;
332 if( !song || !song->file )
333 return -1;
335 /* send the add command to mpd */
336 mpd_sendAddCommand(c->connection, song->file);
337 if( (retval=mpdclient_finish_command(c)) )
338 return retval;
340 #ifdef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_ADD
341 /* add the song to playlist */
342 c->playlist.list = g_list_append(c->playlist.list, mpd_songDup(song));
343 c->playlist.length++;
345 /* increment the playlist id, so we dont retrives a new playlist */
346 c->playlist.id++;
348 /* call playlist updated callback */
349 mpdclient_playlist_callback(c, PLAYLIST_EVENT_ADD, (gpointer) song);
350 #else
351 c->need_update = TRUE;
352 #endif
354 return 0;
355 }
357 gint
358 mpdclient_cmd_delete(mpdclient_t *c, gint index)
359 {
360 gint retval = 0;
361 mpd_Song *song = playlist_get_song(c, index);
363 if( !song )
364 return -1;
366 /* send the delete command to mpd */
367 #ifdef ENABLE_SONG_ID
368 mpd_sendDeleteIdCommand(c->connection, song->id);
369 #else
370 mpd_sendDeleteCommand(c->connection, index);
371 #endif
372 if( (retval=mpdclient_finish_command(c)) )
373 return retval;
375 #ifdef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_DELETE
376 /* increment the playlist id, so we dont retrive a new playlist */
377 c->playlist.id++;
379 /* remove the song from the playlist */
380 c->playlist.list = g_list_remove(c->playlist.list, (gpointer) song);
381 c->playlist.length = g_list_length(c->playlist.list);
383 /* call playlist updated callback */
384 mpdclient_playlist_callback(c, PLAYLIST_EVENT_DELETE, (gpointer) song);
386 /* remove references to the song */
387 if( c->song == song )
388 {
389 c->song = NULL;
390 c->need_update = TRUE;
391 }
393 /* free song */
394 mpd_freeSong(song);
396 #else
397 c->need_update = TRUE;
398 #endif
400 return 0;
401 }
403 gint
404 mpdclient_cmd_move(mpdclient_t *c, gint old_index, gint new_index)
405 {
406 gint retval, index1, index2;
407 GList *item1, *item2;
408 gpointer data1, data2;
409 mpd_Song *song1, *song2;
411 if( old_index==new_index || new_index<0 || new_index>=c->playlist.length )
412 return -1;
414 song1 = playlist_get_song(c, old_index);
415 song2 = playlist_get_song(c, new_index);
417 /* send the move command to mpd */
418 #ifdef ENABLE_SONG_ID
419 mpd_sendMoveIdCommand(c->connection, song1->id, song2->id);
420 #else
421 mpd_sendMoveCommand(c->connection, old_index, new_index);
422 #endif
423 if( (retval=mpdclient_finish_command(c)) )
424 return retval;
426 #ifdef ENABLE_FANCY_PLAYLIST_MANAGMENT_CMD_MOVE
427 index1 = MIN(old_index, new_index);
428 index2 = MAX(old_index, new_index);
429 item1 = g_list_nth(c->playlist.list, index1);
430 item2 = g_list_nth(c->playlist.list, index2);
431 data1 = item1->data;
432 data2 = item2->data;
434 /* move the second item */
435 c->playlist.list = g_list_remove(c->playlist.list, data2);
436 c->playlist.list = g_list_insert_before(c->playlist.list, item1, data2);
438 /* move the first item */
439 if( index2-index1 >1 )
440 {
441 item2 = g_list_nth(c->playlist.list, index2);
442 c->playlist.list = g_list_remove(c->playlist.list, data1);
443 c->playlist.list = g_list_insert_before(c->playlist.list, item2, data1);
444 }
446 /* increment the playlist id, so we dont retrives a new playlist */
447 c->playlist.id++;
449 /* call playlist updated callback */
450 mpdclient_playlist_callback(c, PLAYLIST_EVENT_MOVE, (gpointer) &new_index);
451 #else
452 c->need_update = TRUE;
453 #endif
455 return 0;
456 }
458 gint
459 mpdclient_cmd_save_playlist(mpdclient_t *c, gchar *filename)
460 {
461 gint retval = 0;
462 gchar *filename_utf8 = locale_to_utf8(filename);
464 mpd_sendSaveCommand(c->connection, filename_utf8);
465 if( (retval=mpdclient_finish_command(c)) == 0 )
466 mpdclient_browse_callback(c, BROWSE_PLAYLIST_SAVED, NULL);
467 g_free(filename_utf8);
468 return retval;
469 }
471 gint
472 mpdclient_cmd_load_playlist(mpdclient_t *c, gchar *filename_utf8)
473 {
474 mpd_sendLoadCommand(c->connection, filename_utf8);
475 c->need_update = TRUE;
476 return mpdclient_finish_command(c);
477 }
479 gint
480 mpdclient_cmd_delete_playlist(mpdclient_t *c, gchar *filename_utf8)
481 {
482 gint retval = 0;
484 mpd_sendRmCommand(c->connection, filename_utf8);
485 if( (retval=mpdclient_finish_command(c)) == 0 )
486 mpdclient_browse_callback(c, BROWSE_PLAYLIST_DELETED, NULL);
487 return retval;
488 }
491 /****************************************************************************/
492 /*** Callback managment functions *******************************************/
493 /****************************************************************************/
494 static void
495 do_list_callbacks(mpdclient_t *c, GList *list, gint event, gpointer data)
496 {
497 while(list)
498 {
499 mpdc_list_cb_t fn = list->data;
501 fn(c, event, data);
502 list=list->next;
503 }
504 }
506 void
507 mpdclient_playlist_callback(mpdclient_t *c, int event, gpointer data)
508 {
509 do_list_callbacks(c, c->playlist_callbacks, event, data);
510 }
512 void
513 mpdclient_install_playlist_callback(mpdclient_t *c,mpdc_list_cb_t cb)
514 {
515 c->playlist_callbacks = g_list_append(c->playlist_callbacks, cb);
516 }
518 void
519 mpdclient_remove_playlist_callback(mpdclient_t *c, mpdc_list_cb_t cb)
520 {
521 c->playlist_callbacks = g_list_remove(c->playlist_callbacks, cb);
522 }
524 void
525 mpdclient_browse_callback(mpdclient_t *c, int event, gpointer data)
526 {
527 do_list_callbacks(c, c->browse_callbacks, event, data);
528 }
531 void
532 mpdclient_install_browse_callback(mpdclient_t *c,mpdc_list_cb_t cb)
533 {
534 c->browse_callbacks = g_list_append(c->browse_callbacks, cb);
535 }
537 void
538 mpdclient_remove_browse_callback(mpdclient_t *c, mpdc_list_cb_t cb)
539 {
540 c->browse_callbacks = g_list_remove(c->browse_callbacks, cb);
541 }
543 void
544 mpdclient_install_error_callback(mpdclient_t *c, mpdc_error_cb_t cb)
545 {
546 c->error_callbacks = g_list_append(c->error_callbacks, cb);
547 }
549 void
550 mpdclient_remove_error_callback(mpdclient_t *c, mpdc_error_cb_t cb)
551 {
552 c->error_callbacks = g_list_remove(c->error_callbacks, cb);
553 }
555 /****************************************************************************/
556 /*** Playlist managment functions *******************************************/
557 /****************************************************************************/
559 gint
560 mpdclient_playlist_free(mpdclient_playlist_t *playlist)
561 {
562 GList *list = g_list_first(playlist->list);
564 while(list)
565 {
566 mpd_Song *song = (mpd_Song *) list->data;
567 mpd_freeSong(song);
568 list=list->next;
569 }
570 g_list_free(playlist->list);
571 playlist->list = NULL;
572 playlist->length = 0;
573 return 0;
574 }
576 /* update playlist */
577 gint
578 mpdclient_playlist_update(mpdclient_t *c)
579 {
580 mpd_InfoEntity *entity;
582 D("mpdclient_playlist_update() [%lld]\n", c->status->playlist);
584 if( MPD_ERROR(c) )
585 return -1;
587 if( c->playlist.list )
588 mpdclient_playlist_free(&c->playlist);
590 c->song = NULL;
591 c->playlist.updated = TRUE;
593 mpd_sendPlaylistInfoCommand(c->connection,-1);
594 while( (entity=mpd_getNextInfoEntity(c->connection)) )
595 {
596 if(entity->type==MPD_INFO_ENTITY_TYPE_SONG)
597 {
598 mpd_Song *song = mpd_songDup(entity->info.song);
600 c->playlist.list = g_list_append(c->playlist.list, (gpointer) song);
601 c->playlist.length++;
602 }
603 mpd_freeInfoEntity(entity);
604 }
605 c->playlist.id = c->status->playlist;
606 c->song = NULL;
608 /* call playlist updated callbacks */
609 mpdclient_playlist_callback(c, PLAYLIST_EVENT_UPDATED, NULL);
611 return mpdclient_finish_command(c);
612 }
614 /* update playlist (plchanges) */
615 gint
616 mpdclient_playlist_update_changes(mpdclient_t *c)
617 {
618 mpd_InfoEntity *entity;
620 D("mpdclient_playlist_update_changes() [%lld -> %lld]\n",
621 c->status->playlist, c->playlist.id);
623 if( MPD_ERROR(c) )
624 return -1;
626 mpd_sendPlChangesCommand(c->connection, c->playlist.id);
628 while( (entity=mpd_getNextInfoEntity(c->connection)) != NULL )
629 {
630 if(entity->type==MPD_INFO_ENTITY_TYPE_SONG)
631 {
632 mpd_Song *song;
633 GList *item;
635 if( (song=mpd_songDup(entity->info.song)) == NULL )
636 {
637 D("song==NULL => calling mpdclient_playlist_update()\n");
638 return mpdclient_playlist_update(c);
639 }
641 item = playlist_lookup(c, song->id);
643 if( item && item->data)
644 {
645 /* Update playlist entry */
646 mpd_freeSong((mpd_Song *) item->data);
647 item->data = song;
648 if( c->song && c->song->id == song->id )
649 c->song = song;
650 D("Changing num %d [%d] to %s\n",
651 song->pos, song->id, get_song_name(song));
652 }
653 else
654 {
655 /* Add a new playlist entry */
656 D("Adding pos:%d, id;%d - %s\n",
657 song->pos, song->id, get_song_name(song));
658 c->playlist.list = g_list_append(c->playlist.list,
659 (gpointer) song);
660 c->playlist.length++;
661 }
662 }
663 mpd_freeInfoEntity(entity);
664 }
665 mpd_finishCommand(c->connection);
667 while( g_list_length(c->playlist.list) > c->status->playlistLength )
668 {
669 GList *item = g_list_last(c->playlist.list);
671 /* Remove the last playlist entry */
672 mpd_freeSong((mpd_Song *) item->data);
673 c->playlist.list = g_list_delete_link(c->playlist.list, item);
674 c->playlist.length--;
675 D("Removed the last playlist entry\n");
676 }
678 c->playlist.id = c->status->playlist;
679 c->playlist.updated = TRUE;
681 mpdclient_playlist_callback(c, PLAYLIST_EVENT_UPDATED, NULL);
683 return 0;
684 }
686 mpd_Song *
687 playlist_get_song(mpdclient_t *c, gint index)
688 {
689 return (mpd_Song *) g_list_nth_data(c->playlist.list, index);
690 }
692 GList *
693 playlist_lookup(mpdclient_t *c, gint id)
694 {
695 GList *list = c->playlist.list;
697 while( list )
698 {
699 mpd_Song *song = (mpd_Song *) list->data;
700 if( song->id == id )
701 return list;
702 list=list->next;
703 }
704 return NULL;
705 }
707 mpd_Song *
708 playlist_lookup_song(mpdclient_t *c, gint id)
709 {
710 GList *list = c->playlist.list;
712 while( list )
713 {
714 mpd_Song *song = (mpd_Song *) list->data;
715 if( song->id == id )
716 return song;
717 list=list->next;
718 }
719 return NULL;
720 }
722 gint
723 playlist_get_index(mpdclient_t *c, mpd_Song *song)
724 {
725 return g_list_index(c->playlist.list, song);
726 }
728 gint
729 playlist_get_index_from_id(mpdclient_t *c, gint id)
730 {
731 return g_list_index(c->playlist.list, playlist_lookup_song(c, id));
732 }
734 gint
735 playlist_get_index_from_file(mpdclient_t *c, gchar *filename)
736 {
737 GList *list = c->playlist.list;
738 gint i=0;
740 while( list )
741 {
742 mpd_Song *song = (mpd_Song *) list->data;
743 if( strcmp(song->file, filename ) == 0 )
744 return i;
745 list=list->next;
746 i++;
747 }
748 return -1;
749 }
752 /****************************************************************************/
753 /*** Filelist functions *****************************************************/
754 /****************************************************************************/
756 mpdclient_filelist_t *
757 mpdclient_filelist_free(mpdclient_filelist_t *filelist)
758 {
759 GList *list = g_list_first(filelist->list);
761 while( list!=NULL )
762 {
763 filelist_entry_t *entry = list->data;
765 if( entry->entity )
766 mpd_freeInfoEntity(entry->entity);
767 g_free(entry);
768 list=list->next;
769 }
770 g_list_free(filelist->list);
771 g_free(filelist->path);
772 filelist->path = NULL;
773 filelist->list = NULL;
774 filelist->length = 0;
775 g_free(filelist);
777 return NULL;
778 }
781 mpdclient_filelist_t *
782 mpdclient_filelist_get(mpdclient_t *c, gchar *path)
783 {
784 mpdclient_filelist_t *filelist;
785 mpd_InfoEntity *entity;
786 gchar *path_utf8 = locale_to_utf8(path);
788 mpd_sendLsInfoCommand(c->connection, path_utf8);
789 filelist = g_malloc0(sizeof(mpdclient_filelist_t));
790 if( path && path[0] && strcmp(path, "/") )
791 {
792 /* add a dummy entry for ./.. */
793 filelist_entry_t *entry = g_malloc0(sizeof(filelist_entry_t));
794 entry->entity = NULL;
795 filelist->list = g_list_append(filelist->list, (gpointer) entry);
796 filelist->length++;
797 }
799 while( (entity=mpd_getNextInfoEntity(c->connection)) )
800 {
801 filelist_entry_t *entry = g_malloc0(sizeof(filelist_entry_t));
803 entry->entity = entity;
804 filelist->list = g_list_append(filelist->list, (gpointer) entry);
805 filelist->length++;
806 }
808 if( mpdclient_finish_command(c) )
809 {
810 g_free(path_utf8);
811 return mpdclient_filelist_free(filelist);
812 }
814 g_free(path_utf8);
815 filelist->path = g_strdup(path);
816 filelist->updated = TRUE;
818 return filelist;
819 }
821 mpdclient_filelist_t *
822 mpdclient_filelist_update(mpdclient_t *c, mpdclient_filelist_t *filelist)
823 {
824 if( filelist == NULL )
825 {
826 gchar *path = g_strdup(filelist->path);
828 filelist = mpdclient_filelist_free(filelist);
829 filelist = mpdclient_filelist_get(c, path);
830 g_free(path);
831 return filelist;
832 }
833 return NULL;
834 }
836 filelist_entry_t *
837 mpdclient_filelist_find_song(mpdclient_filelist_t *fl, mpd_Song *song)
838 {
839 GList *list = g_list_first(fl->list);
841 while( list && song)
842 {
843 filelist_entry_t *entry = list->data;
844 mpd_InfoEntity *entity = entry->entity;
846 if( entity && entity->type==MPD_INFO_ENTITY_TYPE_SONG )
847 {
848 mpd_Song *song2 = entity->info.song;
850 if( strcmp(song->file, song2->file) == 0 )
851 {
852 return entry;
853 }
854 }
855 list = list->next;
856 }
857 return NULL;
858 }