fixed firefox websocket bug, added assets
authorAndrew Karpow <andy@ndyk.de>
Tue, 3 Dec 2013 20:48:49 +0000 (21:48 +0100)
committerAndrew Karpow <andy@ndyk.de>
Tue, 3 Dec 2013 20:48:49 +0000 (21:48 +0100)
12 files changed:
CMakeLists.txt
htdocs/assets/favicon.ico [new file with mode: 0644]
htdocs/css/mpd.css
htdocs/index.html
htdocs/js/jquery.simplePagination.js [new file with mode: 0644]
htdocs/js/mpd.js
src/http_server.c
src/http_server.h
src/mpd_client.c
src/mpd_client.h
src/urldecode.c [new file with mode: 0644]
src/ympd_process.c [new file with mode: 0644]

index 2e50618..8fce450 100644 (file)
@@ -8,7 +8,7 @@ set(CPACK_PACKAGE_VENDOR "Andrew Karpow <andy@ndyk.de>")
 set(CPACK_DEBIAN_PACKAGE_MAINTAINER "andy@ndyk.de")
 set(CPACK_PACKAGE_VERSION_MAJORĀ "0")
 set(CPACK_PACKAGE_VERSION_MINORĀ "1")
-set(CPACK_PACKAGE_VERSION_PATCH "0")
+set(CPACK_PACKAGE_VERSION_PATCH "2")
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/")
 
 find_package(LibWebSockets REQUIRED)
@@ -16,7 +16,8 @@ find_package(LibMPDClient REQUIRED)
 include(CheckCSourceCompiles)
 include(CPack)
 
