| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2006-2007 Nokia Corporation |
| * Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org> |
| * Copyright (C) 2012-2012 Intel Corporation |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdlib.h> |
| #include <stdint.h> |
| #include <inttypes.h> |
| #include <stdbool.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <string.h> |
| |
| #include <glib.h> |
| #include <dbus/dbus.h> |
| |
| #include "gdbus/gdbus.h" |
| |
| #include "src/log.h" |
| #include "src/dbus-common.h" |
| #include "src/error.h" |
| |
| #include "player.h" |
| |
| #define MEDIA_PLAYER_INTERFACE "org.bluez.MediaPlayer1" |
| #define MEDIA_FOLDER_INTERFACE "org.bluez.MediaFolder1" |
| #define MEDIA_ITEM_INTERFACE "org.bluez.MediaItem1" |
| |
| struct player_callback { |
| const struct media_player_callback *cbs; |
| void *user_data; |
| }; |
| |
| struct pending_req { |
| GDBusPendingPropertySet id; |
| const char *key; |
| const char *value; |
| }; |
| |
| struct media_item { |
| struct media_player *player; |
| char *path; /* Item object path */ |
| char *name; /* Item name */ |
| player_item_type_t type; /* Item type */ |
| player_folder_type_t folder_type; /* Folder type */ |
| bool playable; /* Item playable flag */ |
| uint64_t uid; /* Item uid */ |
| GHashTable *metadata; /* Item metadata */ |
| }; |
| |
| struct media_folder { |
| struct media_folder *parent; |
| struct media_item *item; /* Folder item */ |
| uint32_t number_of_items;/* Number of items */ |
| GSList *subfolders; |
| GSList *items; |
| DBusMessage *msg; |
| }; |
| |
| struct media_player { |
| char *device; /* Device path */ |
| char *name; /* Player name */ |
| char *type; /* Player type */ |
| char *subtype; /* Player subtype */ |
| bool browsable; /* Player browsing feature */ |
| bool searchable; /* Player searching feature */ |
| struct media_folder *scope; /* Player current scope */ |
| struct media_folder *folder; /* Player current folder */ |
| struct media_folder *search; /* Player search folder */ |
| struct media_folder *playlist; /* Player current playlist */ |
| char *path; /* Player object path */ |
| GHashTable *settings; /* Player settings */ |
| GHashTable *track; /* Player current track */ |
| char *status; |
| uint32_t position; |
| GTimer *progress; |
| guint process_id; |
| struct player_callback *cb; |
| GSList *pending; |
| GSList *folders; |
| }; |
| |
| static void append_track(void *key, void *value, void *user_data) |
| { |
| DBusMessageIter *dict = user_data; |
| const char *strkey = key; |
| |
| if (strcasecmp(strkey, "Duration") == 0 || |
| strcasecmp(strkey, "TrackNumber") == 0 || |
| strcasecmp(strkey, "NumberOfTracks") == 0) { |
| uint32_t num = atoi(value); |
| dict_append_entry(dict, key, DBUS_TYPE_UINT32, &num); |
| } else if (strcasecmp(strkey, "Item") == 0) { |
| dict_append_entry(dict, key, DBUS_TYPE_OBJECT_PATH, &value); |
| } else { |
| dict_append_entry(dict, key, DBUS_TYPE_STRING, &value); |
| } |
| } |
| |
| static struct pending_req *find_pending(struct media_player *mp, |
| const char *key) |
| { |
| GSList *l; |
| |
| for (l = mp->pending; l; l = l->next) { |
| struct pending_req *p = l->data; |
| |
| if (strcasecmp(key, p->key) == 0) |
| return p; |
| } |
| |
| return NULL; |
| } |
| |
| static struct pending_req *pending_new(GDBusPendingPropertySet id, |
| const char *key, const char *value) |
| { |
| struct pending_req *p; |
| |
| p = g_new0(struct pending_req, 1); |
| p->id = id; |
| p->key = key; |
| p->value = value; |
| |
| return p; |
| } |
| |
| static uint32_t media_player_get_position(struct media_player *mp) |
| { |
| double timedelta; |
| uint32_t sec, msec; |
| |
| if (g_strcmp0(mp->status, "playing") != 0 || |
| mp->position == UINT32_MAX) |
| return mp->position; |
| |
| timedelta = g_timer_elapsed(mp->progress, NULL); |
| |
| sec = (uint32_t) timedelta; |
| msec = (uint32_t) ((timedelta - sec) * 1000); |
| |
| return mp->position + sec * 1000 + msec; |
| } |
| |
| static gboolean get_position(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| uint32_t position; |
| |
| position = media_player_get_position(mp); |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &position); |
| |
| return TRUE; |
| } |
| |
| static gboolean status_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->status != NULL; |
| } |
| |
| static gboolean get_status(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| |
| if (mp->status == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->status); |
| |
| return TRUE; |
| } |
| |
| static gboolean setting_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| const char *value; |
| |
| value = g_hash_table_lookup(mp->settings, property->name); |
| |
| return value ? TRUE : FALSE; |
| } |
| |
| static gboolean get_setting(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| const char *value; |
| |
| value = g_hash_table_lookup(mp->settings, property->name); |
| if (value == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &value); |
| |
| return TRUE; |
| } |
| |
| static void player_set_setting(struct media_player *mp, |
| GDBusPendingPropertySet id, |
| const char *key, const char *value) |
| { |
| struct player_callback *cb = mp->cb; |
| struct pending_req *p; |
| |
| if (cb == NULL || cb->cbs->set_setting == NULL) { |
| g_dbus_pending_property_error(id, |
| ERROR_INTERFACE ".NotSupported", |
| "Operation is not supported"); |
| return; |
| } |
| |
| p = find_pending(mp, key); |
| if (p != NULL) { |
| g_dbus_pending_property_error(id, |
| ERROR_INTERFACE ".InProgress", |
| "Operation already in progress"); |
| return; |
| } |
| |
| if (!cb->cbs->set_setting(mp, key, value, cb->user_data)) { |
| g_dbus_pending_property_error(id, |
| ERROR_INTERFACE ".InvalidArguments", |
| "Invalid arguments in method call"); |
| return; |
| } |
| |
| p = pending_new(id, key, value); |
| |
| mp->pending = g_slist_append(mp->pending, p); |
| } |
| |
| static void set_setting(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, GDBusPendingPropertySet id, |
| void *data) |
| { |
| struct media_player *mp = data; |
| const char *value, *current; |
| |
| if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) { |
| g_dbus_pending_property_error(id, |
| ERROR_INTERFACE ".InvalidArguments", |
| "Invalid arguments in method call"); |
| return; |
| } |
| |
| dbus_message_iter_get_basic(iter, &value); |
| |
| current = g_hash_table_lookup(mp->settings, property->name); |
| if (g_strcmp0(current, value) == 0) { |
| g_dbus_pending_property_success(id); |
| return; |
| } |
| |
| player_set_setting(mp, id, property->name, value); |
| } |
| |
| static gboolean track_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return g_hash_table_size(mp->track) != 0; |
| } |
| |
| static gboolean get_track(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| DBusMessageIter dict; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| g_hash_table_foreach(mp->track, append_track, &dict); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| |
| return TRUE; |
| } |
| |
| static gboolean get_device(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, |
| &mp->device); |
| |
| return TRUE; |
| } |
| |
| static gboolean name_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->name != NULL; |
| } |
| |
| static gboolean get_name(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| |
| if (mp->name == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->name); |
| |
| return TRUE; |
| } |
| |
| static gboolean type_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->type != NULL; |
| } |
| |
| static gboolean get_type(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| |
| if (mp->type == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->type); |
| |
| return TRUE; |
| } |
| |
| static gboolean subtype_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->subtype != NULL; |
| } |
| |
| static gboolean get_subtype(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| |
| if (mp->subtype == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &mp->subtype); |
| |
| return TRUE; |
| } |
| |
| static gboolean browsable_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->folder != NULL; |
| } |
| |
| static gboolean get_browsable(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| dbus_bool_t value; |
| |
| if (mp->folder == NULL) |
| return FALSE; |
| |
| value = mp->browsable; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value); |
| |
| return TRUE; |
| } |
| |
| static gboolean searchable_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->folder != NULL; |
| } |
| |
| static gboolean get_searchable(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| dbus_bool_t value; |
| |
| if (mp->folder == NULL) |
| return FALSE; |
| |
| value = mp->searchable; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value); |
| |
| return TRUE; |
| } |
| |
| static gboolean playlist_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->playlist != NULL; |
| } |
| |
| static gboolean get_playlist(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *playlist = mp->playlist; |
| |
| if (playlist == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, |
| &playlist->item->path); |
| |
| return TRUE; |
| } |
| |
| static DBusMessage *media_player_play(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->play == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->play(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_player_pause(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->pause == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->pause(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_player_stop(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->stop == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->stop(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_player_next(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->next == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->next(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_player_previous(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->previous == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->previous(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_player_fast_forward(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->fast_forward == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->fast_forward(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_player_rewind(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (cb->cbs->rewind == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->rewind(mp, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static void parse_folder_list(gpointer data, gpointer user_data) |
| { |
| struct media_item *item = data; |
| DBusMessageIter *array = user_data; |
| DBusMessageIter entry; |
| |
| dbus_message_iter_open_container(array, DBUS_TYPE_DICT_ENTRY, NULL, |
| &entry); |
| |
| dbus_message_iter_append_basic(&entry, DBUS_TYPE_OBJECT_PATH, |
| &item->path); |
| |
| g_dbus_get_properties(btd_get_dbus_connection(), item->path, |
| MEDIA_ITEM_INTERFACE, &entry); |
| |
| dbus_message_iter_close_container(array, &entry); |
| } |
| |
| void media_player_change_folder_complete(struct media_player *mp, |
| const char *path, int ret) |
| { |
| struct media_folder *folder = mp->scope; |
| DBusMessage *reply; |
| |
| if (folder == NULL || folder->msg == NULL) |
| return; |
| |
| if (ret < 0) { |
| reply = btd_error_failed(folder->msg, strerror(-ret)); |
| goto done; |
| } |
| |
| media_player_set_folder(mp, path, ret); |
| |
| reply = g_dbus_create_reply(folder->msg, DBUS_TYPE_INVALID); |
| |
| done: |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| dbus_message_unref(folder->msg); |
| folder->msg = NULL; |
| } |
| |
| void media_player_list_complete(struct media_player *mp, GSList *items, |
| int err) |
| { |
| struct media_folder *folder = mp->scope; |
| DBusMessage *reply; |
| DBusMessageIter iter, array; |
| |
| if (folder == NULL || folder->msg == NULL) |
| return; |
| |
| if (err < 0) { |
| reply = btd_error_failed(folder->msg, strerror(-err)); |
| goto done; |
| } |
| |
| reply = dbus_message_new_method_return(folder->msg); |
| |
| dbus_message_iter_init_append(reply, &iter); |
| |
| dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_OBJECT_PATH_AS_STRING |
| DBUS_TYPE_ARRAY_AS_STRING |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &array); |
| |
| g_slist_foreach(items, parse_folder_list, &array); |
| dbus_message_iter_close_container(&iter, &array); |
| |
| done: |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| dbus_message_unref(folder->msg); |
| folder->msg = NULL; |
| } |
| |
| static struct media_item * |
| media_player_create_subfolder(struct media_player *mp, const char *name, |
| uint64_t uid) |
| { |
| struct media_folder *folder = mp->scope; |
| struct media_item *item; |
| char *path; |
| |
| path = g_strdup_printf("%s/%s", folder->item->name, name); |
| |
| DBG("%s", path); |
| |
| item = media_player_create_item(mp, path, PLAYER_ITEM_TYPE_FOLDER, |
| uid); |
| g_free(path); |
| |
| return item; |
| } |
| |
| void media_player_search_complete(struct media_player *mp, int ret) |
| { |
| struct media_folder *folder = mp->scope; |
| struct media_folder *search = mp->search; |
| DBusMessage *reply; |
| |
| if (folder == NULL || folder->msg == NULL) |
| return; |
| |
| if (ret < 0) { |
| reply = btd_error_failed(folder->msg, strerror(-ret)); |
| goto done; |
| } |
| |
| if (search == NULL) { |
| search = g_new0(struct media_folder, 1); |
| search->item = media_player_create_subfolder(mp, "search", 0); |
| mp->search = search; |
| mp->folders = g_slist_prepend(mp->folders, search); |
| } |
| |
| search->number_of_items = ret; |
| |
| reply = g_dbus_create_reply(folder->msg, |
| DBUS_TYPE_OBJECT_PATH, &search->item->path, |
| DBUS_TYPE_INVALID); |
| |
| done: |
| g_dbus_send_message(btd_get_dbus_connection(), reply); |
| dbus_message_unref(folder->msg); |
| folder->msg = NULL; |
| } |
| |
| void media_player_total_items_complete(struct media_player *mp, |
| uint32_t num_of_items) |
| { |
| struct media_folder *folder = mp->scope; |
| |
| if (folder == NULL || folder->msg == NULL) |
| return; |
| |
| if (folder->number_of_items != num_of_items) { |
| folder->number_of_items = num_of_items; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_FOLDER_INTERFACE, |
| "NumberOfItems"); |
| } |
| } |
| |
| static const GDBusMethodTable media_player_methods[] = { |
| { GDBUS_METHOD("Play", NULL, NULL, media_player_play) }, |
| { GDBUS_METHOD("Pause", NULL, NULL, media_player_pause) }, |
| { GDBUS_METHOD("Stop", NULL, NULL, media_player_stop) }, |
| { GDBUS_METHOD("Next", NULL, NULL, media_player_next) }, |
| { GDBUS_METHOD("Previous", NULL, NULL, media_player_previous) }, |
| { GDBUS_METHOD("FastForward", NULL, NULL, media_player_fast_forward) }, |
| { GDBUS_METHOD("Rewind", NULL, NULL, media_player_rewind) }, |
| { } |
| }; |
| |
| static const GDBusSignalTable media_player_signals[] = { |
| { } |
| }; |
| |
| static const GDBusPropertyTable media_player_properties[] = { |
| { "Name", "s", get_name, NULL, name_exists }, |
| { "Type", "s", get_type, NULL, type_exists }, |
| { "Subtype", "s", get_subtype, NULL, subtype_exists }, |
| { "Position", "u", get_position, NULL, NULL }, |
| { "Status", "s", get_status, NULL, status_exists }, |
| { "Equalizer", "s", get_setting, set_setting, setting_exists }, |
| { "Repeat", "s", get_setting, set_setting, setting_exists }, |
| { "Shuffle", "s", get_setting, set_setting, setting_exists }, |
| { "Scan", "s", get_setting, set_setting, setting_exists }, |
| { "Track", "a{sv}", get_track, NULL, track_exists }, |
| { "Device", "o", get_device, NULL, NULL }, |
| { "Browsable", "b", get_browsable, NULL, browsable_exists }, |
| { "Searchable", "b", get_searchable, NULL, searchable_exists }, |
| { "Playlist", "o", get_playlist, NULL, playlist_exists }, |
| { } |
| }; |
| |
| static DBusMessage *media_folder_search(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *folder = mp->scope; |
| struct player_callback *cb = mp->cb; |
| DBusMessageIter iter; |
| const char *string; |
| int err; |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) |
| return btd_error_invalid_args(msg); |
| |
| dbus_message_iter_get_basic(&iter, &string); |
| |
| if (!mp->searchable || folder != mp->folder || !cb->cbs->search) |
| return btd_error_not_supported(msg); |
| |
| if (folder->msg != NULL) |
| return btd_error_failed(msg, strerror(EINVAL)); |
| |
| err = cb->cbs->search(mp, string, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| folder->msg = dbus_message_ref(msg); |
| |
| return NULL; |
| } |
| |
| static int parse_filters(struct media_player *player, DBusMessageIter *iter, |
| uint32_t *start, uint32_t *end) |
| { |
| struct media_folder *folder = player->scope; |
| DBusMessageIter dict; |
| int ctype; |
| |
| *start = 0; |
| *end = folder->number_of_items ? folder->number_of_items - 1 : |
| UINT32_MAX; |
| |
| ctype = dbus_message_iter_get_arg_type(iter); |
| if (ctype != DBUS_TYPE_ARRAY) |
| return FALSE; |
| |
| dbus_message_iter_recurse(iter, &dict); |
| |
| while ((ctype = dbus_message_iter_get_arg_type(&dict)) != |
| DBUS_TYPE_INVALID) { |
| DBusMessageIter entry, var; |
| const char *key; |
| |
| if (ctype != DBUS_TYPE_DICT_ENTRY) |
| return -EINVAL; |
| |
| dbus_message_iter_recurse(&dict, &entry); |
| if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING) |
| return -EINVAL; |
| |
| dbus_message_iter_get_basic(&entry, &key); |
| dbus_message_iter_next(&entry); |
| |
| if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT) |
| return -EINVAL; |
| |
| dbus_message_iter_recurse(&entry, &var); |
| |
| if (dbus_message_iter_get_arg_type(&var) != DBUS_TYPE_UINT32) |
| return -EINVAL; |
| |
| if (strcasecmp(key, "Start") == 0) |
| dbus_message_iter_get_basic(&var, start); |
| else if (strcasecmp(key, "End") == 0) |
| dbus_message_iter_get_basic(&var, end); |
| |
| dbus_message_iter_next(&dict); |
| } |
| |
| if (folder->number_of_items > 0 && *end > folder->number_of_items) |
| *end = folder->number_of_items; |
| |
| return 0; |
| } |
| |
| static DBusMessage *media_folder_list_items(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *folder = mp->scope; |
| struct player_callback *cb = mp->cb; |
| DBusMessageIter iter; |
| uint32_t start, end; |
| int err; |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| if (parse_filters(mp, &iter, &start, &end) < 0) |
| return btd_error_invalid_args(msg); |
| |
| if (cb->cbs->list_items == NULL) |
| return btd_error_not_supported(msg); |
| |
| if (folder->msg != NULL) |
| return btd_error_failed(msg, strerror(EBUSY)); |
| |
| err = cb->cbs->list_items(mp, folder->item->name, start, end, |
| cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| folder->msg = dbus_message_ref(msg); |
| |
| return NULL; |
| } |
| |
| static void media_item_free(struct media_item *item) |
| { |
| if (item->metadata != NULL) |
| g_hash_table_unref(item->metadata); |
| |
| g_free(item->path); |
| g_free(item->name); |
| g_free(item); |
| } |
| |
| static void media_item_destroy(void *data) |
| { |
| struct media_item *item = data; |
| |
| DBG("%s", item->path); |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), item->path, |
| MEDIA_ITEM_INTERFACE); |
| |
| media_item_free(item); |
| } |
| |
| static void media_folder_destroy(void *data) |
| { |
| struct media_folder *folder = data; |
| |
| g_slist_free_full(folder->subfolders, media_folder_destroy); |
| g_slist_free_full(folder->items, media_item_destroy); |
| |
| if (folder->msg != NULL) |
| dbus_message_unref(folder->msg); |
| |
| media_item_destroy(folder->item); |
| g_free(folder); |
| } |
| |
| static void media_player_change_scope(struct media_player *mp, |
| struct media_folder *folder) |
| { |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (mp->scope == folder) |
| return; |
| |
| DBG("%s", folder->item->name); |
| |
| /* Skip setting current folder if folder is current playlist/search */ |
| if (folder == mp->playlist || folder == mp->search) |
| goto cleanup; |
| |
| mp->folder = folder; |
| |
| /* Skip item cleanup if scope is the current playlist */ |
| if (mp->scope == mp->playlist) |
| goto done; |
| |
| cleanup: |
| g_slist_free_full(mp->scope->items, media_item_destroy); |
| mp->scope->items = NULL; |
| |
| /* Destroy search folder if it exists and is not being set as scope */ |
| if (mp->search != NULL && folder != mp->search) { |
| mp->folders = g_slist_remove(mp->folders, mp->search); |
| media_folder_destroy(mp->search); |
| mp->search = NULL; |
| } |
| |
| done: |
| mp->scope = folder; |
| |
| if (cb->cbs->total_items) { |
| err = cb->cbs->total_items(mp, folder->item->name, |
| cb->user_data); |
| if (err < 0) |
| DBG("Failed to get total num of items"); |
| } else { |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_FOLDER_INTERFACE, |
| "NumberOfItems"); |
| } |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path, |
| MEDIA_FOLDER_INTERFACE, "Name"); |
| } |
| |
| static struct media_folder *find_folder(GSList *folders, const char *pattern) |
| { |
| GSList *l; |
| |
| for (l = folders; l; l = l->next) { |
| struct media_folder *folder = l->data; |
| |
| if (g_str_equal(folder->item->name, pattern)) |
| return folder; |
| |
| if (g_str_equal(folder->item->path, pattern)) |
| return folder; |
| |
| folder = find_folder(folder->subfolders, pattern); |
| if (folder != NULL) |
| return folder; |
| } |
| |
| return NULL; |
| } |
| |
| static struct media_folder *media_player_find_folder(struct media_player *mp, |
| const char *pattern) |
| { |
| return find_folder(mp->folders, pattern); |
| } |
| |
| static DBusMessage *media_folder_change_folder(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *folder = mp->scope; |
| struct player_callback *cb = mp->cb; |
| const char *path; |
| int err; |
| |
| if (!dbus_message_get_args(msg, NULL, |
| DBUS_TYPE_OBJECT_PATH, &path, |
| DBUS_TYPE_INVALID)) |
| return btd_error_invalid_args(msg); |
| |
| if (folder->msg != NULL) |
| return btd_error_failed(msg, strerror(EBUSY)); |
| |
| folder = media_player_find_folder(mp, path); |
| if (folder == NULL) |
| return btd_error_invalid_args(msg); |
| |
| if (mp->scope == folder) |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| |
| if (folder == mp->playlist || folder == mp->folder || |
| folder == mp->search) { |
| media_player_change_scope(mp, folder); |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| /* |
| * ChangePath can only navigate one level up/down so check if folder |
| * is direct child or parent of the current folder otherwise fail. |
| */ |
| if (!g_slist_find(mp->folder->subfolders, folder) && |
| !g_slist_find(folder->subfolders, mp->folder)) |
| return btd_error_invalid_args(msg); |
| |
| if (cb->cbs->change_folder == NULL) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->change_folder(mp, folder->item->name, folder->item->uid, |
| cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| mp->scope->msg = dbus_message_ref(msg); |
| |
| return NULL; |
| } |
| |
| static gboolean folder_name_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *folder = mp->scope; |
| |
| if (folder == NULL || folder->item == NULL) |
| return FALSE; |
| |
| return folder->item->name != NULL; |
| } |
| |
| static gboolean get_folder_name(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *folder = mp->scope; |
| |
| if (folder == NULL || folder->item == NULL) |
| return FALSE; |
| |
| DBG("%s", folder->item->name); |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, |
| &folder->item->name); |
| |
| return TRUE; |
| } |
| |
| static gboolean items_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_player *mp = data; |
| |
| return mp->scope != NULL; |
| } |
| |
| static gboolean get_items(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_player *mp = data; |
| struct media_folder *folder = mp->scope; |
| |
| if (folder == NULL) |
| return FALSE; |
| |
| DBG("%u", folder->number_of_items); |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, |
| &folder->number_of_items); |
| |
| return TRUE; |
| } |
| |
| static const GDBusMethodTable media_folder_methods[] = { |
| { GDBUS_ASYNC_METHOD("Search", |
| GDBUS_ARGS({ "string", "s" }, { "filter", "a{sv}" }), |
| GDBUS_ARGS({ "folder", "o" }), |
| media_folder_search) }, |
| { GDBUS_ASYNC_METHOD("ListItems", |
| GDBUS_ARGS({ "filter", "a{sv}" }), |
| GDBUS_ARGS({ "items", "a{oa{sv}}" }), |
| media_folder_list_items) }, |
| { GDBUS_ASYNC_METHOD("ChangeFolder", |
| GDBUS_ARGS({ "folder", "o" }), NULL, |
| media_folder_change_folder) }, |
| { } |
| }; |
| |
| static const GDBusPropertyTable media_folder_properties[] = { |
| { "Name", "s", get_folder_name, NULL, folder_name_exists }, |
| { "NumberOfItems", "u", get_items, NULL, items_exists }, |
| { } |
| }; |
| |
| static void media_player_set_scope(struct media_player *mp, |
| struct media_folder *folder) |
| { |
| if (mp->scope == NULL) { |
| if (!g_dbus_register_interface(btd_get_dbus_connection(), |
| mp->path, MEDIA_FOLDER_INTERFACE, |
| media_folder_methods, |
| NULL, |
| media_folder_properties, mp, NULL)) { |
| error("D-Bus failed to register %s on %s path", |
| MEDIA_FOLDER_INTERFACE, mp->path); |
| return; |
| } |
| mp->scope = folder; |
| return; |
| } |
| |
| return media_player_change_scope(mp, folder); |
| } |
| |
| void media_player_destroy(struct media_player *mp) |
| { |
| DBG("%s", mp->path); |
| |
| g_dbus_unregister_interface(btd_get_dbus_connection(), mp->path, |
| MEDIA_PLAYER_INTERFACE); |
| |
| if (mp->track) |
| g_hash_table_unref(mp->track); |
| |
| if (mp->settings) |
| g_hash_table_unref(mp->settings); |
| |
| if (mp->process_id > 0) |
| g_source_remove(mp->process_id); |
| |
| if (mp->scope) |
| g_dbus_unregister_interface(btd_get_dbus_connection(), |
| mp->path, |
| MEDIA_FOLDER_INTERFACE); |
| |
| g_slist_free_full(mp->pending, g_free); |
| g_slist_free_full(mp->folders, media_folder_destroy); |
| |
| g_timer_destroy(mp->progress); |
| g_free(mp->cb); |
| g_free(mp->status); |
| g_free(mp->path); |
| g_free(mp->device); |
| g_free(mp->subtype); |
| g_free(mp->type); |
| g_free(mp->name); |
| g_free(mp); |
| } |
| |
| struct media_player *media_player_controller_create(const char *path, |
| uint16_t id) |
| { |
| struct media_player *mp; |
| |
| mp = g_new0(struct media_player, 1); |
| mp->device = g_strdup(path); |
| mp->path = g_strdup_printf("%s/player%u", path, id); |
| mp->settings = g_hash_table_new_full(g_str_hash, g_str_equal, |
| g_free, g_free); |
| mp->track = g_hash_table_new_full(g_str_hash, g_str_equal, |
| g_free, g_free); |
| mp->progress = g_timer_new(); |
| |
| if (!g_dbus_register_interface(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| media_player_methods, |
| media_player_signals, |
| media_player_properties, mp, NULL)) { |
| error("D-Bus failed to register %s path", mp->path); |
| media_player_destroy(mp); |
| return NULL; |
| } |
| |
| DBG("%s", mp->path); |
| |
| return mp; |
| } |
| |
| const char *media_player_get_path(struct media_player *mp) |
| { |
| return mp->path; |
| } |
| |
| void media_player_set_duration(struct media_player *mp, uint32_t duration) |
| { |
| char *value, *curval; |
| |
| DBG("%u", duration); |
| |
| /* Only update duration if track exists */ |
| if (g_hash_table_size(mp->track) == 0) |
| return; |
| |
| /* Ignore if duration is already set */ |
| curval = g_hash_table_lookup(mp->track, "Duration"); |
| if (curval != NULL) |
| return; |
| |
| value = g_strdup_printf("%u", duration); |
| |
| g_hash_table_replace(mp->track, g_strdup("Duration"), value); |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Track"); |
| } |
| |
| void media_player_set_position(struct media_player *mp, uint32_t position) |
| { |
| DBG("%u", position); |
| |
| /* Only update duration if track exists */ |
| if (g_hash_table_size(mp->track) == 0) |
| return; |
| |
| mp->position = position; |
| g_timer_start(mp->progress); |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path, |
| MEDIA_PLAYER_INTERFACE, "Position"); |
| } |
| |
| void media_player_set_setting(struct media_player *mp, const char *key, |
| const char *value) |
| { |
| char *curval; |
| struct pending_req *p; |
| |
| DBG("%s: %s", key, value); |
| |
| if (strcasecmp(key, "Error") == 0) { |
| p = g_slist_nth_data(mp->pending, 0); |
| if (p == NULL) |
| return; |
| |
| g_dbus_pending_property_error(p->id, ERROR_INTERFACE ".Failed", |
| value); |
| goto send; |
| } |
| |
| curval = g_hash_table_lookup(mp->settings, key); |
| if (g_strcmp0(curval, value) == 0) |
| goto done; |
| |
| g_hash_table_replace(mp->settings, g_strdup(key), g_strdup(value)); |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path, |
| MEDIA_PLAYER_INTERFACE, key); |
| |
| done: |
| p = find_pending(mp, key); |
| if (p == NULL) |
| return; |
| |
| if (strcasecmp(value, p->value) == 0) |
| g_dbus_pending_property_success(p->id); |
| else |
| g_dbus_pending_property_error(p->id, |
| ERROR_INTERFACE ".NotSupported", |
| "Operation is not supported"); |
| |
| send: |
| mp->pending = g_slist_remove(mp->pending, p); |
| g_free(p); |
| |
| return; |
| } |
| |
| const char *media_player_get_status(struct media_player *mp) |
| { |
| return mp->status; |
| } |
| |
| void media_player_set_status(struct media_player *mp, const char *status) |
| { |
| DBG("%s", status); |
| |
| if (g_strcmp0(mp->status, status) == 0) |
| return; |
| |
| g_free(mp->status); |
| mp->status = g_strdup(status); |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path, |
| MEDIA_PLAYER_INTERFACE, "Status"); |
| |
| mp->position = media_player_get_position(mp); |
| g_timer_start(mp->progress); |
| } |
| |
| static gboolean process_metadata_changed(void *user_data) |
| { |
| struct media_player *mp = user_data; |
| const char *item; |
| |
| mp->process_id = 0; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Track"); |
| |
| item = g_hash_table_lookup(mp->track, "Item"); |
| if (item == NULL) |
| return FALSE; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| item, MEDIA_ITEM_INTERFACE, |
| "Metadata"); |
| |
| return FALSE; |
| } |
| |
| void media_player_set_metadata(struct media_player *mp, |
| struct media_item *item, const char *key, |
| void *data, size_t len) |
| { |
| char *value, *curval; |
| |
| value = g_strndup(data, len); |
| |
| DBG("%s: %s", key, value); |
| |
| curval = g_hash_table_lookup(mp->track, key); |
| if (g_strcmp0(curval, value) == 0) { |
| g_free(value); |
| return; |
| } |
| |
| if (mp->process_id == 0) { |
| g_hash_table_remove_all(mp->track); |
| mp->process_id = g_idle_add(process_metadata_changed, mp); |
| } |
| |
| g_hash_table_replace(mp->track, g_strdup(key), value); |
| } |
| |
| void media_player_set_type(struct media_player *mp, const char *type) |
| { |
| if (g_strcmp0(mp->type, type) == 0) |
| return; |
| |
| DBG("%s", type); |
| |
| mp->type = g_strdup(type); |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Type"); |
| } |
| |
| void media_player_set_subtype(struct media_player *mp, const char *subtype) |
| { |
| if (g_strcmp0(mp->subtype, subtype) == 0) |
| return; |
| |
| DBG("%s", subtype); |
| |
| mp->subtype = g_strdup(subtype); |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Subtype"); |
| } |
| |
| void media_player_set_name(struct media_player *mp, const char *name) |
| { |
| if (g_strcmp0(mp->name, name) == 0) |
| return; |
| |
| DBG("%s", name); |
| |
| mp->name = g_strdup(name); |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Name"); |
| } |
| |
| void media_player_set_browsable(struct media_player *mp, bool enabled) |
| { |
| if (mp->browsable == enabled) |
| return; |
| |
| DBG("%s", enabled ? "true" : "false"); |
| |
| mp->browsable = enabled; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Browsable"); |
| } |
| |
| bool media_player_get_browsable(struct media_player *mp) |
| { |
| return mp->browsable; |
| } |
| |
| void media_player_set_searchable(struct media_player *mp, bool enabled) |
| { |
| if (mp->searchable == enabled) |
| return; |
| |
| DBG("%s", enabled ? "true" : "false"); |
| |
| mp->searchable = enabled; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), |
| mp->path, MEDIA_PLAYER_INTERFACE, |
| "Searchable"); |
| } |
| |
| void media_player_set_folder(struct media_player *mp, const char *name, |
| uint32_t number_of_items) |
| { |
| struct media_folder *folder; |
| |
| DBG("%s number of items %u", name, number_of_items); |
| |
| folder = media_player_find_folder(mp, name); |
| if (folder == NULL) { |
| error("Unknown folder: %s", name); |
| return; |
| } |
| |
| folder->number_of_items = number_of_items; |
| |
| media_player_set_scope(mp, folder); |
| } |
| |
| void media_player_set_playlist(struct media_player *mp, const char *name) |
| { |
| struct media_folder *folder; |
| |
| DBG("%s", name); |
| |
| folder = media_player_find_folder(mp, name); |
| if (folder == NULL) { |
| error("Unknown folder: %s", name); |
| return; |
| } |
| |
| mp->playlist = folder; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path, |
| MEDIA_PLAYER_INTERFACE, "Playlist"); |
| } |
| |
| static struct media_item *media_folder_find_item(struct media_folder *folder, |
| uint64_t uid) |
| { |
| GSList *l; |
| |
| if (uid == 0) |
| return NULL; |
| |
| for (l = folder->items; l; l = l->next) { |
| struct media_item *item = l->data; |
| |
| if (item->uid == uid) |
| return item; |
| } |
| |
| return NULL; |
| } |
| |
| static DBusMessage *media_item_play(DBusConnection *conn, DBusMessage *msg, |
| void *data) |
| { |
| struct media_item *item = data; |
| struct media_player *mp = item->player; |
| struct player_callback *cb = mp->cb; |
| const char *path; |
| int err; |
| |
| if (!item->playable || !cb->cbs->play_item) |
| return btd_error_not_supported(msg); |
| |
| path = mp->search && mp->scope == mp->search ? "/Search" : item->path; |
| |
| err = cb->cbs->play_item(mp, path, item->uid, cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static DBusMessage *media_item_add_to_nowplaying(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct media_item *item = data; |
| struct media_player *mp = item->player; |
| struct player_callback *cb = mp->cb; |
| int err; |
| |
| if (!item->playable || !cb->cbs->play_item) |
| return btd_error_not_supported(msg); |
| |
| err = cb->cbs->add_to_nowplaying(mp, item->path, item->uid, |
| cb->user_data); |
| if (err < 0) |
| return btd_error_failed(msg, strerror(-err)); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static gboolean get_player(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_item *item = data; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH, |
| &item->player->path); |
| |
| return TRUE; |
| } |
| |
| static gboolean item_name_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct media_item *item = data; |
| |
| return item->name != NULL; |
| } |
| |
| static gboolean get_item_name(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_item *item = data; |
| |
| if (item->name == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &item->name); |
| |
| return TRUE; |
| } |
| |
| static const char *type_to_string(uint8_t type) |
| { |
| switch (type) { |
| case PLAYER_ITEM_TYPE_AUDIO: |
| return "audio"; |
| case PLAYER_ITEM_TYPE_VIDEO: |
| return "video"; |
| case PLAYER_ITEM_TYPE_FOLDER: |
| return "folder"; |
| } |
| |
| return NULL; |
| } |
| |
| static gboolean get_item_type(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_item *item = data; |
| const char *string; |
| |
| string = type_to_string(item->type); |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string); |
| |
| return TRUE; |
| } |
| |
| static gboolean get_playable(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_item *item = data; |
| dbus_bool_t value; |
| |
| value = item->playable; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value); |
| |
| return TRUE; |
| } |
| |
| static const char *folder_type_to_string(uint8_t type) |
| { |
| switch (type) { |
| case PLAYER_FOLDER_TYPE_MIXED: |
| return "mixed"; |
| case PLAYER_FOLDER_TYPE_TITLES: |
| return "titles"; |
| case PLAYER_FOLDER_TYPE_ALBUMS: |
| return "albums"; |
| case PLAYER_FOLDER_TYPE_ARTISTS: |
| return "artists"; |
| case PLAYER_FOLDER_TYPE_GENRES: |
| return "genres"; |
| case PLAYER_FOLDER_TYPE_PLAYLISTS: |
| return "playlists"; |
| case PLAYER_FOLDER_TYPE_YEARS: |
| return "years"; |
| } |
| |
| return NULL; |
| } |
| |
| static gboolean folder_type_exists(const GDBusPropertyTable *property, |
| void *data) |
| { |
| struct media_item *item = data; |
| |
| return folder_type_to_string(item->folder_type) != NULL; |
| } |
| |
| static gboolean get_folder_type(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_item *item = data; |
| const char *string; |
| |
| string = folder_type_to_string(item->folder_type); |
| if (string == NULL) |
| return FALSE; |
| |
| dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &string); |
| |
| return TRUE; |
| } |
| |
| static gboolean metadata_exists(const GDBusPropertyTable *property, void *data) |
| { |
| struct media_item *item = data; |
| |
| return item->metadata != NULL; |
| } |
| |
| static void append_metadata(void *key, void *value, void *user_data) |
| { |
| DBusMessageIter *dict = user_data; |
| const char *strkey = key; |
| |
| if (strcasecmp(strkey, "Item") == 0) |
| return; |
| |
| if (strcasecmp(strkey, "Duration") == 0 || |
| strcasecmp(strkey, "TrackNumber") == 0 || |
| strcasecmp(strkey, "NumberOfTracks") == 0) { |
| uint32_t num = atoi(value); |
| dict_append_entry(dict, key, DBUS_TYPE_UINT32, &num); |
| } else { |
| dict_append_entry(dict, key, DBUS_TYPE_STRING, &value); |
| } |
| } |
| |
| static gboolean get_metadata(const GDBusPropertyTable *property, |
| DBusMessageIter *iter, void *data) |
| { |
| struct media_item *item = data; |
| DBusMessageIter dict; |
| |
| dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING |
| DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, |
| &dict); |
| |
| if (g_hash_table_size(item->metadata) > 0) |
| g_hash_table_foreach(item->metadata, append_metadata, &dict); |
| else if (item->name != NULL) |
| dict_append_entry(&dict, "Title", DBUS_TYPE_STRING, |
| &item->name); |
| |
| dbus_message_iter_close_container(iter, &dict); |
| |
| return TRUE; |
| } |
| |
| static const GDBusMethodTable media_item_methods[] = { |
| { GDBUS_METHOD("Play", NULL, NULL, media_item_play) }, |
| { GDBUS_METHOD("AddtoNowPlaying", NULL, NULL, |
| media_item_add_to_nowplaying) }, |
| { } |
| }; |
| |
| static const GDBusPropertyTable media_item_properties[] = { |
| { "Player", "o", get_player, NULL, NULL }, |
| { "Name", "s", get_item_name, NULL, item_name_exists }, |
| { "Type", "s", get_item_type, NULL, NULL }, |
| { "FolderType", "s", get_folder_type, NULL, folder_type_exists }, |
| { "Playable", "b", get_playable, NULL, NULL }, |
| { "Metadata", "a{sv}", get_metadata, NULL, metadata_exists }, |
| { } |
| }; |
| |
| void media_item_set_playable(struct media_item *item, bool value) |
| { |
| if (item->playable == value) |
| return; |
| |
| item->playable = value; |
| |
| g_dbus_emit_property_changed(btd_get_dbus_connection(), item->path, |
| MEDIA_ITEM_INTERFACE, "Playable"); |
| } |
| |
| static struct media_item *media_folder_create_item(struct media_player *mp, |
| struct media_folder *folder, |
| const char *name, |
| player_item_type_t type, |
| uint64_t uid) |
| { |
| struct media_item *item; |
| const char *strtype; |
| |
| item = media_folder_find_item(folder, uid); |
| if (item != NULL) |
| return item; |
| |
| strtype = type_to_string(type); |
| if (strtype == NULL) |
| return NULL; |
| |
| DBG("%s type %s uid %" PRIu64 "", name, strtype, uid); |
| |
| item = g_new0(struct media_item, 1); |
| item->player = mp; |
| item->uid = uid; |
| |
| if (uid > 0) |
| item->path = g_strdup_printf("%s/item%" PRIu64 "", |
| folder->item->path, uid); |
| else |
| item->path = g_strdup_printf("%s%s", mp->path, name); |
| |
| item->name = g_strdup(name); |
| item->type = type; |
| item->folder_type = PLAYER_FOLDER_TYPE_INVALID; |
| |
| if (!g_dbus_register_interface(btd_get_dbus_connection(), |
| item->path, MEDIA_ITEM_INTERFACE, |
| media_item_methods, |
| NULL, |
| media_item_properties, item, NULL)) { |
| error("D-Bus failed to register %s on %s path", |
| MEDIA_ITEM_INTERFACE, item->path); |
| media_item_free(item); |
| return NULL; |
| } |
| |
| if (type != PLAYER_ITEM_TYPE_FOLDER) { |
| folder->items = g_slist_prepend(folder->items, item); |
| item->metadata = g_hash_table_new_full(g_str_hash, g_str_equal, |
| g_free, g_free); |
| } |
| |
| DBG("%s", item->path); |
| |
| return item; |
| } |
| |
| struct media_item *media_player_create_item(struct media_player *mp, |
| const char *name, |
| player_item_type_t type, |
| uint64_t uid) |
| { |
| return media_folder_create_item(mp, mp->scope, name, type, uid); |
| } |
| |
| static struct media_folder * |
| media_player_find_folder_by_uid(struct media_player *mp, uint64_t uid) |
| { |
| struct media_folder *folder = mp->scope; |
| GSList *l; |
| |
| for (l = folder->subfolders; l; l = l->next) { |
| struct media_folder *folder = l->data; |
| |
| if (folder->item->uid == uid) |
| return folder; |
| } |
| |
| return NULL; |
| } |
| |
| struct media_item *media_player_create_folder(struct media_player *mp, |
| const char *name, |
| player_folder_type_t type, |
| uint64_t uid) |
| { |
| struct media_folder *folder; |
| struct media_item *item; |
| |
| if (uid > 0) |
| folder = media_player_find_folder_by_uid(mp, uid); |
| else |
| folder = media_player_find_folder(mp, name); |
| |
| if (folder != NULL) |
| return folder->item; |
| |
| if (uid > 0) |
| item = media_player_create_subfolder(mp, name, uid); |
| else |
| item = media_player_create_item(mp, name, |
| PLAYER_ITEM_TYPE_FOLDER, uid); |
| |
| if (item == NULL) |
| return NULL; |
| |
| folder = g_new0(struct media_folder, 1); |
| folder->item = item; |
| |
| item->folder_type = type; |
| |
| if (mp->folder != NULL) |
| goto done; |
| |
| mp->folder = folder; |
| |
| done: |
| if (uid > 0) { |
| folder->parent = mp->folder; |
| mp->folder->subfolders = g_slist_prepend( |
| mp->folder->subfolders, |
| folder); |
| } else |
| mp->folders = g_slist_prepend(mp->folders, folder); |
| |
| return item; |
| } |
| |
| void media_player_set_callbacks(struct media_player *mp, |
| const struct media_player_callback *cbs, |
| void *user_data) |
| { |
| struct player_callback *cb; |
| |
| if (mp->cb) |
| g_free(mp->cb); |
| |
| cb = g_new0(struct player_callback, 1); |
| cb->cbs = cbs; |
| cb->user_data = user_data; |
| |
| mp->cb = cb; |
| } |
| |
| struct media_item *media_player_set_playlist_item(struct media_player *mp, |
| uint64_t uid) |
| { |
| struct media_folder *folder = mp->playlist; |
| struct media_item *item; |
| |
| DBG("%" PRIu64 "", uid); |
| |
| if (folder == NULL || uid == 0) |
| return NULL; |
| |
| item = media_folder_create_item(mp, folder, NULL, |
| PLAYER_ITEM_TYPE_AUDIO, uid); |
| if (item == NULL) |
| return NULL; |
| |
| media_item_set_playable(item, true); |
| |
| if (mp->track != item->metadata) { |
| g_hash_table_unref(mp->track); |
| mp->track = g_hash_table_ref(item->metadata); |
| } |
| |
| if (item == g_hash_table_lookup(mp->track, "Item")) |
| return item; |
| |
| if (mp->process_id == 0) { |
| g_hash_table_remove_all(mp->track); |
| mp->process_id = g_idle_add(process_metadata_changed, mp); |
| } |
| |
| g_hash_table_replace(mp->track, g_strdup("Item"), |
| g_strdup(item->path)); |
| |
| return item; |
| } |