<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:wfw="http://wellformedweb.org/CommentAPI/"
     >
  <channel>
    <title>Death of a Gremmie</title>
    <link>http://deathofagremmie.com</link>
    <description>Brian Neal's blog about programming.</description>
    <pubDate>Sun, 13 May 2012 18:30:33 GMT</pubDate>
    <generator>Blogofile</generator>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>A TeamSpeak 3 viewer with Python & Javascript</title>
      <link>http://deathofagremmie.com/2012/01/20/a-teamspeak-3-viewer-with-python-javascript</link>
      <pubDate>Fri, 20 Jan 2012 19:15:00 CST</pubDate>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[Javascript]]></category>
      <category><![CDATA[TeamSpeak]]></category>
      <guid isPermaLink="true">http://deathofagremmie.com/2012/01/20/a-teamspeak-3-viewer-with-python-javascript</guid>
      <description>A TeamSpeak 3 viewer with Python & Javascript</description>
      <content:encoded><![CDATA[<div class="document">
<div class="section" id="the-problem">
<h3>The Problem</h3>
<p>My gaming clan started using <a class="reference external" href="http://teamspeak.com/?page=teamspeak3">TeamSpeak 3</a> (TS3) for voice communications, so
it wasn't long before we wanted to see who was on the TS3 server from the clan's
server status page. Long ago, before I met Python, I had built the clan a server
status page in PHP. This consisted of cobbling together various home-made and
3rd party PHP scripts for querying game servers (Call of Duty, Battlefield) and
voice servers (TeamSpeak 2 and Mumble). But TeamSpeak 3 was a new one for us,
and I didn't have anything to query that. My interests in PHP are long behind
me, but we needed to add a TS3 viewer to the PHP page. The gaming clan's web
hosting is pretty vanilla; in other words PHP is the first class citizen. If I
really wanted to host a Python app I probably could have resorted to Fast CGI or
something. But I had no experience in that and no desire to go that way.</p>
<p>I briefly thought about finding a 3rd party PHP library to query a TS3 server.
The libraries are out there, but they are as you might expect: overly
complicated and/or pretty amateurish (no public source code repository). I even
considered writing my own PHP code to do the job, so I started looking for any
documentation on the TS3 server query protocol. Luckily, there is a <a class="reference external" href="http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf">TS3
query protocol document</a>, and it is fairly decent.</p>
<p>But, I just could not bring myself to write PHP again. On top of this, the
gaming clan's shared hosting blocks non-standard ports. If I did have a PHP
solution, the outgoing query to the TS3 server would have been blocked by the
host's firewall. It is a hassle to contact their technical support and try to
find a person who knows what a port is and get it unblocked (we've had to do
this over and over as each game comes out). Thus it ultimately boiled down to me
wanting to do this in Python. For me, life is too short to write PHP scripts.</p>
<p>I started thinking about writing a query application in Python using my
dedicated server that I use to host a few <a class="reference external" href="https://www.djangoproject.org">Django</a> powered websites. At first I
thought I'd generate the server status HTML on my server and display it in an
<tt class="docutils literal">&lt;iframe&gt;</tt> on the gaming clan's server. But then it hit me that all I really
needed to do is have my <a class="reference external" href="https://www.djangoproject.org">Django</a> application output a representation of the TS3
server status in <a class="reference external" href="http://json.org">JSON</a>, and then perhaps I could find a slick <a class="reference external" href="http://jquery.org">jQuery</a> tree menu
to display the status graphically. I really liked this idea, so here is a post
about the twists and turns I took implementing it.</p>
</div>
<div class="section" id="the-javascript">
<h3>The Javascript</h3>
<p>My searching turned up several jQuery tree menu plugins, but in the end I
settled on <a class="reference external" href="http://code.google.com/p/dynatree/">dynatree</a>. Dynatree had clear documentation I could understand, it
seems to be actively maintained, and it can generate a menu from JSON. After one
evening of reading the docs, I built a static test HTML page that could display
a tree menu built from JSON. Here the Javascript code I put in the test page's
<tt class="docutils literal">&lt;head&gt;</tt> section:</p>
<div class="highlight"><pre><span class="kd">var</span> <span class="nx">ts3_data</span> <span class="o">=</span> <span class="p">[</span>
   <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;Phantom Aces&quot;</span><span class="p">,</span> <span class="nx">isFolder</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">expand</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="nx">children</span><span class="o">:</span> <span class="p">[</span>
         <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;MW3&quot;</span><span class="p">,</span> <span class="nx">isFolder</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">expand</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
            <span class="nx">children</span><span class="o">:</span> <span class="p">[</span>
               <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;Hogan&quot;</span><span class="p">,</span> <span class="nx">icon</span><span class="o">:</span> <span class="s2">&quot;client.png&quot;</span><span class="p">},</span>
               <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;Fritz!!&quot;</span><span class="p">,</span> <span class="nx">icon</span><span class="o">:</span> <span class="s2">&quot;client.png&quot;</span><span class="p">}</span>
            <span class="p">]</span>
         <span class="p">},</span>
         <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;COD4&quot;</span><span class="p">,</span> <span class="nx">isFolder</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">expand</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
            <span class="nx">children</span><span class="o">:</span> <span class="p">[</span>
               <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;Klink&quot;</span><span class="p">,</span> <span class="nx">icon</span><span class="o">:</span> <span class="s2">&quot;client.png&quot;</span><span class="p">}</span>
            <span class="p">]</span>
         <span class="p">},</span>
         <span class="p">{</span><span class="nx">title</span><span class="o">:</span> <span class="s2">&quot;Away&quot;</span><span class="p">,</span> <span class="nx">isFolder</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">children</span><span class="o">:</span> <span class="p">[],</span> <span class="nx">expand</span><span class="o">:</span> <span class="kc">true</span><span class="p">}</span>
     <span class="p">]</span>
   <span class="p">}</span>
