410 lines
19 KiB
C
410 lines
19 KiB
C
#include "mpris.h"
|
|
#include <systemd/sd-bus.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define MPRIS_PATH "/org/mpris/MediaPlayer2"
|
|
#define IFACE_ROOT "org.mpris.MediaPlayer2"
|
|
#define IFACE_PLAYER "org.mpris.MediaPlayer2.Player"
|
|
|
|
struct MprisPlayer {
|
|
sd_bus *bus;
|
|
sd_bus_slot *slot_root;
|
|
sd_bus_slot *slot_player;
|
|
|
|
char *identity;
|
|
char *playback_status;
|
|
double volume;
|
|
int64_t position_us;
|
|
int can_go_next;
|
|
int can_go_previous;
|
|
int can_play;
|
|
int can_pause;
|
|
int can_seek;
|
|
|
|
char *track_id;
|
|
char *title;
|
|
char *artist;
|
|
char *album;
|
|
int64_t length_us;
|
|
|
|
MprisCallback on_play; void *ud_play;
|
|
MprisCallback on_pause; void *ud_pause;
|
|
MprisCallback on_play_pause; void *ud_play_pause;
|
|
MprisCallback on_stop; void *ud_stop;
|
|
MprisCallback on_next; void *ud_next;
|
|
MprisCallback on_previous; void *ud_previous;
|
|
MprisSeekCallback on_seek; void *ud_seek;
|
|
MprisSetPositionCallback on_set_position; void *ud_set_position;
|
|
MprisVolumeCallback on_volume; void *ud_volume;
|
|
};
|
|
|
|
/* ── Root interface ─────────────────────────────────────────────────────── */
|
|
|
|
static int handle_raise(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_quit(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
|
|
static int get_identity(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "s", ((MprisPlayer *)ud)->identity);
|
|
}
|
|
static int get_false(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", 0);
|
|
}
|
|
static int get_empty_strv(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
int r = sd_bus_message_open_container(reply, 'a', "s");
|
|
if (r < 0) return r;
|
|
return sd_bus_message_close_container(reply);
|
|
}
|
|
|
|
static const sd_bus_vtable root_vtable[] = {
|
|
SD_BUS_VTABLE_START(0),
|
|
SD_BUS_METHOD("Raise", "", "", handle_raise, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("Quit", "", "", handle_quit, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_PROPERTY("CanQuit", "b", get_false, 0, 0),
|
|
SD_BUS_PROPERTY("CanRaise", "b", get_false, 0, 0),
|
|
SD_BUS_PROPERTY("HasTrackList", "b", get_false, 0, 0),
|
|
SD_BUS_PROPERTY("Identity", "s", get_identity, 0, 0),
|
|
SD_BUS_PROPERTY("SupportedUriSchemes", "as", get_empty_strv, 0, 0),
|
|
SD_BUS_PROPERTY("SupportedMimeTypes", "as", get_empty_strv, 0, 0),
|
|
SD_BUS_VTABLE_END
|
|
};
|
|
|
|
/* ── Player interface ───────────────────────────────────────────────────── */
|
|
|
|
static int handle_play(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
if (p->on_play) p->on_play(p->ud_play);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_pause(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
if (p->on_pause) p->on_pause(p->ud_pause);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_play_pause(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
if (p->on_play_pause) p->on_play_pause(p->ud_play_pause);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_stop(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
if (p->on_stop) p->on_stop(p->ud_stop);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_next(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
if (p->on_next) p->on_next(p->ud_next);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_previous(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
if (p->on_previous) p->on_previous(p->ud_previous);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_seek(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
int64_t offset;
|
|
int r = sd_bus_message_read(m, "x", &offset);
|
|
if (r < 0) return r;
|
|
if (p->on_seek) p->on_seek(offset, p->ud_seek);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_set_position(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
const char *track_id;
|
|
int64_t position;
|
|
int r = sd_bus_message_read(m, "ox", &track_id, &position);
|
|
if (r < 0) return r;
|
|
if (p->on_set_position) p->on_set_position(track_id, position, p->ud_set_position);
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
static int handle_open_uri(sd_bus_message *m, void *ud, sd_bus_error *e) {
|
|
return sd_bus_reply_method_return(m, "");
|
|
}
|
|
|
|
static int get_playback_status(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "s", ((MprisPlayer *)ud)->playback_status);
|
|
}
|
|
|
|
static int get_loop_status(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "s", "None");
|
|
}
|
|
|
|
static int get_shuffle(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", 0);
|
|
}
|
|
|
|
static int get_metadata(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
int r;
|
|
|
|
r = sd_bus_message_open_container(reply, 'a', "{sv}");
|
|
if (r < 0) return r;
|
|
|
|
/* mpris:trackid is mandatory */
|
|
const char *trackid = p->track_id ? p->track_id : "/org/mpris/MediaPlayer2/TrackList/NoTrack";
|
|
r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", "mpris:trackid"); if (r < 0) return r;
|
|
r = sd_bus_message_open_container(reply, 'v', "o"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "o", trackid); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
|
|
if (p->title) {
|
|
r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", "xesam:title"); if (r < 0) return r;
|
|
r = sd_bus_message_open_container(reply, 'v', "s"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", p->title); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
}
|
|
|
|
/* xesam:artist is an array of strings */
|
|
if (p->artist) {
|
|
r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", "xesam:artist"); if (r < 0) return r;
|
|
r = sd_bus_message_open_container(reply, 'v', "as"); if (r < 0) return r;
|
|
r = sd_bus_message_open_container(reply, 'a', "s"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", p->artist); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
}
|
|
|
|
if (p->album) {
|
|
r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", "xesam:album"); if (r < 0) return r;
|
|
r = sd_bus_message_open_container(reply, 'v', "s"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", p->album); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
}
|
|
|
|
if (p->length_us > 0) {
|
|
r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "s", "mpris:length"); if (r < 0) return r;
|
|
r = sd_bus_message_open_container(reply, 'v', "x"); if (r < 0) return r;
|
|
r = sd_bus_message_append(reply, "x", p->length_us); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
r = sd_bus_message_close_container(reply); if (r < 0) return r;
|
|
}
|
|
|
|
return sd_bus_message_close_container(reply);
|
|
}
|
|
|
|
static int get_volume(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "d", ((MprisPlayer *)ud)->volume);
|
|
}
|
|
|
|
static int set_volume(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *value,
|
|
void *ud, sd_bus_error *e) {
|
|
MprisPlayer *p = ud;
|
|
double v;
|
|
int r = sd_bus_message_read(value, "d", &v);
|
|
if (r < 0) return r;
|
|
p->volume = v;
|
|
if (p->on_volume) p->on_volume(v, p->ud_volume);
|
|
return 1; /* signal that the value changed so PropertiesChanged is emitted */
|
|
}
|
|
|
|
static int get_position(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "x", ((MprisPlayer *)ud)->position_us);
|
|
}
|
|
|
|
static int get_rate(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "d", 1.0);
|
|
}
|
|
|
|
static int get_can_go_next(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_go_next);
|
|
}
|
|
static int get_can_go_previous(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_go_previous);
|
|
}
|
|
static int get_can_play(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_play);
|
|
}
|
|
static int get_can_pause(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_pause);
|
|
}
|
|
static int get_can_seek(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_seek);
|
|
}
|
|
static int get_can_control(sd_bus *bus, const char *path, const char *iface,
|
|
const char *prop, sd_bus_message *reply,
|
|
void *ud, sd_bus_error *e) {
|
|
return sd_bus_message_append(reply, "b", 1);
|
|
}
|
|
|
|
static const sd_bus_vtable player_vtable[] = {
|
|
SD_BUS_VTABLE_START(0),
|
|
SD_BUS_METHOD("Play", "", "", handle_play, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("Pause", "", "", handle_pause, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("PlayPause", "", "", handle_play_pause, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("Stop", "", "", handle_stop, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("Next", "", "", handle_next, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("Previous", "", "", handle_previous, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("Seek", "x", "", handle_seek, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("SetPosition", "ox", "", handle_set_position, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_METHOD("OpenUri", "s", "", handle_open_uri, SD_BUS_VTABLE_UNPRIVILEGED),
|
|
SD_BUS_SIGNAL("Seeked", "x", 0),
|
|
SD_BUS_PROPERTY ("PlaybackStatus", "s", get_playback_status, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("LoopStatus", "s", get_loop_status, 0, 0),
|
|
SD_BUS_PROPERTY ("Shuffle", "b", get_shuffle, 0, 0),
|
|
SD_BUS_PROPERTY ("Metadata", "a{sv}", get_metadata, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_WRITABLE_PROPERTY("Volume", "d", get_volume, set_volume, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("Position", "x", get_position, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION),
|
|
SD_BUS_PROPERTY ("Rate", "d", get_rate, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("MinimumRate", "d", get_rate, 0, 0),
|
|
SD_BUS_PROPERTY ("MaximumRate", "d", get_rate, 0, 0),
|
|
SD_BUS_PROPERTY ("CanGoNext", "b", get_can_go_next, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("CanGoPrevious", "b", get_can_go_previous, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("CanPlay", "b", get_can_play, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("CanPause", "b", get_can_pause, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("CanSeek", "b", get_can_seek, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
|
SD_BUS_PROPERTY ("CanControl", "b", get_can_control, 0, 0),
|
|
SD_BUS_VTABLE_END
|
|
};
|
|
|
|
/* ── Public API ─────────────────────────────────────────────────────────── */
|
|
|
|
MprisPlayer *mpris_player_create(const char *player_name, const char *identity) {
|
|
MprisPlayer *p = calloc(1, sizeof(MprisPlayer));
|
|
if (!p) return NULL;
|
|
|
|
p->identity = strdup(identity);
|
|
p->playback_status = strdup("Stopped");
|
|
p->volume = 1.0;
|
|
p->can_play = 1;
|
|
p->can_pause = 1;
|
|
|
|
int r;
|
|
|
|
r = sd_bus_open_user(&p->bus);
|
|
if (r < 0) { fprintf(stderr, "mpris: D-Bus connect failed: %s\n", strerror(-r)); goto fail; }
|
|
|
|
char bus_name[256];
|
|
snprintf(bus_name, sizeof(bus_name), "org.mpris.MediaPlayer2.%s", player_name);
|
|
|
|
r = sd_bus_request_name(p->bus, bus_name, 0);
|
|
if (r < 0) { fprintf(stderr, "mpris: could not acquire %s: %s\n", bus_name, strerror(-r)); goto fail; }
|
|
|
|
r = sd_bus_add_object_vtable(p->bus, &p->slot_root,
|
|
MPRIS_PATH, IFACE_ROOT, root_vtable, p);
|
|
if (r < 0) { fprintf(stderr, "mpris: register root interface failed: %s\n", strerror(-r)); goto fail; }
|
|
|
|
r = sd_bus_add_object_vtable(p->bus, &p->slot_player,
|
|
MPRIS_PATH, IFACE_PLAYER, player_vtable, p);
|
|
if (r < 0) { fprintf(stderr, "mpris: register player interface failed: %s\n", strerror(-r)); goto fail; }
|
|
|
|
return p;
|
|
|
|
fail:
|
|
mpris_player_destroy(p);
|
|
return NULL;
|
|
}
|
|
|
|
void mpris_player_destroy(MprisPlayer *p) {
|
|
if (!p) return;
|
|
sd_bus_slot_unref(p->slot_player);
|
|
sd_bus_slot_unref(p->slot_root);
|
|
sd_bus_unref(p->bus);
|
|
free(p->identity);
|
|
free(p->playback_status);
|
|
free(p->track_id);
|
|
free(p->title);
|
|
free(p->artist);
|
|
free(p->album);
|
|
free(p);
|
|
}
|
|
|
|
#define EMIT_PLAYER(p, ...) \
|
|
sd_bus_emit_properties_changed((p)->bus, MPRIS_PATH, IFACE_PLAYER, __VA_ARGS__, NULL)
|
|
|
|
void mpris_set_playback_status(MprisPlayer *p, const char *status) {
|
|
free(p->playback_status);
|
|
p->playback_status = strdup(status);
|
|
EMIT_PLAYER(p, "PlaybackStatus");
|
|
}
|
|
|
|
void mpris_set_metadata(MprisPlayer *p, const char *track_id, const char *title,
|
|
const char *artist, const char *album, int64_t length_us) {
|
|
free(p->track_id); p->track_id = track_id ? strdup(track_id) : NULL;
|
|
free(p->title); p->title = title ? strdup(title) : NULL;
|
|
free(p->artist); p->artist = artist ? strdup(artist) : NULL;
|
|
free(p->album); p->album = album ? strdup(album) : NULL;
|
|
p->length_us = length_us;
|
|
EMIT_PLAYER(p, "Metadata");
|
|
}
|
|
|
|
void mpris_set_volume(MprisPlayer *p, double volume) {
|
|
p->volume = volume;
|
|
EMIT_PLAYER(p, "Volume");
|
|
}
|
|
|
|
void mpris_set_position(MprisPlayer *p, int64_t position_us) {
|
|
p->position_us = position_us;
|
|
/* Position uses EMITS_INVALIDATION — clients poll it or watch the Seeked signal */
|
|
}
|
|
|
|
void mpris_set_can_go_next(MprisPlayer *p, int v) { p->can_go_next = v; EMIT_PLAYER(p, "CanGoNext"); }
|
|
void mpris_set_can_go_previous(MprisPlayer *p, int v) { p->can_go_previous = v; EMIT_PLAYER(p, "CanGoPrevious"); }
|
|
void mpris_set_can_play(MprisPlayer *p, int v) { p->can_play = v; EMIT_PLAYER(p, "CanPlay"); }
|
|
void mpris_set_can_pause(MprisPlayer *p, int v) { p->can_pause = v; EMIT_PLAYER(p, "CanPause"); }
|
|
void mpris_set_can_seek(MprisPlayer *p, int v) { p->can_seek = v; EMIT_PLAYER(p, "CanSeek"); }
|
|
|
|
void mpris_emit_seeked(MprisPlayer *p, int64_t position_us) {
|
|
p->position_us = position_us;
|
|
sd_bus_emit_signal(p->bus, MPRIS_PATH, IFACE_PLAYER, "Seeked", "x", position_us);
|
|
}
|
|
|
|
void mpris_on_play (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_play = cb; p->ud_play = ud; }
|
|
void mpris_on_pause (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_pause = cb; p->ud_pause = ud; }
|
|
void mpris_on_play_pause (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_play_pause = cb; p->ud_play_pause = ud; }
|
|
void mpris_on_stop (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_stop = cb; p->ud_stop = ud; }
|
|
void mpris_on_next (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_next = cb; p->ud_next = ud; }
|
|
void mpris_on_previous (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_previous = cb; p->ud_previous = ud; }
|
|
void mpris_on_seek (MprisPlayer *p, MprisSeekCallback cb, void *ud) { p->on_seek = cb; p->ud_seek = ud; }
|
|
void mpris_on_set_position(MprisPlayer *p, MprisSetPositionCallback cb, void *ud) { p->on_set_position = cb; p->ud_set_position = ud; }
|
|
void mpris_on_volume (MprisPlayer *p, MprisVolumeCallback cb, void *ud) { p->on_volume = cb; p->ud_volume = ud; }
|
|
|
|
int mpris_process(MprisPlayer *p) {
|
|
return sd_bus_process(p->bus, NULL);
|
|
}
|