-set(CMAKE_C_FLAGS_DEBRELEASE "-O2 -DNDEBUG -pipe")
+set(CMAKE_C_FLAGS_DEBRELEASE "-Wall -O2 -DNDEBUG -pipe")
+set(CMAKE_C_FLAGS_DEBUG "-Wall -ggdb")
 
 set(SOURCES
     src/ympd.c
diff --git a/htdocs/assets/favicon.ico b/htdocs/assets/favicon.ico
new file mode 100644 (file)
index 0000000..01108ee
Binary files /dev/null and b/htdocs/assets/favicon.ico differ
index deb280c..b1a3a0a 100644 (file)
@@ -31,4 +31,3 @@ body {
 .btn-group-hover {
     opacity: 0;
 }
-
index 1e286d7..6ef24fa 100644 (file)
@@ -15,6 +15,7 @@
   <!-- Custom styles for this template -->
   <link href="css/slider.css" rel="stylesheet">
   <link href="css/mpd.css" rel="stylesheet">
+  <link href="assets/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon">
 
   <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
   <!--[if lt IE 9]>
@@ -32,7 +33,7 @@
           <span class="icon-bar"></span>
           <span class="icon-bar"></span>
         </button>
-        <a class="navbar-brand" href="/">ympd</a>
+        <a class="navbar-brand" href="/"><span class="glyphicon glyphicon-play-circle"></span> ympd</a>
       </div>
       <div class="collapse navbar-collapse">
 
           <h2 class="modal-title" id="aboutLabel">About</h2>
         </div>
         <div class="modal-body">
-          <h4>ympd - <small>a lightweight and fast Music Player Daemon web client</small></h4>
+          <h4><span class="glyphicon glyphicon-play-circle"></span> ympd&nbsp;&nbsp;&nbsp;<small>MPD Web GUI - written in C, utilizing Websockets and Bootstrap/JS</small></h4>
           <br/>
-          ympd is written in pure C and based completely on HTML5 WebSockets with Bootstrap.
-          <h5>ympd uses following excellent software:</h5>
+          <span class="glyphicon glyphicon-play-circle"></span> ympd is a lightweight MPD (Music Player Daemon) web client that runs without a dedicated werbserver or interpreters like PHP, NodeJS or Ruby. It's tuned for minimal resource usage and requires only very litte dependencies.
+          <h5><span class="glyphicon glyphicon-play-circle"></span> ympd uses following excellent software:</h5>
           <h6><a href="http://libwebsockets.org">libWebSockets</a> <small>LGPL2.1 + static link exception</small></h6>
           <h6><a href="http://www.musicpd.org/libs/libmpdclient/">libMPDClient</a> <small>BSD License</small></h6>
           <br/>
           <blockquote>
             <address>
               <strong>Andrew Karpow</strong><br>
-              <a href="mailto:andy@ndyk.de">andy@ndyk.de</a><br/>
-              <a href="http://www.ympd.org">www.ympd.de</a>
+              <a href="mailto:andy@ympd.org">andy@ympd.org</a><br/>
+              <a href="http://www.ympd.org">www.ympd.org</a><br/>
+              XMPP: <a href="xmpp:andy@jabber.ccc.de?subscribe">andy_@jabber.ccc.de</a>
             </address>
           </blockquote>
         </div>
diff --git a/htdocs/js/jquery.simplePagination.js b/htdocs/js/jquery.simplePagination.js
new file mode 100644 (file)
index 0000000..e5a4664
--- /dev/null
@@ -0,0 +1,268 @@
+/**
+* simplePagination.js v1.6
+* A simple jQuery pagination plugin.
+* http://flaviusmatis.github.com/simplePagination.js/
+*
+* Copyright 2012, Flavius Matis
+* Released under the MIT license.
+* http://flaviusmatis.github.com/license.html
+*/
+
+(function($){
+
+       var methods = {
+               init: function(options) {
+                       var o = $.extend({
+                               items: 1,
+                               itemsOnPage: 1,
+                               pages: 0,
+                               displayedPages: 5,
+                               edges: 2,
+                               currentPage: 1,
+                               hrefTextPrefix: '#page-',
+                               hrefTextSuffix: '',
+                               prevText: 'Prev',
+                               nextText: 'Next',
+                               ellipseText: '&hellip;',
+                               cssStyle: 'light-theme',
+                               labelMap: [],
+                               selectOnClick: true,
+                               onPageClick: function(pageNumber, event) {
+                                       // Callback triggered when a page is clicked
+                                       // Page number is given as an optional parameter
+                               },
+                               onInit: function() {
+                                       // Callback triggered immediately after initialization
+                               }
+                       }, options || {});
+
+                       var self = this;
+
+                       o.pages = o.pages ? o.pages : Math.ceil(o.items / o.itemsOnPage) ? Math.ceil(o.items / o.itemsOnPage) : 1;
+                       o.currentPage = o.currentPage - 1;
+                       o.halfDisplayed = o.displayedPages / 2;
+
+                       this.each(function() {
+                               self.addClass(o.cssStyle + ' simple-pagination').data('pagination', o);
+                               methods._draw.call(self);
+                       });
+
+                       o.onInit();
+
+                       return this;
+               },
+
+               selectPage: function(page) {
+                       methods._selectPage.call(this, page - 1);
+                       return this;
+               },
+
+               prevPage: function() {
+                       var o = this.data('pagination');
+                       if (o.currentPage > 0) {
+                               methods._selectPage.call(this, o.currentPage - 1);
+                       }
+                       return this;
+               },
+
+               nextPage: function() {
+                       var o = this.data('pagination');
+                       if (o.currentPage < o.pages - 1) {
+                               methods._selectPage.call(this, o.currentPage + 1);
+                       }
+                       return this;
+               },
+
+               getPagesCount: function() {
+                       return this.data('pagination').pages;
+               },
+
+               getCurrentPage: function () {
+                       return this.data('pagination').currentPage + 1;
+               },
+
+               destroy: function(){
+                       this.empty();
+                       return this;
+               },
+
+               drawPage: function (page) {
+                       var o = this.data('pagination');
+                       o.currentPage = page - 1;
+                       this.data('pagination', o);
+                       methods._draw.call(this);
+                       return this;
+               },
+
+               redraw: function(){
+                       methods._draw.call(this);
+                       return this;
+               },
+
+               disable: function(){
+                       var o = this.data('pagination');
+                       o.disabled = true;
+                       this.data('pagination', o);
+                       methods._draw.call(this);
+                       return this;
+               },
+
+               enable: function(){
+                       var o = this.data('pagination');
+                       o.disabled = false;
+                       this.data('pagination', o);
+                       methods._draw.call(this);
+                       return this;
+               },
+
+               updateItems: function (newItems) {
+                       var o = this.data('pagination');
+                       o.items = newItems;
+                       o.pages = methods._getPages(o);
+                       this.data('pagination', o);
+                       methods._draw.call(this);
+               },
+
+               updateItemsOnPage: function (itemsOnPage) {
+                       var o = this.data('pagination');
+                       o.itemsOnPage = itemsOnPage;
+                       o.pages = methods._getPages(o);
+                       this.data('pagination', o);
+                       methods._selectPage.call(this, 0);
+                       return this;
+               },
+
+               _draw: function() {
+                       var     o = this.data('pagination'),
+                               interval = methods._getInterval(o),
+                               i,
+                               tagName;
+
+                       methods.destroy.call(this);
+                       
+                       tagName = (typeof this.prop === 'function') ? this.prop('tagName') : this.attr('tagName');
+
+                       var $panel = tagName === 'UL' ? this : $('<ul></ul>').appendTo(this);
+
+                       // Generate Prev link
+                       if (o.prevText) {
+                               methods._appendItem.call(this, o.currentPage - 1, {text: o.prevText, classes: 'prev'});
+                       }
+
+                       // Generate start edges
+                       if (interval.start > 0 && o.edges > 0) {
+                               var end = Math.min(o.edges, interval.start);
+                               for (i = 0; i < end; i++) {
+                                       methods._appendItem.call(this, i);
+                               }
+                               if (o.edges < interval.start && (interval.start - o.edges != 1)) {
+                                       $panel.append('<li class="disabled"><span class="ellipse">' + o.ellipseText + '</span></li>');
+                               } else if (interval.start - o.edges == 1) {
+                                       methods._appendItem.call(this, o.edges);
+                               }
+                       }
+
+                       // Generate interval links
+                       for (i = interval.start; i < interval.end; i++) {
+                               methods._appendItem.call(this, i);
+                       }
+
+                       // Generate end edges
+                       if (interval.end < o.pages && o.edges > 0) {
+                               if (o.pages - o.edges > interval.end && (o.pages - o.edges - interval.end != 1)) {
+                                       $panel.append('<li class="disabled"><span class="ellipse">' + o.ellipseText + '</span></li>');
+                               } else if (o.pages - o.edges - interval.end == 1) {
+                                       methods._appendItem.call(this, interval.end++);
+                               }
+                               var begin = Math.max(o.pages - o.edges, interval.end);
+                               for (i = begin; i < o.pages; i++) {
+                                       methods._appendItem.call(this, i);
+                               }
+                       }
+
+                       // Generate Next link
+                       if (o.nextText) {
+                               methods._appendItem.call(this, o.currentPage + 1, {text: o.nextText, classes: 'next'});
+                       }
+               },
+
+               _getPages: function(o) {
+                       var pages = Math.ceil(o.items / o.itemsOnPage);
+                       return pages || 1;
+               },
+
+               _getInterval: function(o) {
+                       return {
+                               start: Math.ceil(o.currentPage > o.halfDisplayed ? Math.max(Math.min(o.currentPage - o.halfDisplayed, (o.pages - o.displayedPages)), 0) : 0),
+                               end: Math.ceil(o.currentPage > o.halfDisplayed ? Math.min(o.currentPage + o.halfDisplayed, o.pages) : Math.min(o.displayedPages, o.pages))
+                       };
+               },
+
+               _appendItem: function(pageIndex, opts) {
+                       var self = this, options, $link, o = self.data('pagination'), $linkWrapper = $('<li></li>'), $ul = self.find('ul');
+
+                       pageIndex = pageIndex < 0 ? 0 : (pageIndex < o.pages ? pageIndex : o.pages - 1);
+
+                       options = {
+                               text: pageIndex + 1,
+                               classes: ''
+                       };
+
+                       if (o.labelMap.length && o.labelMap[pageIndex]) {
+                               options.text = o.labelMap[pageIndex];
+                       }
+
+                       options = $.extend(options, opts || {});
+
+                       if (pageIndex == o.currentPage || o.disabled) {
+                               if (o.disabled) {
+                                       $linkWrapper.addClass('disabled');
+                               } else {
+                                       $linkWrapper.addClass('active');
+                               }
+                               $link = $('<span class="current">' + (options.text) + '</span>');
+                       } else {
+                               $link = $('<a href="' + o.hrefTextPrefix + (pageIndex + 1) + o.hrefTextSuffix + '" class="page-link">' + (options.text) + '</a>');
+                               $link.click(function(event){
+                                       return methods._selectPage.call(self, pageIndex, event);
+                               });
+                       }
+
+                       if (options.classes) {
+                               $link.addClass(options.classes);
+                       }
+
+                       $linkWrapper.append($link);
+
+                       if ($ul.length) {
+                               $ul.append($linkWrapper);
+                       } else {
+                               self.append($linkWrapper);
+                       }
+               },
+
+               _selectPage: function(pageIndex, event) {
+                       var o = this.data('pagination');
+                       o.currentPage = pageIndex;
+                       if (o.selectOnClick) {
+                               methods._draw.call(this);
+                       }
+                       return o.onPageClick(pageIndex + 1, event);
+               }
+
+       };
+
+       $.fn.pagination = function(method) {
+
+               // Method calling logic
+               if (methods[method] && method.charAt(0) != '_') {
+                       return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+               } else if (typeof method === 'object' || !method) {
+                       return methods.init.apply(this, arguments);
+               } else {
+                       $.error('Method ' +  method + ' does not exist on jQuery.pagination');
+               }
+
+       };
+
+})(jQuery);
index 32136c0..8f66e88 100644 (file)
@@ -1,6 +1,7 @@
 var socket;
 var last_state;
 var current_app;
+var is_firefox;
 
 $('#volumeslider').slider().on('slide', function(event) {
        socket.send("MPD_API_SET_VOLUME,"+event.value);
@@ -16,7 +17,11 @@ var app = $.sammy(function() {
                current_app = 'playlist';
                $('#breadcrump').addClass('hide');
                $('#salamisandwich').find("tr:gt(0)").remove();
-               socket.send("MPD_API_GET_PLAYLIST");
+               if(is_firefox)
+                       $.get( "/api/get_playlist", socket.onmessage);
+               else
+                       socket.send("MPD_API_GET_PLAYLIST");
+
                $('#panel-heading').text("Playlist");
                $('#playlist').addClass('active');
        });
@@ -29,7 +34,11 @@ var app = $.sammy(function() {
                if(path == '')
                        path = "/";
 
-               socket.send("MPD_API_GET_BROWSE,"+path);
+               if(is_firefox)
+                       $.get( "/api/get_browse/" + encodeURIComponent(path), socket.onmessage);
+               else
+                       socket.send("MPD_API_GET_BROWSE,"+path);
+               
                $('#panel-heading').text("Browse database: "+path+"");
                var path_array = path[0].split('/');
                var full_path = "";
@@ -52,6 +61,7 @@ var app = $.sammy(function() {
 });
 
 $(document).ready(function(){
+       is_firefox = true;
        webSocketConnect();
 });
 
@@ -73,10 +83,16 @@ function webSocketConnect() {
                }
 
                socket.onmessage =function got_packet(msg) {
-                       if(msg.data === last_state)
-                               return;
+                       console.log(typeof msg);
+                       if(msg instanceof MessageEvent) {
+                               if(msg.data === last_state)
+                                       return;
+
+                               var obj = JSON.parse(msg.data);
+                       } else {
+                               var obj = msg;
+                       }
 
-                       var obj = JSON.parse(msg.data);
                        switch (obj.type) {
                                case "playlist":
                                        if(current_app !== 'playlist')
index 7ccb118..15df26f 100644 (file)
@@ -1,7 +1,11 @@
 #include <libwebsockets.h>
 #include <stdio.h>
 #include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+
 #include "http_server.h"
+#include "mpd_client.h"
 
 char *resource_path = LOCAL_RESOURCE_PATH;
 
@@ -26,30 +30,117 @@ static const struct serveable whitelist[] = {
     { "/fonts/glyphicons-halflings-regular.ttf", "application/x-font-ttf"},
     { "/fonts/glyphicons-halflings-regular.eot", "application/vnd.ms-fontobject"},
 
+    { "assets/favicon.ico", "image/vnd.microsoft.icon" },
+
     /* last one is the default served if no match */
     { "/index.html", "text/html" },
 };
 
+/* Converts a hex character to its integer value */
+char from_hex(char ch) {
+  return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10;
+}
+
+/* Converts an integer value to its hex character*/
+char to_hex(char code) {
+  static char hex[] = "0123456789abcdef";
+  return hex[code & 15];
+}
+
+/* Returns a url-decoded version of str */
+/* IMPORTANT: be sure to free() the returned string after use */
+char *url_decode(char *str) {
+  char *pstr = str, *buf = malloc(strlen(str) + 1), *pbuf = buf;
+  while (*pstr) {
+    if (*pstr == '%') {
+      if (pstr[1] && pstr[2]) {
+        *pbuf++ = from_hex(pstr[1]) << 4 | from_hex(pstr[2]);
+        pstr += 2;
+      }
+    } else if (*pstr == '+') {
+      *pbuf++ = ' ';
+    } else {
+      *pbuf++ = *pstr;
+    }
+    pstr++;
+  }
+  *pbuf = '\0';
+  return buf;
+}
+
 int callback_http(struct libwebsocket_context *context,
         struct libwebsocket *wsi,
         enum libwebsocket_callback_reasons reason, void *user,
         void *in, size_t len)
 {
-    char buf[256];
-    size_t n;
+    char buf[256], *response_buffer, *p;
+    size_t n, response_size;
 
     switch (reason) {
         case LWS_CALLBACK_HTTP:
-            for (n = 0; n < (sizeof(whitelist) / sizeof(whitelist[0]) - 1); n++)
+            if(in && strncmp((const char *)in, "/api/", 5) == 0)
             {
-                if (in && strcmp((const char *)in, whitelist[n].urlpath) == 0)
-                    break;
-            }
-            sprintf(buf, "%s%s", resource_path, whitelist[n].urlpath);
+                response_buffer = (char *)malloc(100 * 1024 + 100);
+                p = response_buffer;
+
+                /* put content length and payload to buffer */
+                if(strncmp((const char *)in, "/api/get_browse", 15) == 0)
+                {
+                    char *url;
+                    if(sscanf(in, "/api/get_browse/%m[^\t\n]", &url))
+                    {
+                        char *url_decoded = url_decode(url);
+                        printf("searching for %s", url_decoded);
+                        response_size = mpd_put_browse(response_buffer + 98, url_decoded);
+                        free(url_decoded);
+                        free(url);
+                    }
+                    else
+                        response_size = mpd_put_browse(response_buffer + 98, "/");
+
+                }
+                else if(strncmp((const char *)in, "/api/get_playlist", 17)  == 0)
+                    response_size = mpd_put_playlist(response_buffer + 98);
+                else
+                {
+                    /* invalid request, close connection */
+                    free(response_buffer);
+                    return -1;
+                }
+                p += response_size + sprintf(p, "HTTP/1.0 200 OK\x0d\x0a"
+                                "Server: libwebsockets\x0d\x0a"
+                                "Content-Type: application/json\x0d\x0a"
+                                "Content-Length: %6lu\x0d\x0a\x0d\x0a", 
+                                response_size
+                );
+                response_buffer[98] = '{';
 
-            if (libwebsockets_serve_http_file(context, wsi, buf, whitelist[n].mimetype))
-                return -1; /* through completion or error, close the socket */
+                n = libwebsocket_write(wsi, (unsigned char *)response_buffer,
+                    p - response_buffer, LWS_WRITE_HTTP);
 
+                free(response_buffer);
+                /*
+                 * book us a LWS_CALLBACK_HTTP_WRITEABLE callback
+                 */
+                libwebsocket_callback_on_writable(context, wsi);
+
+            }
+            else if(in && strcmp((const char *)in, "getPlaylist") == 0)
+            {
+                
+            }
+            else
+            {            
+                for (n = 0; n < (sizeof(whitelist) / sizeof(whitelist[0]) - 1); n++)
+                {
+                    if (in && strcmp((const char *)in, whitelist[n].urlpath) == 0)
+                        break;
+                }
+                sprintf(buf, "%s%s", resource_path, whitelist[n].urlpath);
+
+                if (libwebsockets_serve_http_file(context, wsi, buf, whitelist[n].mimetype))
+                    return -1; /* through completion or error, close the socket */
+            }
             break;
 
         case LWS_CALLBACK_HTTP_FILE_COMPLETION:
index f360106..9f899b6 100644 (file)
@@ -3,7 +3,6 @@
 struct per_session_data__http {
     int fd;
 };
-
 int callback_http(struct libwebsocket_context *context,
         struct libwebsocket *wsi,
         enum libwebsocket_callback_reasons reason, void *user,
index 27184d0..c7eec25 100644 (file)
@@ -21,6 +21,11 @@ enum mpd_conn_states mpd_conn_state = MPD_DISCONNECTED;
 enum mpd_state mpd_play_state = MPD_STATE_UNKNOWN;
 unsigned queue_version;
 
+void *mpd_idle_connection(void *_data)
+{
+
+}
+
 int callback_ympd(struct libwebsocket_context *context,
         struct libwebsocket *wsi,
         enum libwebsocket_callback_reasons reason,
@@ -251,24 +256,30 @@ int mpd_put_state(char *buffer)
 
 int mpd_put_current_song(char *buffer)
 {
+    char *cur = buffer;
+    const char *end = buffer + MAX_SIZE;
     struct mpd_song *song;
-    int len;
 
     song = mpd_run_current_song(conn);
     if(song == NULL)
         return 0;
 
-    len = snprintf(buffer, MAX_SIZE, "{\"type\": \"current_song\", \"data\":"
-            "{\"pos\":%d, \"title\":\"%s\", \"artist\":\"%s\", \"album\":\"%s\"}}",
+    cur += snprintf(cur, end - cur, "{\"type\": \"current_song\", \"data\":"
+            "{\"pos\":%d, \"title\":\"%s\"",
             mpd_song_get_pos(song),
-            mpd_get_title(song),
-            mpd_song_get_tag(song, MPD_TAG_ARTIST, 0),
-            mpd_song_get_tag(song, MPD_TAG_ALBUM, 0)
-            );
+            mpd_get_title(song));
+    if(mpd_song_get_tag(song, MPD_TAG_ARTIST, 0) != NULL)
+        cur += snprintf(cur, end - cur, ", \"artist\":\"%s\"",
+            mpd_song_get_tag(song, MPD_TAG_ARTIST, 0));
+    if(mpd_song_get_tag(song, MPD_TAG_ALBUM, 0) != NULL)
+        cur += snprintf(cur, end - cur, ", \"album\":\"%s\"",
+            mpd_song_get_tag(song, MPD_TAG_ALBUM, 0));
+
+    cur += snprintf(cur, end - cur, "}}");
     mpd_song_free(song);
     mpd_response_finish(conn);
 
-    return len;
+    return cur - buffer;
 }
 
 int mpd_put_playlist(char *buffer)
@@ -342,8 +353,9 @@ int mpd_put_browse(char *buffer, char *path)
             case MPD_ENTITY_TYPE_DIRECTORY:
                 dir = mpd_entity_get_directory(entity);
                 cur += snprintf(cur, end  - cur, 
-                        "{\"type\":\"directory\",\"dir\":\"%s\"},",
-                        mpd_directory_get_path(dir)
+                        "{\"type\":\"directory\",\"dir\":\"%s\", \"basename\":\"%s\"},",
+                        mpd_directory_get_path(dir), 
+                        basename((char *)mpd_directory_get_path(dir))
                         );
                 break;
 
index 3c29ce0..7b88bd9 100644 (file)
@@ -38,6 +38,7 @@ enum mpd_conn_states {
     MPD_CONNECTED
 };
 
+void *mpd_idle_connection(void *_data);
 int callback_ympd(struct libwebsocket_context *context,
         struct libwebsocket *wsi,
         enum libwebsocket_callback_reasons reason,
diff --git a/src/urldecode.c b/src/urldecode.c
new file mode 100644 (file)
index 0000000..8a38ac4
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    from http://www.geekhideout.com/urlcode.shtml
+    public domain
+*/
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Converts a hex character to its integer value */
+char from_hex(char ch) {
+  return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10;
+}
+
+/* Converts an integer value to its hex character*/
+char to_hex(char code) {
+  static char hex[] = "0123456789abcdef";
+  return hex[code & 15];
+}
+
+/* Returns a url-decoded version of str */
+/* IMPORTANT: be sure to free() the returned string after use */
+char *url_decode(char *str) {
+  char *pstr = str, *buf = malloc(strlen(str) + 1), *pbuf = buf;
+  while (*pstr) {
+    if (*pstr == '%') {
+      if (pstr[1] && pstr[2]) {
+        *pbuf++ = from_hex(pstr[1]) << 4 | from_hex(pstr[2]);
+        pstr += 2;
+      }
+    } else if (*pstr == '+') { 
+      *pbuf++ = ' ';
+    } else {
+      *pbuf++ = *pstr;
+    }
+    pstr++;
+  }
+  *pbuf = '\0';
+  return buf;
+}
diff --git a/src/ympd_process.c b/src/ympd_process.c
new file mode 100644 (file)
index 0000000..40ab565
--- /dev/null
@@ -0,0 +1 @@
+ympd_process.c
\ No newline at end of file