1899 lines
45 KiB
C
1899 lines
45 KiB
C
/*
|
|
*
|
|
* 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;
|
|
|
|
DBG("%s", mp->status);
|
|
|
|
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;
|
|
|
|
DBG("%s %s", property->name, value);
|
|
|
|
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;
|
|
|
|
DBG("%s", mp->name);
|
|
|
|
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;
|
|
|
|
DBG("%s", mp->type);
|
|
|
|
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;
|
|
|
|
DBG("%s", mp->subtype);
|
|
|
|
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;
|
|
|
|
DBG("%s", mp->browsable ? "true" : "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;
|
|
|
|
DBG("%s", mp->searchable ? "true" : "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;
|
|
}
|
|
|
|
static const GDBusMethodTable media_player_methods[] = {
|
|
{ GDBUS_EXPERIMENTAL_METHOD("Play", NULL, NULL, media_player_play) },
|
|
{ GDBUS_EXPERIMENTAL_METHOD("Pause", NULL, NULL, media_player_pause) },
|
|
{ GDBUS_EXPERIMENTAL_METHOD("Stop", NULL, NULL, media_player_stop) },
|
|
{ GDBUS_EXPERIMENTAL_METHOD("Next", NULL, NULL, media_player_next) },
|
|
{ GDBUS_EXPERIMENTAL_METHOD("Previous", NULL, NULL,
|
|
media_player_previous) },
|
|
{ GDBUS_EXPERIMENTAL_METHOD("FastForward", NULL, NULL,
|
|
media_player_fast_forward) },
|
|
{ GDBUS_EXPERIMENTAL_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,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Type", "s", get_type, NULL, type_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Subtype", "s", get_subtype, NULL, subtype_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Position", "u", get_position, NULL, NULL,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Status", "s", get_status, NULL, status_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Equalizer", "s", get_setting, set_setting, setting_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Repeat", "s", get_setting, set_setting, setting_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Shuffle", "s", get_setting, set_setting, setting_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Scan", "s", get_setting, set_setting, setting_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Track", "a{sv}", get_track, NULL, track_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Device", "o", get_device, NULL, NULL,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Browsable", "b", get_browsable, NULL, browsable_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Searchable", "b", get_searchable, NULL, searchable_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Playlist", "o", get_playlist, NULL, playlist_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ }
|
|
};
|
|
|
|
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)
|
|
{
|
|
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;
|
|
|
|
g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
|
|
MEDIA_FOLDER_INTERFACE, "Name");
|
|
g_dbus_emit_property_changed(btd_get_dbus_connection(), mp->path,
|
|
MEDIA_FOLDER_INTERFACE, "NumberOfItems");
|
|
}
|
|
|
|
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_EXPERIMENTAL_ASYNC_METHOD("Search",
|
|
GDBUS_ARGS({ "string", "s" }, { "filter", "a{sv}" }),
|
|
GDBUS_ARGS({ "folder", "o" }),
|
|
media_folder_search) },
|
|
{ GDBUS_EXPERIMENTAL_ASYNC_METHOD("ListItems",
|
|
GDBUS_ARGS({ "filter", "a{sv}" }),
|
|
GDBUS_ARGS({ "items", "a{oa{sv}}" }),
|
|
media_folder_list_items) },
|
|
{ GDBUS_EXPERIMENTAL_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,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "NumberOfItems", "u", get_items, NULL, items_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ }
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
void media_player_set_searchable(struct media_player *mp, bool enabled)
|
|
{
|
|
if (mp->browsable == 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;
|
|
int err;
|
|
|
|
if (!item->playable || !cb->cbs->play_item)
|
|
return btd_error_not_supported(msg);
|
|
|
|
err = cb->cbs->play_item(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 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;
|
|
|
|
DBG("%s", item->player->path);
|
|
|
|
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;
|
|
|
|
DBG("%s", item->name);
|
|
|
|
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);
|
|
|
|
DBG("%s", string);
|
|
|
|
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;
|
|
|
|
DBG("%s", item->playable ? "true" : "false");
|
|
|
|
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;
|
|
|
|
DBG("%s", string);
|
|
|
|
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_EXPERIMENTAL_METHOD("Play", NULL, NULL,
|
|
media_item_play) },
|
|
{ GDBUS_EXPERIMENTAL_METHOD("AddtoNowPlaying", NULL, NULL,
|
|
media_item_add_to_nowplaying) },
|
|
{ }
|
|
};
|
|
|
|
static const GDBusPropertyTable media_item_properties[] = {
|
|
{ "Player", "o", get_player, NULL, NULL,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Name", "s", get_item_name, NULL, item_name_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Type", "s", get_item_type, NULL, NULL,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "FolderType", "s", get_folder_type, NULL, folder_type_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Playable", "b", get_playable, NULL, NULL,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ "Metadata", "a{sv}", get_metadata, NULL, metadata_exists,
|
|
G_DBUS_PROPERTY_FLAG_EXPERIMENTAL },
|
|
{ }
|
|
};
|
|
|
|
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;
|
|
}
|