<span class="p">];</span>

<span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
   <span class="nx">$</span><span class="p">(</span><span class="s2">&quot;#ts3-tree&quot;</span><span class="p">).</span><span class="nx">dynatree</span><span class="p">({</span>
      <span class="nx">persist</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="nx">children</span><span class="o">:</span> <span class="nx">ts3_data</span>
   <span class="p">});</span>
 <span class="p">});</span>
</pre></div>
<p>Note that <tt class="docutils literal">client.png</tt> is a small icon I found that I use in place of
dynatree's default file icon to represent TS3 clients. If I omitted the icon
attribute, the TS3 client would have appeared as a small file icon. Channels
appear as folder icons, and this didn't seem to unreasonable to me. In other
words I had no idea what a channel icon would look like. A folder was fine.</p>
<p>With dynatree, you don't need a lot of HTML markup, it does all the heavy
lifting. You simply have to give it an empty <tt class="docutils literal">&lt;div&gt;</tt> tag it can render
into.</p>
<div class="highlight"><pre><span class="nt">&lt;body&gt;</span>
   <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">&quot;ts3-tree&quot;</span><span class="nt">&gt;&lt;/div&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</pre></div>
<p>Here is a screenshot of the static test page in action.</p>
<img alt="/images/011-tree1.png" src="/images/011-tree1.png" />
<p>Nice! Thanks dynatree! Now all I need to do is figure out how to dynamically
generate the JSON data and get it into the gaming clan's server status page.</p>
</div>
<div class="section" id="the-python">
<h3>The Python</h3>
<p>Looking through the <a class="reference external" href="http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf">TS3 protocol documentation</a> I was somewhat surprised to
see that TS3 used the Telnet protocol for queries. So from my trusty shell I
telnet'ed into the TS3 server and played with the available commands. I made
notes on what commands I needed to issue to build my status display.</p>
<p>My experiments worked, and I could see a path forward, but there were still some
kinks to be worked out with the TS3 protocol. The data it sent back was escaped
in a strange way for one thing. I would have to post-process the data in Python
before I could use it. I didn't want to reinvent the wheel, so I did a quick
search for Python libraries for working with TS3. I found a few, but quickly
settled on Andrew William's <a class="reference external" href="http://pypi.python.org/pypi/python-ts3/0.1">python-ts3</a> library. It was small, easy to
understand, had tests, and a GitHub page. Perfect.</p>
<p>One of the great things about Python, of course, is the interactive shell. Armed
with the <a class="reference external" href="http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf">TS3 protocol documentation</a>, <a class="reference external" href="http://pypi.python.org/pypi/python-ts3/0.1">python-ts3</a>, and the Python shell, I was
able to interactively connect to the TS3 server and poke around again. This time
I was sitting above telnet using <a class="reference external" href="http://pypi.python.org/pypi/python-ts3/0.1">python-ts3</a> and I confirmed it would do the job
for me.</p>
<p>Another evening was spent coding up a Django view to query the TS3 server using
<a class="reference external" href="http://pypi.python.org/pypi/python-ts3/0.1">python-ts3</a> and to output the channel status as JSON.</p>
<div class="highlight"><pre><span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="kn">from</span> <span class="nn">django.core.cache</span> <span class="kn">import</span> <span class="n">cache</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponse</span><span class="p">,</span> <span class="n">HttpResponseServerError</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">simplejson</span>
<span class="kn">import</span> <span class="nn">ts3</span>

