/* XMMS2 - X Music Multiplexer System * Copyright (C) 2003-2007 XMMS2 Team * * PLUGINS ARE NOT CONSIDERED TO BE DERIVED WORK !!! * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. */ #include "xmms_configuration.h" #include "xmmspriv/xmms_medialib.h" #include "xmmspriv/xmms_xform.h" #include "xmmspriv/xmms_utils.h" #include "xmms/xmms_error.h" #include "xmms/xmms_config.h" #include "xmms/xmms_object.h" #include "xmms/xmms_ipc.h" #include "xmms/xmms_log.h" #include #include #include #include #include /** * @fil * Medialib is a metainfo cache that is searchable. */ static GList *xmms_medialib_select_method (xmms_medialib_t *, gchar *, xmms_error_t *); static void xmms_medialib_entry_remove_method (xmms_medialib_t *medialib, guint32 entry, xmms_error_t *error); static gboolean xmms_medialib_int_cb (xmms_object_cmd_value_t **row, gpointer udata); gchar *xmms_medialib_url_encode (const gchar *path); static void xmms_medialib_add_entry (xmms_medialib_t *, gchar *, xmms_error_t *); static void xmms_medialib_path_import (xmms_medialib_t *medialib, gchar *path, xmms_error_t *error); static void xmms_medialib_rehash (xmms_medialib_t *medialib, guint32 id, xmms_error_t *error); static void xmms_medialib_property_set_str_method (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, gchar *value, xmms_error_t *error); static void xmms_medialib_property_set_int_method (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, gint32 value, xmms_error_t *error); static void xmms_medialib_property_remove_method (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, xmms_error_t *error); static guint32 xmms_medialib_entry_get_id (xmms_medialib_t *medialib, gchar *url, xmms_error_t *error); XMMS_CMD_DEFINE (select, xmms_medialib_select_method, xmms_medialib_t *, LIST, STRING, NONE); XMMS_CMD_DEFINE (info, xmms_medialib_info, xmms_medialib_t *, PROPDICT, UINT32, NONE); XMMS_CMD_DEFINE (mlib_add, xmms_medialib_add_entry, xmms_medialib_t *, NONE, STRING, NONE); XMMS_CMD_DEFINE (mlib_remove, xmms_medialib_entry_remove_method, xmms_medialib_t *, NONE, UINT32, NONE); XMMS_CMD_DEFINE (path_import, xmms_medialib_path_import, xmms_medialib_t *, NONE, STRING, NONE); XMMS_CMD_DEFINE (rehash, xmms_medialib_rehash, xmms_medialib_t *, NONE, UINT32, NONE); XMMS_CMD_DEFINE (get_id, xmms_medialib_entry_get_id, xmms_medialib_t *, UINT32, STRING, NONE); XMMS_CMD_DEFINE4 (set_property_str, xmms_medialib_property_set_str_method, xmms_medialib_t *, NONE, UINT32, STRING, STRING, STRING); XMMS_CMD_DEFINE4 (set_property_int, xmms_medialib_property_set_int_method, xmms_medialib_t *, NONE, UINT32, STRING, STRING, INT32); XMMS_CMD_DEFINE3 (remove_property, xmms_medialib_property_remove_method, xmms_medialib_t *, NONE, UINT32, STRING, STRING); /** * * @defgroup Medialib Medialib * @ingroup XMMSServer * @brief Medialib caches metadata * * Controls metadata storage. * * @{ */ /** * Medialib structure */ struct xmms_medialib_St { xmms_object_t object; /** The current playlist */ xmms_playlist_t *playlist; GMutex *source_lock; GHashTable *sources; }; /** * This is handed out by xmms_medialib_begin() */ struct xmms_medialib_session_St { xmms_medialib_t *medialib; /** The SQLite handler */ sqlite3 *sql; /** The source to be working with */ gchar *source_pref; /** debug file */ const gchar *file; /** debug line number */ gint line; /* Write or read lock, true if write */ gboolean write; }; /** * Ok, so the functions are written with reentrency in mind, but * we choose to have a global medialib object here. It will be * much easier, and I don't see the real use of multiple medialibs * right now. This could be changed by removing this global one * and altering the function callers... */ static xmms_medialib_t *medialib; /** * This is only used if we are using a older version of sqlite. * The reason for this is that we must have a global session, due to some * strange limitiations in older sqlite libraries. */ static xmms_medialib_session_t *global_medialib_session; /** This protects the above global session */ static GMutex *global_medialib_session_mutex; static GMutex *xmms_medialib_debug_mutex; static GHashTable *xmms_medialib_debug_hash; #define destroy_array(a) { gint i = 0; while (a[i]) { xmms_object_cmd_value_free (a[i]); i++; }; g_free (a); } static void xmms_medialib_destroy (xmms_object_t *object) { xmms_medialib_t *mlib = (xmms_medialib_t *)object; if (global_medialib_session) { xmms_sqlite_close (global_medialib_session->sql); g_free (global_medialib_session); } g_mutex_free (mlib->source_lock); g_hash_table_destroy (mlib->sources); g_mutex_free (global_medialib_session_mutex); xmms_ipc_broadcast_unregister (XMMS_IPC_SIGNAL_MEDIALIB_ENTRY_UPDATE); xmms_ipc_object_unregister (XMMS_IPC_OBJECT_OUTPUT); } static void xmms_medialib_path_changed (xmms_object_t *object, gconstpointer data, gpointer userdata) { /*gboolean c; xmms_medialib_t *mlib = userdata; g_mutex_lock (mlib->mutex); xmms_sqlite_close (mlib->sql); medialib->sql = xmms_sqlite_open (&medialib->nextid, &c); g_mutex_unlock (mlib->mutex);*/ } #define XMMS_MEDIALIB_SOURCE_SERVER "server" #define XMMS_MEDIALIB_SOURCE_SERVER_ID 1 static gint source_match_pattern (gchar* source, gchar* pattern) { gboolean match = FALSE; gint lpos = strlen (pattern) - 1; if (g_strcasecmp (pattern, source) == 0) { match = TRUE; } else if (lpos >= 0 && pattern[lpos] == '*' && (lpos == 0 || g_strncasecmp (source, pattern, lpos) == 0)) { match = TRUE; } return match; } static void xmms_sqlite_source_pref (sqlite3_context *context, int args, sqlite3_value **val) { gint source; const guchar *pref; gchar **split; xmms_medialib_t *mlib; gchar *source_name; gint i; mlib = sqlite3_user_data (context); if (sqlite3_value_type (val[0]) != SQLITE_INTEGER) { sqlite3_result_error (context, "First argument to xmms_source_pref should be a integer", -1); return; } if (sqlite3_value_type (val[1]) != SQLITE3_TEXT) { sqlite3_result_error (context, "Second argument to xmms_source_pref should be a string", -1); return; } source = sqlite3_value_int (val[0]); pref = sqlite3_value_text (val[1]); g_mutex_lock (mlib->source_lock); source_name = g_hash_table_lookup (mlib->sources, (gpointer)source); g_mutex_unlock (mlib->source_lock); split = g_strsplit ((gchar *)pref, ":", 0); for (i = 0; split[i]; i++) { if (source_match_pattern (source_name, split[i])) { sqlite3_result_int (context, i); g_strfreev (split); return; } } g_strfreev (split); sqlite3_result_int (context, -1); } int add_to_source (void *hash, int columns, char **vals, char **cols) { int source = strtol (vals[0], NULL, 10); g_hash_table_insert ((GHashTable*)hash, (gpointer)source, g_strdup (vals[1])); return 0; } guint32 xmms_medialib_source_to_id (xmms_medialib_session_t *session, gchar *source) { guint32 ret = 0; g_return_val_if_fail (source, 0); xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, "select id from Sources where source=%Q", source); if (ret == 0) { xmms_sqlite_exec (session->sql, "insert into Sources (source) values (%Q)", source); xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, "select id from Sources where source=%Q", source); XMMS_DBG ("Added source %s with id %d", source, ret); g_mutex_lock (session->medialib->source_lock); g_hash_table_insert (session->medialib->sources, GUINT_TO_POINTER (ret), g_strdup (source)); g_mutex_unlock (session->medialib->source_lock); } return ret; } static xmms_medialib_session_t * xmms_medialib_session_new (const char *file, int line) { xmms_medialib_session_t *session; gint create; session = g_new0 (xmms_medialib_session_t, 1); session->medialib = medialib; session->file = file; session->line = line; session->sql = xmms_sqlite_open (&create); session->source_pref = g_strdup ("server:client/*:plugin/id3v2:plugin/*"); sqlite3_create_function (session->sql, "xmms_source_pref", 2, SQLITE_UTF8, session->medialib, xmms_sqlite_source_pref, NULL, NULL); if (create) { xmms_medialib_entry_t entry; xmms_error_t error; entry = xmms_medialib_entry_new (session, "file://" SHAREDDIR "/mind.in.a.box-lament_snipplet.ogg", &error); /* A default playlist containing that song has been created with the mlib */ } return session; } /** * Initialize the medialib and open the database file. * * @param playlist the current playlist pointer * @returns TRUE if successful and FALSE if there was a problem */ xmms_medialib_t * xmms_medialib_init (xmms_playlist_t *playlist) { gchar *path; xmms_medialib_session_t *session; medialib = xmms_object_new (xmms_medialib_t, xmms_medialib_destroy); medialib->playlist = playlist; xmms_ipc_object_register (XMMS_IPC_OBJECT_MEDIALIB, XMMS_OBJECT (medialib)); xmms_ipc_broadcast_register (XMMS_OBJECT (medialib), XMMS_IPC_SIGNAL_MEDIALIB_ENTRY_ADDED); xmms_ipc_broadcast_register (XMMS_OBJECT (medialib), XMMS_IPC_SIGNAL_MEDIALIB_ENTRY_UPDATE); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_SELECT, XMMS_CMD_FUNC (select)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_INFO, XMMS_CMD_FUNC (info)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_ADD_URL, XMMS_CMD_FUNC (mlib_add)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_REMOVE_ID, XMMS_CMD_FUNC (mlib_remove)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_PATH_IMPORT, XMMS_CMD_FUNC (path_import)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_REHASH, XMMS_CMD_FUNC (rehash)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_GET_ID, XMMS_CMD_FUNC (get_id)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_PROPERTY_SET_STR, XMMS_CMD_FUNC (set_property_str)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_PROPERTY_SET_INT, XMMS_CMD_FUNC (set_property_int)); xmms_object_cmd_add (XMMS_OBJECT (medialib), XMMS_IPC_CMD_PROPERTY_REMOVE, XMMS_CMD_FUNC (remove_property)); path = XMMS_BUILD_PATH ("medialib.db"); xmms_config_property_register ("medialib.path", path, xmms_medialib_path_changed, medialib); xmms_config_property_register ("medialib.allow_remote_fs", "0", NULL, NULL); g_free (path); xmms_medialib_debug_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free); xmms_medialib_debug_mutex = g_mutex_new (); global_medialib_session = NULL; if (sqlite3_libversion_number () < 3002004) { xmms_log_info ("**************************************************************"); xmms_log_info ("* Using thread hack to compensate for old sqlite version!"); xmms_log_info ("* This can be a huge performance penalty - consider upgrading"); xmms_log_info ("**************************************************************"); /** Create a global session, this is only used when the sqlite version * doesn't support concurrent sessions */ global_medialib_session = xmms_medialib_session_new ("global", 0); } global_medialib_session_mutex = g_mutex_new (); /** * this dummy just wants to put the default song in the playlist */ medialib->source_lock = g_mutex_new (); medialib->sources = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free); session = xmms_medialib_begin_write (); sqlite3_exec (session->sql, "select id, source from Sources", add_to_source, medialib->sources, NULL); xmms_medialib_end (session); return medialib; } /** Session handling */ xmms_medialib_session_t * _xmms_medialib_begin (gboolean write, const char *file, int line) { xmms_medialib_session_t *session; { gchar *r; void *me = g_thread_self (); g_mutex_lock (xmms_medialib_debug_mutex); r = g_hash_table_lookup (xmms_medialib_debug_hash, me); if (r) { xmms_log_fatal ("Medialib session begun recursivly at %s:%d, after %s", file, line, r); } else { g_hash_table_insert (xmms_medialib_debug_hash, me, g_strdup_printf ("%s:%d", file, line)); } g_mutex_unlock (xmms_medialib_debug_mutex); } if (global_medialib_session) { /** This will only happen when OLD_SQLITE_VERSION is set. */ g_mutex_lock (global_medialib_session_mutex); return global_medialib_session; } session = xmms_medialib_session_new (file, line); xmms_object_ref (XMMS_OBJECT (medialib)); session->write = write; if (write) { /* Start a exclusive transaction */ if (!xmms_sqlite_exec (session->sql, "BEGIN EXCLUSIVE TRANSACTION")) { xmms_log_error ("transaction failed!"); } } return session; } void xmms_medialib_end (xmms_medialib_session_t *session) { g_return_if_fail (session); { void *me = g_thread_self (); g_mutex_lock (xmms_medialib_debug_mutex); g_hash_table_remove (xmms_medialib_debug_hash, me); g_mutex_unlock (xmms_medialib_debug_mutex); } if (session->write) { xmms_sqlite_exec (session->sql, "COMMIT"); } if (session == global_medialib_session) { g_mutex_unlock (global_medialib_session_mutex); return; } g_free (session->source_pref); xmms_sqlite_close (session->sql); xmms_object_unref (XMMS_OBJECT (session->medialib)); g_free (session); } static gboolean xmms_medialib_int_cb (xmms_object_cmd_value_t **row, gpointer udata) { guint *i = udata; if (row && row[0] && row[0]->type == XMMS_OBJECT_CMD_ARG_INT32) *i = row[0]->value.int32; else XMMS_DBG ("Expected int32 but got something else!"); destroy_array (row); return TRUE; } static int xmms_medialib_string_cb (xmms_object_cmd_value_t **row, gpointer udata) { gchar **str = udata; if (row && row[0] && row[0]->type == XMMS_OBJECT_CMD_ARG_STRING) *str = g_strdup (row[0]->value.string); else XMMS_DBG ("Expected string but got something else!"); destroy_array (row); return 0; } static int xmms_medialib_cmd_value_cb (xmms_object_cmd_value_t **row, gpointer udata) { xmms_object_cmd_value_t **ret = udata; *ret = xmms_object_cmd_value_copy (row[0]); destroy_array (row); return 0; } /** * Retrieve a property from an entry * * @see xmms_medialib_entry_property_get_str */ #define XMMS_MEDIALIB_RETRV_PROPERTY_SQL "select value from Media where key=%Q and id=%d order by xmms_source_pref(source, %Q) limit 1" xmms_object_cmd_value_t * xmms_medialib_entry_property_get_cmd_value (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property) { xmms_object_cmd_value_t *ret = NULL; g_return_val_if_fail (property, NULL); g_return_val_if_fail (session, NULL); if (!strcmp (property, XMMS_MEDIALIB_ENTRY_PROPERTY_ID)) { ret = xmms_object_cmd_value_int_new (entry); } else { xmms_sqlite_query_array (session->sql, xmms_medialib_cmd_value_cb, &ret, XMMS_MEDIALIB_RETRV_PROPERTY_SQL, property, entry, session->source_pref); } return ret; } /** * Retrieve a property from an entry. * * @param session The medialib session to be used for the transaction. * @param entry Entry to query. * @param property The property to extract. Strings passed should * be defined in medialib.h * * @returns Newly allocated gchar that needs to be freed with g_free */ gchar * xmms_medialib_entry_property_get_str (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property) { gchar *ret = NULL; g_return_val_if_fail (property, NULL); g_return_val_if_fail (session, NULL); xmms_sqlite_query_array (session->sql, xmms_medialib_string_cb, &ret, XMMS_MEDIALIB_RETRV_PROPERTY_SQL, property, entry, session->source_pref); return ret; } /** * Retrieve a property as a int from a entry. * * @param session The medialib session to be used for the transaction. * @param entry Entry to query. * @param property The property to extract. Strings passed should * be defined in medialib.h * * @returns Property as integer, or -1 if it doesn't exist. */ gint xmms_medialib_entry_property_get_int (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property) { gint ret = -1; g_return_val_if_fail (property, -1); g_return_val_if_fail (session, -1); xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, XMMS_MEDIALIB_RETRV_PROPERTY_SQL, property, entry, session->source_pref); return ret; } /** * Set a entry property to a new value, overwriting the old value. * * @param session The medialib session to be used for the transaction. * @param entry Entry to alter. * @param property The property to extract. Strings passed should * be defined in medialib.h * @param value gint with the new value, will be copied in to the medialib * * @returns TRUE on success and FALSE on failure. */ gboolean xmms_medialib_entry_property_set_int (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property, gint value) { return xmms_medialib_entry_property_set_int_source (session, entry, property, value, XMMS_MEDIALIB_SOURCE_SERVER_ID); } gboolean xmms_medialib_entry_property_set_int_source (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property, gint value, guint32 source) { gboolean ret; g_return_val_if_fail (property, FALSE); g_return_val_if_fail (session, FALSE); ret = xmms_sqlite_exec (session->sql, "insert or replace into Media (id, value, key, source) values (%d, %d, %Q, %d)", entry, value, property, source); return ret; } /** * Set a entry property to a new value, overwriting the old value. * * @param session The medialib session to be used for the transaction. * @param entry Entry to alter. * @param property The property to extract. Strings passed should * be defined in medialib.h * @param value gchar with the new value, will be copied in to the medialib * * @returns TRUE on success and FALSE on failure. */ gboolean xmms_medialib_entry_property_set_str (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property, const gchar *value) { return xmms_medialib_entry_property_set_str_source (session, entry, property, value, XMMS_MEDIALIB_SOURCE_SERVER_ID); } gboolean xmms_medialib_entry_property_set_str_source (xmms_medialib_session_t *session, xmms_medialib_entry_t entry, const gchar *property, const gchar *value, guint32 source) { gboolean ret; g_return_val_if_fail (property, FALSE); g_return_val_if_fail (session, FALSE); if (value && !g_utf8_validate (value, -1, NULL)) { XMMS_DBG ("OOOOOPS! Trying to set property %s to a NON UTF-8 string (%s) I will deny that!", property, value); return FALSE; } ret = xmms_sqlite_exec (session->sql, "insert or replace into Media (id, value, key, source) values (%d, %Q, %Q, %d)", entry, value, property, source); return ret; } /** * Trigger a update signal to the client. This should be called * when important information in the entry has been changed and * should be visible to the user. * * @param entry Entry to signal a update for. */ void xmms_medialib_entry_send_update (xmms_medialib_entry_t entry) { xmms_object_emit_f (XMMS_OBJECT (medialib), XMMS_IPC_SIGNAL_MEDIALIB_ENTRY_UPDATE, XMMS_OBJECT_CMD_ARG_UINT32, entry); } /** * Trigger an added siginal to the client. This should be * called when a new entry has been added to the medialib * * @param entry Entry to siginal an add for. */ void xmms_medialib_entry_send_added (xmms_medialib_entry_t entry) { xmms_object_emit_f (XMMS_OBJECT (medialib), XMMS_IPC_SIGNAL_MEDIALIB_ENTRY_ADDED, XMMS_OBJECT_CMD_ARG_UINT32, entry); } static void xmms_medialib_entry_remove_method (xmms_medialib_t *medialib, guint32 entry, xmms_error_t *error) { xmms_medialib_entry_remove (entry); } /** * Remove a medialib entry from the database * * @param session The medialib session to be used for the transaction. * @param entry Entry to remove */ void xmms_medialib_entry_remove (xmms_medialib_entry_t entry) { xmms_medialib_session_t *session; session = xmms_medialib_begin_write (); xmms_sqlite_exec (session->sql, "delete from Media where id=%d", entry); xmms_medialib_end (session); /** @todo safe ? */ xmms_playlist_remove_by_entry (medialib->playlist, entry); } static xmms_medialib_entry_t xmms_medialib_entry_new_insert (xmms_medialib_session_t *session, guint32 id, const char *url, xmms_error_t *error); static void process_file (xmms_medialib_session_t *session, gchar *playlist, const gchar *path, guint32 *id, xmms_error_t *error) { guint32 ret = 0; xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, "select id as value from Media where " "key='%s' and value=%Q and source=%d", XMMS_MEDIALIB_ENTRY_PROPERTY_URL, path, XMMS_MEDIALIB_SOURCE_SERVER_ID); if (!ret) { *id = *id + 1; /* increment id */ xmms_medialib_entry_new_insert (session, *id, path, error); if (playlist != NULL) { xmms_playlist_add_entry (session->medialib->playlist, playlist, *id, error); } } else if (playlist != NULL) { xmms_playlist_add_entry (session->medialib->playlist, playlist, ret, error); } } static gchar * lookup_string (xmms_object_cmd_value_t *tbl, gchar *key) { xmms_object_cmd_value_t *val; if (!tbl || tbl->type != XMMS_OBJECT_CMD_ARG_DICT) return NULL; val = g_hash_table_lookup (tbl->value.dict, key); if (!val) return NULL; if (val->type != XMMS_OBJECT_CMD_ARG_STRING) return NULL; return g_strdup (val->value.string); } static gint32 lookup_int (xmms_object_cmd_value_t *tbl, gchar *key) { xmms_object_cmd_value_t *val; if (!tbl || tbl->type != XMMS_OBJECT_CMD_ARG_DICT) return 0; val = g_hash_table_lookup (tbl->value.dict, key); if (!val) return 0; if (val->type != XMMS_OBJECT_CMD_ARG_INT32) return 0; return val->value.int32; } static gint cmp_val (gconstpointer a, gconstpointer b) { xmms_object_cmd_value_t *v1, *v2; v1 = (xmms_object_cmd_value_t *)a; v2 = (xmms_object_cmd_value_t *)b; if (v1->type != XMMS_OBJECT_CMD_ARG_DICT) return 0; if (v2->type != XMMS_OBJECT_CMD_ARG_DICT) return 0; return strcmp (lookup_string (v1, "path"), lookup_string (v2, "path")); } /* code ported over from CLI's "radd" command. */ static gboolean process_dir (const gchar *directory, GList **ret, xmms_error_t *error) { GList *list, *n; list = xmms_xform_browse (NULL, directory, error); if (!list) { return FALSE; } list = g_list_sort (list, cmp_val); for (n = list; n; n = g_list_next (n)) { xmms_object_cmd_value_t *val = n->data; if (lookup_int (val, "isdir") == 1) { process_dir (lookup_string (val, "path"), ret, error); } else { *ret = g_list_prepend (*ret, lookup_string (val, "path")); } xmms_object_cmd_value_free (val); } g_list_free (list); return TRUE; } void xmms_medialib_entry_cleanup (xmms_medialib_session_t *session, xmms_medialib_entry_t entry) { xmms_sqlite_exec (session->sql, "delete from Media where id=%d and source=%d " "and key not in (%Q, %Q, %Q, %Q, %Q)", entry, XMMS_MEDIALIB_SOURCE_SERVER_ID, XMMS_MEDIALIB_ENTRY_PROPERTY_URL, XMMS_MEDIALIB_ENTRY_PROPERTY_ADDED, XMMS_MEDIALIB_ENTRY_PROPERTY_STATUS, XMMS_MEDIALIB_ENTRY_PROPERTY_LMOD, XMMS_MEDIALIB_ENTRY_PROPERTY_LASTSTARTED); xmms_sqlite_exec (session->sql, "delete from Media where id=%d and source in " "(select id from Sources where source like 'plugin/%%' and source != 'plugin/playlist')", entry); } static void xmms_medialib_rehash (xmms_medialib_t *medialib, guint32 id, xmms_error_t *error) { xmms_mediainfo_reader_t *mr; xmms_medialib_session_t *session; session = xmms_medialib_begin_write (); if (id) { xmms_sqlite_exec (session->sql, "update Media set value = %d where key='%s' and id=%d", XMMS_MEDIALIB_ENTRY_STATUS_REHASH, XMMS_MEDIALIB_ENTRY_PROPERTY_STATUS, id); } else { xmms_sqlite_exec (session->sql, "update Media set value = %d where key='%s'", XMMS_MEDIALIB_ENTRY_STATUS_REHASH, XMMS_MEDIALIB_ENTRY_PROPERTY_STATUS); } xmms_medialib_end (session); mr = xmms_playlist_mediainfo_reader_get (medialib->playlist); xmms_mediainfo_reader_wakeup (mr); } /* Recursively add entries under the given path to the medialib, * optionally adding them to a playlist if the playlist argument is * not NULL. */ void xmms_medialib_add_recursive (xmms_medialib_t *medialib, gchar *playlist, gchar *path, xmms_error_t *error) { xmms_mediainfo_reader_t *mr; xmms_medialib_session_t *session; guint32 id; GList *list = NULL, *n; g_return_if_fail (medialib); g_return_if_fail (path); process_dir (path, &list, error); XMMS_DBG ("taking the transaction!"); session = xmms_medialib_begin_write (); if (!xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &id, "select ifnull(MAX (id),0) from Media")) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Sql error/corruption selecting max(id)"); return; } list = g_list_reverse (list); for (n = list; n; n = g_list_next (n)) { process_file (session, playlist, n->data, &id, error); g_free (n->data); } g_list_free (list); XMMS_DBG ("and we are done!"); xmms_medialib_end (session); mr = xmms_playlist_mediainfo_reader_get (medialib->playlist); xmms_mediainfo_reader_wakeup (mr); } static void xmms_medialib_path_import (xmms_medialib_t *medialib, gchar *path, xmms_error_t *error) { xmms_medialib_add_recursive (medialib, NULL, path, error); } static xmms_medialib_entry_t xmms_medialib_entry_new_insert (xmms_medialib_session_t *session, guint32 id, const char *url, xmms_error_t *error) { guint source; source = XMMS_MEDIALIB_SOURCE_SERVER_ID; if (!xmms_sqlite_exec (session->sql, "insert into Media (id, key, value, source) values (%d, '%s', %Q, %d)", id, XMMS_MEDIALIB_ENTRY_PROPERTY_URL, url, source)) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Sql error/corruption inserting url"); return 0; } xmms_medialib_entry_status_set (session, id, XMMS_MEDIALIB_ENTRY_STATUS_NEW); return 1; } /** * @internal */ xmms_medialib_entry_t xmms_medialib_entry_new_encoded (xmms_medialib_session_t *session, const char *url, xmms_error_t *error) { guint id = 0; guint ret = 0; guint source; g_return_val_if_fail (url, 0); g_return_val_if_fail (session, 0); source = XMMS_MEDIALIB_SOURCE_SERVER_ID; xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &id, "select id as value from Media where key='%s' and value=%Q and source=%d", XMMS_MEDIALIB_ENTRY_PROPERTY_URL, url, source); if (id) { ret = id; } else { if (!xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, "select ifnull(MAX (id),0) from Media")) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Sql error/corruption selecting max(id)"); return 0; } ret++; /* next id */ if (!xmms_medialib_entry_new_insert (session, ret, url, error)) return 0; } xmms_medialib_entry_send_added (ret); return ret; } /** * Welcome to a function that should be called something else. * Returns a entry for a URL, if the URL is already in the medialib * the current entry will be returned otherwise a new one will be * created and returned. * * @todo rename to something better? * * @param session The medialib session to be used for the transaction. * @param url URL to add/retrieve from the medialib * @param error If an error occurs, it will be stored in there. * * @returns Entry mapped to the URL */ xmms_medialib_entry_t xmms_medialib_entry_new (xmms_medialib_session_t *session, const char *url, xmms_error_t *error) { gchar *enc_url; xmms_medialib_entry_t res; enc_url = xmms_medialib_url_encode (url); if (!enc_url) return 0; res = xmms_medialib_entry_new_encoded (session, enc_url, error); g_free (enc_url); return res; } static guint32 xmms_medialib_entry_get_id (xmms_medialib_t *medialib, gchar *url, xmms_error_t *error) { guint32 id = 0; xmms_medialib_session_t *session = xmms_medialib_begin (); xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &id, "select id as value from Media where key='%s' and value=%Q and source=%d", XMMS_MEDIALIB_ENTRY_PROPERTY_URL, url, XMMS_MEDIALIB_SOURCE_SERVER_ID); xmms_medialib_end (session); return id; } static gboolean xmms_medialib_list_cb (xmms_object_cmd_value_t **row, gpointer udata) { GList **list = (GList**)udata; /* Source */ *list = g_list_append (*list, xmms_object_cmd_value_copy (row[0])); /* Key */ *list = g_list_append (*list, xmms_object_cmd_value_copy (row[1])); /* Value */ *list = g_list_append (*list, xmms_object_cmd_value_copy (row[2])); destroy_array (row); return TRUE; } /** * Convert a entry and all properties to a hashtable that * could be feed to the client or somewhere else in the daemon. * * @param session The medialib session to be used for the transaction. * @param entry Entry to convert. * * @returns Newly allocated hashtable with newly allocated strings * make sure to free them all. */ GList * xmms_medialib_entry_to_list (xmms_medialib_session_t *session, xmms_medialib_entry_t entry) { GList *ret = NULL; gboolean s; g_return_val_if_fail (session, NULL); g_return_val_if_fail (entry, NULL); s = xmms_sqlite_query_array (session->sql, xmms_medialib_list_cb, &ret, "select s.source, m.key, " "m.value from Media m left join " "Sources s on m.source = s.id " "where m.id=%d", entry); if (!s || !ret) { return NULL; } ret = g_list_append (ret, xmms_object_cmd_value_str_new ("server")); ret = g_list_append (ret, xmms_object_cmd_value_str_new ("id")); ret = g_list_append (ret, xmms_object_cmd_value_int_new (entry)); return ret; } GList * xmms_medialib_info (xmms_medialib_t *medialib, guint32 id, xmms_error_t *err) { xmms_medialib_session_t *session; GList *ret = NULL; if (!id) { xmms_error_set (err, XMMS_ERROR_NOENT, "No such entry, 0"); } else { session = xmms_medialib_begin (); ret = xmms_medialib_entry_to_list (session, id); xmms_medialib_end (session); if (!ret) { xmms_error_set (err, XMMS_ERROR_NOENT, "Could not retrive info for that entry!"); } } return ret; } static gboolean select_callback (GHashTable *row, gpointer udata) { GList **l = (GList **) udata; *l = g_list_prepend (*l, xmms_object_cmd_value_dict_new (row)); return TRUE; } /** * Add a entry to the medialib. Calls #xmms_medialib_entry_new and then * wakes up the mediainfo_reader in order to resolve the metadata. * * @param medialib Medialib pointer * @param url URL to add * @param error In case of error this will be filled. */ static void xmms_medialib_add_entry (xmms_medialib_t *medialib, gchar *url, xmms_error_t *error) { xmms_medialib_entry_t entry; xmms_mediainfo_reader_t *mr; xmms_medialib_session_t *session; g_return_if_fail (medialib); g_return_if_fail (url); session = xmms_medialib_begin_write (); entry = xmms_medialib_entry_new_encoded (session, url, error); xmms_medialib_end (session); mr = xmms_playlist_mediainfo_reader_get (medialib->playlist); xmms_mediainfo_reader_wakeup (mr); } static void xmms_medialib_property_set_str_method (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, gchar *value, xmms_error_t *error) { guint32 sourceid; xmms_medialib_session_t *session; if (g_strcasecmp (source, "server") == 0) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Can't write to source server!"); return; } session = xmms_medialib_begin_write (); sourceid = xmms_medialib_source_to_id (session, source); xmms_medialib_entry_property_set_str_source (session, entry, key, value, sourceid); xmms_medialib_end (session); xmms_medialib_entry_send_update (entry); } static void xmms_medialib_property_set_int_method (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, gint32 value, xmms_error_t *error) { guint32 sourceid; xmms_medialib_session_t *session; if (g_strcasecmp (source, "server") == 0) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Can't write to source server!"); return; } session = xmms_medialib_begin_write (); sourceid = xmms_medialib_source_to_id (session, source); xmms_medialib_entry_property_set_int_source (session, entry, key, value, sourceid); xmms_medialib_end (session); xmms_medialib_entry_send_update (entry); } void xmms_medialib_property_remove (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, xmms_error_t *error) { guint32 sourceid; xmms_medialib_session_t *session = xmms_medialib_begin_write (); sourceid = xmms_medialib_source_to_id (session, source); xmms_sqlite_exec (session->sql, "delete from Media where source=%d and " "key='%s' and id=%d", sourceid, key, entry); xmms_medialib_end (session); xmms_medialib_entry_send_update (entry); } static void xmms_medialib_property_remove_method (xmms_medialib_t *medialib, guint32 entry, gchar *source, gchar *key, xmms_error_t *error) { if (g_strcasecmp (source, "server") == 0) { xmms_error_set (error, XMMS_ERROR_GENERIC, "Can't remove properties set by the server!"); return; } return xmms_medialib_property_remove (medialib, entry, source, key, error); } static GList * xmms_medialib_select_method (xmms_medialib_t *medialib, gchar *query, xmms_error_t *error) { GList *ret; xmms_medialib_session_t *session = xmms_medialib_begin (); ret = xmms_medialib_select (session, query, error); xmms_medialib_end (session); return ret; } /** * Get a list of GHashTables 's that matches the query. * * @param session The medialib session to be used for the transaction. * @param query SQL query that should be executed. * @param error In case of error this will be filled. * @returns GList containing GHashTables. Caller are responsible to * free all memory. */ GList * xmms_medialib_select (xmms_medialib_session_t *session, gchar *query, xmms_error_t *error) { GList *res = NULL; gint ret; g_return_val_if_fail (query, 0); g_return_val_if_fail (session, 0); ret = xmms_sqlite_query_table (session->sql, select_callback, (void *)&res, error, "%s", query); return ret ? g_list_reverse (res) : NULL; } /** @} */ /** * @internal */ gboolean xmms_medialib_check_id (xmms_medialib_entry_t entry) { xmms_medialib_session_t *session; gint c = 0; session = xmms_medialib_begin (); if (!xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &c, "select count(id) from Media where id=%d", entry)) { xmms_medialib_end (session); return FALSE; } xmms_medialib_end (session); return c > 0; } /** * @internal * Get the next unresolved entry. Used by the mediainfo reader.. */ xmms_medialib_entry_t xmms_medialib_entry_not_resolved_get (xmms_medialib_session_t *session) { xmms_medialib_entry_t ret = 0; g_return_val_if_fail (session, 0); xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, "select id from Media where key='%s' " "and source=%d and value in (%d, %d) limit 1", XMMS_MEDIALIB_ENTRY_PROPERTY_STATUS, XMMS_MEDIALIB_SOURCE_SERVER_ID, XMMS_MEDIALIB_ENTRY_STATUS_NEW, XMMS_MEDIALIB_ENTRY_STATUS_REHASH); return ret; } guint xmms_medialib_num_not_resolved (xmms_medialib_session_t *session) { guint ret; g_return_val_if_fail (session, 0); xmms_sqlite_query_array (session->sql, xmms_medialib_int_cb, &ret, "select count(id) as value from Media where " "key='%s' and value in (%d, %d) and source=%d", XMMS_MEDIALIB_ENTRY_PROPERTY_STATUS, XMMS_MEDIALIB_ENTRY_STATUS_NEW, XMMS_MEDIALIB_ENTRY_STATUS_REHASH, XMMS_MEDIALIB_SOURCE_SERVER_ID); return ret; } gboolean xmms_medialib_decode_url (char *url) { int i = 0, j = 0; g_return_val_if_fail (url, TRUE); while (url[i]) { unsigned char chr = url[i++]; if (chr == '+') { url[j++] = ' '; } else if (chr == '%') { char ts[3]; char *t; ts[0] = url[i++]; if (!ts[0]) return FALSE; ts[1] = url[i++]; if (!ts[1]) return FALSE; ts[2] = '\0'; url[j++] = strtoul (ts, &t, 16); if (t != &ts[2]) return FALSE; } else { url[j++] = chr; } } url[j] = '\0'; return TRUE; } #define GOODCHAR(a) ((((a) >= 'a') && ((a) <= 'z')) || \ (((a) >= 'A') && ((a) <= 'Z')) || \ (((a) >= '0') && ((a) <= '9')) || \ ((a) == ':') || \ ((a) == '/') || \ ((a) == '-') || \ ((a) == '.') || \ ((a) == '_')) /* we don't share code here with medialib because we want to use g_malloc :( */ gchar * xmms_medialib_url_encode (const gchar *path) { static gchar hex[16] = "0123456789abcdef"; gchar *res; int i = 0, j = 0; res = g_malloc (strlen (path) * 3 + 1); if (!res) return NULL; while (path[i]) { guchar chr = path[i++]; if (GOODCHAR (chr)) { res[j++] = chr; } else if (chr == ' ') { res[j++] = '+'; } else { res[j++] = '%'; res[j++] = hex[((chr & 0xf0) >> 4)]; res[j++] = hex[(chr & 0x0f)]; } } res[j] = '\0'; return res; }