<span class="n">CACHE_KEY</span> <span class="o">=</span> <span class="s">&#39;ts3-json&#39;</span>
<span class="n">CACHE_TIMEOUT</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="mi">60</span>

<span class="k">def</span> <span class="nf">ts3_query</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
    <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    Query the TeamSpeak3 server for status, and output a JSON</span>
<span class="sd">    representation.</span>

<span class="sd">    The JSON we return is targeted towards the jQuery plugin Dynatree</span>
<span class="sd">    http://code.google.com/p/dynatree/</span>

<span class="sd">    &quot;&quot;&quot;</span>
    <span class="c"># Do we have the result cached?</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">CACHE_KEY</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">content_type</span><span class="o">=</span><span class="s">&#39;application/json&#39;</span><span class="p">)</span>

    <span class="c"># Cache miss, go query the remote server</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="n">svr</span> <span class="o">=</span> <span class="n">ts3</span><span class="o">.</span><span class="n">TS3Server</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">TS3_IP</span><span class="p">,</span> <span class="n">settings</span><span class="o">.</span><span class="n">TS3_PORT</span><span class="p">,</span>
                <span class="n">settings</span><span class="o">.</span><span class="n">TS3_VID</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">ts3</span><span class="o">.</span><span class="n">ConnectionError</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">HttpResponseServerError</span><span class="p">()</span>

    <span class="n">response</span> <span class="o">=</span> <span class="n">svr</span><span class="o">.</span><span class="n">send_command</span><span class="p">(</span><span class="s">&#39;serverinfo&#39;</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">response</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s">&#39;msg&#39;</span><span class="p">]</span> <span class="o">!=</span> <span class="s">&#39;ok&#39;</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">HttpResponseServerError</span><span class="p">()</span>
    <span class="n">svr_info</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>

    <span class="n">response</span> <span class="o">=</span> <span class="n">svr</span><span class="o">.</span><span class="n">send_command</span><span class="p">(</span><span class="s">&#39;channellist&#39;</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">response</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s">&#39;msg&#39;</span><span class="p">]</span> <span class="o">!=</span> <span class="s">&#39;ok&#39;</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">HttpResponseServerError</span><span class="p">()</span>
    <span class="n">channel_list</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">data</span>

    <span class="n">response</span> <span class="o">=</span> <span class="n">svr</span><span class="o">.</span><span class="n">send_command</span><span class="p">(</span><span class="s">&#39;clientlist&#39;</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">response</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s">&#39;msg&#39;</span><span class="p">]</span> <span class="o">!=</span> <span class="s">&#39;ok&#39;</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">HttpResponseServerError</span><span class="p">()</span>
    <span class="n">client_list</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">data</span>

    <span class="c"># Start building the channel / client tree.</span>
    <span class="c"># We save tree nodes in a dictionary, keyed by their id so we can find</span>
    <span class="c"># them later in order to support arbitrary channel hierarchies.</span>
    <span class="n">channels</span> <span class="o">=</span> <span class="p">{}</span>

    <span class="c"># Build the root, or channel 0</span>
    <span class="n">channels</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">&#39;title&#39;</span><span class="p">:</span> <span class="n">svr_info</span><span class="p">[</span><span class="s">&#39;virtualserver_name&#39;</span><span class="p">],</span>
        <span class="s">&#39;isFolder&#39;</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
        <span class="s">&#39;expand&#39;</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
        <span class="s">&#39;children&#39;</span><span class="p">:</span> <span class="p">[]</span>
    <span class="p">}</span>

    <span class="c"># Add the channels to our tree</span>

    <span class="k">for</span> <span class="n">channel</span> <span class="ow">in</span> <span class="n">channel_list</span><span class="p">:</span>
        <span class="n">node</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">&#39;title&#39;</span><span class="p">:</span> <span class="n">channel</span><span class="p">[</span><span class="s">&#39;channel_name&#39;</span><span class="p">],</span>
            <span class="s">&#39;isFolder&#39;</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="s">&#39;expand&#39;</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
            <span class="s">&#39;children&#39;</span><span class="p">:</span> <span class="p">[]</span>
        <span class="p">}</span>
        <span class="n">parent</span> <span class="o">=</span> <span class="n">channels</span><span class="p">[</span><span class="nb">int</span><span class="p">(</span><span class="n">channel</span><span class="p">[</span><span class="s">&#39;pid&#39;</span><span class="p">])]</span>
        <span class="n">parent</span><span class="p">[</span><span class="s">&#39;children&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
        <span class="n">channels</span><span class="p">[</span><span class="nb">int</span><span class="p">(</span><span class="n">channel</span><span class="p">[</span><span class="s">&#39;cid&#39;</span><span class="p">])]</span> <span class="o">=</span> <span class="n">node</span>

    <span class="c"># Add the clients to the tree</span>

    <span class="k">for</span> <span class="n">client</span> <span class="ow">in</span> <span class="n">client_list</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">client</span><span class="p">[</span><span class="s">&#39;client_type&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="s">&#39;0&#39;</span><span class="p">:</span>
            <span class="n">node</span> <span class="o">=</span> <span class="p">{</span>
                <span class="s">&#39;title&#39;</span><span class="p">:</span> <span class="n">client</span><span class="p">[</span><span class="s">&#39;client_nickname&#39;</span><span class="p">],</span>
                <span class="s">&#39;icon&#39;</span><span class="p">:</span> <span class="s">&#39;client.png&#39;</span>
            <span class="p">}</span>
            <span class="n">channel</span> <span class="o">=</span> <span class="n">channels</span><span class="p">[</span><span class="nb">int</span><span class="p">(</span><span class="n">client</span><span class="p">[</span><span class="s">&#39;cid&#39;</span><span class="p">])]</span>
            <span class="n">channel</span><span class="p">[</span><span class="s">&#39;children&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>

    <span class="n">tree</span> <span class="o">=</span> <span class="p">[</span><span class="n">channels</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>

    <span class="c"># convert to JSON</span>
    <span class="n">json</span> <span class="o">=</span> <span class="n">simplejson</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">tree</span><span class="p">)</span>

    <span class="n">cache</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">CACHE_KEY</span><span class="p">,</span> <span class="n">json</span><span class="p">,</span> <span class="n">CACHE_TIMEOUT</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">json</span><span class="p">,</span> <span class="n">content_type</span><span class="o">=</span><span class="s">&#39;application/json&#39;</span><span class="p">)</span>
</pre></div>
<p>I have to make three queries to the TS3 server to get all the information I
need. The <tt class="docutils literal">serverinfo</tt> command is issued to retrieve the TS3 virtual server's
name. The <tt class="docutils literal">channellist</tt> command retrieves the list of channels. The
<tt class="docutils literal">clientlist</tt> command gets the list of TS3 clients that are currently
connected. For more information on these three commands see the TS3 query
protocol document.</p>
<p>The only real tricky part of this code was figuring out how to represent an
arbitrary, deeply-nested channel tree in Python. I ended up guessing that
<tt class="docutils literal">cid</tt> meant channel ID and <tt class="docutils literal">pid</tt> meant parent ID in the TS3 query data. I
squirrel away the channels in a <tt class="docutils literal">channels</tt> dictionary, keyed by channel ID.
The root channel has an ID of 0. While iterating over the channel list, I can
retrieve the parent channel from the <tt class="docutils literal">channels</tt> dictionary by ID and append
the new channel to the parent's <tt class="docutils literal">children</tt> list. Clients are handled the same
way, but have different attributes. By inspecting the <tt class="docutils literal">clientlist</tt> data in the
Python shell, I noticed that my Telnet client also showed up in that list.
However it had a <tt class="docutils literal">client_type</tt> of 1, whereas the normal PC clients had a
<tt class="docutils literal">client_type</tt> of 0.</p>
<p>I decided to cache the results for 2 minutes to reduce hits on the TS3 server,
as it has flood protection. This probably isn't needed given the size of our
gaming clan, but Django makes it easy to do, so why not?</p>
</div>
<div class="section" id="putting-it-all-together">
<h3>Putting it all together</h3>
<p>At this point I knew how to use my Django application to query the TS3 server
and build status in JSON format. I also knew what the Javascript and HTML on the
gaming clan's server status page (written in PHP) had to look like to render
that JSON status.</p>
<p>The problem was the server status page was on one server, and my Django
application was on another. At first I thought it would be no problem for the
Javascript to do a <tt class="docutils literal">GET</tt> on my Django server and retrieve the JSON. However I
had some vague memory of the browser security model, and after some googling I
was reminded of the <a class="reference external" href="http://en.wikipedia.org/wiki/Same_origin_policy">same origin policy</a>. Rats. That wasn't going to work.</p>
<p>I briefly researched <a class="reference external" href="http://en.wikipedia.org/wiki/JSONP">JSONP</a>, which is the technique that Facebook &amp; Google use
to embed those little &quot;like&quot; and &quot;+1&quot; buttons on your web pages. But in the end
it was just as easy to have the PHP script make the <tt class="docutils literal">GET</tt> request to my Django
application using a <a class="reference external" href="http://php.net/manual/en/function.file-get-contents.php">file_get_contents()</a> call. The PHP can then embed the JSON
directly into the server status page:</p>
<div class="highlight"><pre><span class="x">$ts3_source = &#39;http://example.com/ts3/&#39;;</span>
<span class="x">$ts3_json = file_get_contents($ts3_source);</span>

<span class="x">require_once &#39;header.php&#39;;</span>
</pre></div>
<p>And in header.php, some HTML sprinkled with some PHP:</p>
<div class="highlight"><pre><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">&quot;text/javascript&quot;</span><span class="nt">&gt;</span>
   <span class="kd">var</span> <span class="nx">ts3_data</span> <span class="o">=</span> <span class="o">&lt;?</span><span class="nx">php</span> <span class="nx">echo</span> <span class="nx">$ts3_json</span><span class="p">;</span> <span class="o">?&gt;</span><span class="p">;</span>

   <span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
      <span class="nx">$</span><span class="p">(</span><span class="s2">&quot;#ts3-tree&quot;</span><span class="p">).</span><span class="nx">dynatree</span><span class="p">({</span>
         <span class="nx">persist</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span>
         <span class="nx">children</span><span class="o">:</span> <span class="nx">ts3_data</span>
      <span class="p">});</span>
    <span class="p">});</span>
<span class="nt">&lt;/script&gt;</span>
</pre></div>
<p>That did the trick. In the end I had to touch a little PHP, but it was
tolerable.  That was a very round-about solution to building a TS3 viewer in
Python and Javascript. While I doubt you will have the same strange requirements
that I had (multiple servers), I hope you can see how to combine a few
technologies to make a TS3 viewer in Python.</p>
</div>
</div>
]]></content:encoded>
    </item>
  </channel>
</rss>

