[SCM] streamtuner2/master: Imported Upstream version 2.1.3

takaki at users.alioth.debian.org takaki at users.alioth.debian.org
Wed Nov 25 07:06:48 UTC 2015


The following commit has been merged in the master branch:
commit 29bc2ac8caab3deefe57f4fe885bd6511329c162
Author: TANIGUCHI Takaki <takaki at asis.media-as.org>
Date:   Mon Aug 18 00:00:56 2014 +0900

    Imported Upstream version 2.1.3

diff --git a/README b/README
index 1e155de..21edff8 100644
--- a/README
+++ b/README
@@ -51,6 +51,15 @@ Public Domain.
 history
 -------
 
+2.1.3 (2014-08-15)
+- New plugin for Dirble.com introduced.
+- Channel tabs can now be rearranged from notebook top to left side.
+- Live365 was fixed again.
+- Xiph cache service was fixed, and duplicates are now filtered out.
+- Category map storage is now handled by backend instead of channels.
+- Shorter Youtube homepage URLs are used, HTTP headers compacted.
+
+
 2.1.2 (2014-07-31)
 - Listing from the renewed Radionomy Shoutcast has been fixed.
 - Live365 was disabled.
diff --git a/_package.epm b/_package.epm
index 8751128..4e740f2 100644
--- a/_package.epm
+++ b/_package.epm
@@ -1,5 +1,5 @@
 %product streamtuner2 - internet radio browser
-%version 2.1.2
+%version 2.1.3
 %vendor Mario Salzer
 %license
 %copyright Placed into the Public Domain, 2009-2014
@@ -47,6 +47,8 @@ f 644 root root /usr/share/streamtuner2/pq.py			./pq.py
 d 755 root root /usr/share/streamtuner2/channels		-
 f 644 root root /usr/share/streamtuner2/channels/__init__.py	./channels/__init__.py
 f 644 root root /usr/share/streamtuner2/channels/_generic.py	./channels/_generic.py
+f 644 root root /usr/share/streamtuner2/channels/dirble.py 	./channels/dirble.py
+f 644 root root /usr/share/streamtuner2/channels/dirble.png 	./channels/dirble.png
 f 644 root root /usr/share/streamtuner2/channels/icast.py 	./channels/icast.py
 f 644 root root /usr/share/streamtuner2/channels/icast.png 	./channels/icast.png
 f 644 root root /usr/share/streamtuner2/channels/internet_radio.py ./channels/internet_radio.py
diff --git a/ahttp.py b/ahttp.py
index 6fd1516..bd9d662 100644
--- a/ahttp.py
+++ b/ahttp.py
@@ -56,7 +56,7 @@ session.headers.update({
 #
 #  Well, it says "get", but it actually does POST and AJAXish GET requests too.
 #
-def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None):
+def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None, content=True):
     __print__( dbg.HTTP, "GET", url, params )
 
     # statusbar info
@@ -78,14 +78,18 @@ def get(url, params={}, referer="", post=0, ajax=0, binary=0, feedback=None):
     __print__( dbg.HTTP, r.request.headers );
     __print__( dbg.HTTP, r.headers );
             
-    # result
-    #progress_feedback(0.9)
-    content = (r.content if binary else r.text)
-    
     # finish, clean statusbar
+    #progress_feedback(0.9)
     progress_feedback("")
-    __print__( dbg.INFO, "Content-Length", len(content) )
-    return content
+
+    # result
+    __print__( dbg.INFO, "Content-Length", len(r.content) )
+    if not content:
+        return r
+    elif binary:
+        return r.content
+    else:
+        return r.text
 
 
 
diff --git a/channels/_generic.py b/channels/_generic.py
index 06c0468..1335b34 100644
--- a/channels/_generic.py
+++ b/channels/_generic.py
@@ -58,6 +58,7 @@ class GenericChannel(object):
 
         # categories
         categories = ["empty", ]
+        catmap = {}
         current = ""
         default = "empty"
         shown = None     # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet
@@ -133,6 +134,10 @@ class GenericChannel(object):
             cache = conf.load("cache/categories_" + self.module)
             if (cache):
                 self.categories = cache
+            # catmap (optional)
+            cache = conf.load("cache/catmap_" + self.module)
+            if (cache):
+                self.catmap = cache
             pass
 
             
@@ -376,7 +381,10 @@ class GenericChannel(object):
         
             # get data and save
             self.update_categories()
-            conf.save("cache/categories_"+self.module, self.categories)
+            if self.categories:
+                conf.save("cache/categories_"+self.module, self.categories)
+            if self.catmap:
+                conf.save("cache/catmap_" + self.module, self.catmap);
 
             # display outside of this non-main thread            
             mygtk.do(self.display_categories)
@@ -464,6 +472,11 @@ class GenericChannel(object):
         # convert special characters to &xx; escapes
         def xmlentities(self, s):
             return xml.sax.saxutils.escape(s)
+        
+        # Extracts integer from string
+        def to_int(self, s):
+            i = re.findall("\d+", s) or [0]
+            return int(i[0])
 
 
 
diff --git a/channels/dirble.png b/channels/dirble.png
new file mode 100644
index 0000000..ee6337e
Binary files /dev/null and b/channels/dirble.png differ
diff --git a/channels/dirble.py b/channels/dirble.py
new file mode 100644
index 0000000..9d01df5
--- /dev/null
+++ b/channels/dirble.py
@@ -0,0 +1,127 @@
+# encoding: UTF-8
+# api: streamtuner2
+# title: Dirble
+# description: Open radio station directory.
+# version: 0.2
+# type: channel
+# category: radio
+# priority: optional
+# documentation: http://dirble.com/developer/api
+#
+# Provides a nice JSON API, so is easy to support.
+#
+# However useful station information (homepage, etc.) only
+# with extraneous requests. So just for testing as of now.
+#
+# Uh, and API is appearently becoming for-pay (two days
+# after writing this plugin;). So ST2 users may have to
+# request their own Dirble.com key probably.
+#
+
+
+import re
+import json
+from config import conf, dbg, __print__
+from channels import *
+import ahttp as http
+
+
+# Surfmusik sharing site
+class dirble (ChannelPlugin):
+
+    # description
+    title = "Dirble"
+    module = "dirble"
+    homepage = "http://dirble.com/"
+    has_search = True
+    listformat = "audio/x-scpls"
+    titles = dict(listeners=False, playing="Location")
+
+    categories = []
+    config = [
+        {"name": "dirble_api_key",
+         "value": "",
+         "type": "text",
+         "description": "Custom API access key."
+        },
+        {"name": "dirble_fetch_homepage",
+         "value": 0,
+         "type": "boolean",
+         "description": "Also fetch homepages when updating stations. (This is slow, as it requires one extra request for each.)"
+        }
+    ]    
+    catmap = {}
+    
+    base = "http://api.dirble.com/v1/%s/apikey/%s/"
+    cid = "84be582567ff418c9ba94d90d075d7fee178ad60"
+
+
+    # Retrieve cat list and map
+    def update_categories(self):
+        self.categories = []
+        # Main categories
+        for row in self.api("primaryCategories"):
+            self.categories.append(row["name"])
+            self.catmap[row["name"]] = row["id"]
+            # Request subcats
+            sub = []
+            self.categories.append(sub)
+            for subrow in self.api("childCategories", "primaryid", row["id"]):
+                sub.append(subrow["name"])
+                self.catmap[subrow["name"]] = subrow["id"]
+
+
+    # Just copy over stream URLs and station titles
+    def update_streams(self, cat, search=None):
+    
+        if cat:
+            id = self.catmap.get(cat, 0);
+            data = self.api("stations", "id", id)
+        elif search:
+            data = self.api("search", "search", search)
+        else:
+            pass
+
+        r = []
+        for e in data:
+            # skip musicgoal (resolve to just a blocking teaser)
+            if e["streamurl"].find("musicgoal") > 0:
+                continue
+            # append dict after renaming fields
+            r.append(dict(
+                id = e["id"],
+                genre = str(cat),
+                status = e["status"],
+                title = e["name"],
+                playing = e["country"],
+                bitrate = self.to_int(e["bitrate"]),
+                url = e["streamurl"],
+                homepage = e.get("homepage") or self.get_homepage(e["id"], e["name"]),
+                format = "audio/mpeg"
+            ))
+        return r
+
+
+    # Request homepage for stations, else try to deduce Dirble page
+    def get_homepage(self, id, name):
+        if conf.dirble_fetch_homepage:
+            try:
+                return self.api("station", "id", id)["website"]
+            except:
+                None
+        name = re.sub("[^\w\s]+", "", name)
+        name = re.sub("\s", "-", name)
+        return "http://dirble.com/station/" + name.lower();
+
+
+    # Patch API url together, send request, decode JSON and whathaveyou
+    def api(self, *params):
+        method = params[0]
+        try:
+            j = http.get((self.base % (method, conf.dirble_api_key or self.cid)) + "/".join([str(e) for e in params[1:]]))
+            r = json.loads(j);
+        except:
+            r = []
+        return r
+
+
diff --git a/channels/live365.py b/channels/live365.py
index 882441e..f2100c5 100644
--- a/channels/live365.py
+++ b/channels/live365.py
@@ -7,20 +7,18 @@
 # version: 0.3
 # priority: optional
 #
-# 2.1.2 broken,
-# new URLs:
+# 
 #
-#   GET /cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web&caller=&tag=web&station_name=bofbm&_=1404610275892
-#      <NANOCASTER_PARAMS> (session id)
+# We're currently extracting from the JavaScript;
+#
+#    stn.set("param", "value");
+#
+# And using a HTML5 player direct URL now:
+#
+#    /cgi-bin/play.pls?stationid=%s&direct=1&file=%s.pls
 #
-#   GET /play?now=59&membername=&session=1404610276-475426&tag=web&s=bofbm&d=LIVE365&r=0
-#       &app_id=web%3ABROWSER&token=b99d7f579bacab06b9baa1502d53bedc-3101060080001248&AuthType=NORMAL
-#       &lid=276006-deu&SaneID=178.24.130.71-1404610229579
 #
 #
-
-raise Exception
-
 
 
 # streamtuner2 modules
@@ -42,6 +40,7 @@ from itertools import groupby
 from time import time
 from xml.dom.minidom import parseString
 
+
 # channel live365
 class live365(ChannelPlugin):
 
@@ -83,9 +82,12 @@ class live365(ChannelPlugin):
 
     # extract stream infos
     def update_streams(self, cat):
-    
-        url = "http://www.live365.com/genres/%s" % cat.lower()
-        html = http.get(url, feedback=self.parent.status)
+
+        # Retrieve genere index pages    
+        html = ""
+        for i in [1, 17, 33, 49]:
+            url = "http://www.live365.com/cgi-bin/directory.cgi?first=%i&site=web&mode=3&genre=%s&charset=UTF-8&target=content" % (i, cat.lower())
+            html += http.get(url, feedback=self.parent.status)
         
         # Extract from JavaScript       
         rx = re.compile(r"""
@@ -94,65 +96,62 @@ class live365(ChannelPlugin):
 
         # Group entries before adding them
         ls = []
-        for i,g in groupby(rx.findall(html), self.group_by_station):
-            row = dict(g)
+        for i,row in groupby(rx.findall(html), self.group_by_station):
+            row = dict(row)
             ls.append({
+                "status": (None if row["listenerAccess"] == "PUBLIC" else gtk.STOCK_STOP),
+                "deleted": row["status"] != "OK",
                 "name": row["stationName"],
                 "title": row["title"],
-                "playing": "",
+                "playing": "n/a",
                 "id": row["id"],
                 "access": row["listenerAccess"],
                 "status": row["status"],
                 "mode": row["serverMode"],
                 "rating": int(row["rating"]),
-                "rating": row["ratingCount"],
+                #"rating": row["ratingCount"],
                 "listeners": int(row["tlh"]),
                 "location": row["location"],
                 "favicon": row["imgUrl"],
                 "format": self.mediatype,
-                "url": "urn:live365:%s:%s" % (row["id"], row["stationName"])
+                "url": "%scgi-bin/play.pls?stationid=%s&direct=1&file=%s.pls" % (self.base_url, row["id"], row["stationName"])
             })
-        print ls
         return ls
 
+    # itertools.groupby filter
+    gi = 0
+    def group_by_station(self, kv):
+        if kv[0] == "stationName":
+            self.gi += 1
+        return self.gi
+
+
     # inject session id etc. into direct audio url
-    def play(self, row):
+    def UNUSED_play(self, row):
         if row.get("url"):
 
             # params
             id = row["id"]
             name = row["name"]
 
-            # get session
-            mini = "http://www.live365.com/cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web" \
+            # get mini.cgi station resource
+            mini_url = "http://www.live365.com/cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web" \
                  + "&caller=&tag=web&station_name=%s&_=%i111" % (name, time())
-            xml = parseString(http.get(mini)).getElementsByTagName("LIVE365_PLAYER_WINDOW")[0]
-            x = lambda name: xml.getElementsByTagName(name)[0].childNodes[0].data
+            mini_r = http.get(mini_url, content=False)
+            mini_xml = parseString(mini_r.text).getElementsByTagName("LIVE365_PLAYER_WINDOW")[0]
+            mini = lambda name: mini_xml.getElementsByTagName(name)[0].childNodes[0].data
+            
+            # authorize with play.cgi
+            play_url = ""
 
             # mk audio url
-            play =  "http://www.live365.com/play?now=0&" \
-                 + x("NANOCASTER_PARAMS") \
-                 + "&token=" + x("TOKEN") \
+            play =  "http://%s/play" % mini("STREAM_URL") \
+                 + "?now=0&" \
+                 + mini("NANOCASTER_PARAMS") \
+                 + "&token=" + mini("TOKEN") \
                  + "&AuthType=NORMAL&lid=276006-deu&SaneID=178.24.130.71-1406763621701"
-            __print__(dbg.DATA, play)
             
             # let's see what happens
             action.action.play(play, self.mediatype, self.listformat)
 
-            
-
-
-    # itertools.groupby filter
-    gi = 0
-    def group_by_station(self, kv):
-        if kv[0] == "stationName":
-            self.gi += 1
-        return self.gi
-
-    # we can no longer cache all the things
-    def cache(self):
-        pass
-    def save(self):
-        pass
-
 
diff --git a/channels/shoutcast.py b/channels/shoutcast.py
index e8adb26..67e6c57 100644
--- a/channels/shoutcast.py
+++ b/channels/shoutcast.py
@@ -5,7 +5,7 @@
 # type: channel
 # category: radio
 # priority: default
-# version: 1.4
+# version: 1.5
 # depends: pq, re, http
 # author: Mario
 # original: Jean-Yves Lefort
@@ -23,7 +23,7 @@
 # Therefore we'll be retrieving stuff from the homepage still. The new
 # interface conveniently uses JSON already, so let's use that:
 #
-#   POST http://www.shoutcast.com/Home/BrowseByGenre {genreid: 9}
+#   POST http://www.shoutcast.com/Home/BrowseByGenre {genrename: Pop}
 #
 # We do need a catmap now too, but that's easy to aquire and will be kept
 # within the cache dirs.
@@ -74,27 +74,22 @@ class shoutcast(channels.ChannelPlugin):
         html = http.get(self.base_url)
         #__print__( dbg.DATA, html )
         self.categories = []
-
-        # Main genres from mobile dropdown
-        """ <option value="1">Alternative</option> """
-        rx = re.compile(r'<option value="(\d+)">(\w+\h*\w+)<')
-        main = rx.findall(html)
+        
         # Genre list in sidebar
-        """ <li><a href="#c-genre-2" onclick="loadStationsByGenre(2, true)">Adult Alternative</a></li> """
-        rx = re.compile(r'c-genre-(\d+).+?>(\w[\w\h]*\w)<')
+        """  <li><a id="genre-90" href="/Genre?name=Adult" onclick="loadStationsByGenre('Adult', 90, 89); return false;">Adult</a></li> """
+        rx = re.compile(r"loadStationsByGenre\(  '([^']+)' [,\s]* (\d+) [,\s]* (\d+)  \)", re.X)
         subs = rx.findall(html)
-        print main
-        print subs
 
         # group
-        for (id, title) in subs:
-            if (id,title) in main:
+        current = []
+        for (title, id, main) in subs:
+            self.catmap[title] = int(id)
+            if not int(main):
                 self.categories.append(title)
                 current = []
                 self.categories.append(current)
             else:
                 current.append(title)
-            self.catmap[title] = int(id)
         self.save()
 
 
@@ -108,7 +103,7 @@ class shoutcast(channels.ChannelPlugin):
 
         # page
         url = "http://www.shoutcast.com/Home/BrowseByGenre"
-        params = { "genreid": int(id) }
+        params = { "genrename": cat }
         referer = None
         json = http.get(url, params=params, referer=referer, post=1, ajax=1)
         self.parent.status(0.75)
diff --git a/channels/xiph.py b/channels/xiph.py
index e34cc67..9ef9784 100644
--- a/channels/xiph.py
+++ b/channels/xiph.py
@@ -107,17 +107,18 @@ class xiph (ChannelPlugin):
                 #__print__(dbg.DATA, e)
                 bitrate = int(e["bitrate"])
                 if conf.xiph_min_bitrate and bitrate and bitrate >= int(conf.xiph_min_bitrate):
-                  l.append({
-                    "title": e["stream_name"],
-                    "url": e["listen_url"],
-                    "format": e["type"],
-                    "bitrate": int(e["bitrate"]),
-                    "genre": e["genre"],
-                    "playing": e["current_song"],
-                    "listeners": 0,
-                    "max": 0,
-                    "homepage": (e["homepage"] if ("homepage" in e) else ""),
-                  })
+                    if not len(l) or l[-1]["title"] != e["stream_name"]:
+                        l.append({
+                          "title": e["stream_name"],
+                          "url": e["listen_url"],
+                          "format": e["type"],
+                          "bitrate": bitrate,
+                          "genre": e["genre"],
+                          "playing": e["current_song"],
+                          "listeners": 0,
+                          "max": 0,
+                          "homepage": (e["homepage"] if ("homepage" in e) else ""),
+                        })
                 
             # send back the list 
             return l
diff --git a/config.py b/config.py
index 6bdf9e7..b5875ee 100644
--- a/config.py
+++ b/config.py
@@ -214,15 +214,15 @@ def __print__(*args):
 
 # error colorization
 dbg = type('obj', (object,), {
-    "ERR":  "[ERR]",  # red    ERROR
-    "INIT": "[INIT]", # red    INIT ERROR
-    "PROC": "[PROC]", # green  PROCESS
-    "CONF": "[CONF]", # brown  CONFIG DATA
-    "UI":   "[UI]",   # blue   USER INTERFACE BEHAVIOUR
-    "HTTP": "[HTTP]", # magenta HTTP REQUEST
-    "DATA": "[DATA]", # cyan   DATA
-    "INFO": "[INFO]", # gray   INFO
-    "STAT": "[STATE]", # gray  CONFIG STATE
+    "ERR":  r"[ERR]",  # red    ERROR
+    "INIT": r"[INIT]", # red    INIT ERROR
+    "PROC": r"[PROC]", # green  PROCESS
+    "CONF": r"[CONF]", # brown  CONFIG DATA
+    "UI":   r"[UI]",   # blue   USER INTERFACE BEHAVIOUR
+    "HTTP": r"[HTTP]", # magenta HTTP REQUEST
+    "DATA": r"[DATA]", # cyan   DATA
+    "INFO": r"[INFO]", # gray   INFO
+    "STAT": r"[STATE]", # gray  CONFIG STATE
 })
 
 
diff --git a/g.py b/g.py
new file mode 100644
index 0000000..b679756
--- /dev/null
+++ b/g.py
@@ -0,0 +1,15 @@
+from itertools import groupby
+
+ls = [(0,7), (1,7), (0,5), (1,5), (2,5), (0,9), (9,9)]
+
+i = 0
+def grp(kv):
+    global i
+    if kv[0] == 0:
+        i += 1
+    return i
+
+
+for i,row in groupby(ls, grp):
+    print list(row)
+
diff --git a/gtk2.xml b/gtk2.xml
index 56c18e0..a2518f4 100644
--- a/gtk2.xml
+++ b/gtk2.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <requires lib="gtk+" version="2.20"/>
+  <!-- interface-requires gtk+ 3.0 -->
   <!-- interface-naming-policy toplevel-contextual -->
   <object class="GtkListStore" id="config_play">
     <columns>
@@ -47,13 +47,10 @@
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
                 <property name="scrollable">True</property>
-                <property name="homogeneous">True</property>
                 <child>
                   <object class="GtkScrolledWindow" id="scrolledwindow2">
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
-                    <property name="hscrollbar_policy">automatic</property>
-                    <property name="vscrollbar_policy">automatic</property>
                     <child>
                       <object class="GtkViewport" id="viewport2">
                         <property name="visible">True</property>
@@ -82,8 +79,6 @@
                               <object class="GtkScrolledWindow" id="scrolledwindow4">
                                 <property name="visible">True</property>
                                 <property name="can_focus">True</property>
-                                <property name="hscrollbar_policy">automatic</property>
-                                <property name="vscrollbar_policy">automatic</property>
                                 <child>
                                   <object class="GtkTreeView" id="tv_config_player">
                                     <property name="width_request">540</property>
@@ -264,8 +259,6 @@ and <b>%srv</b> to use direct streaming URLs.</property>
                   <object class="GtkScrolledWindow" id="scrolledwindow3">
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
-                    <property name="hscrollbar_policy">automatic</property>
-                    <property name="vscrollbar_policy">automatic</property>
                     <child>
                       <object class="GtkViewport" id="viewport3">
                         <property name="visible">True</property>
@@ -463,7 +456,6 @@ the website extraction method can fix some station list update errors.</small
                                     <property name="width_chars">5</property>
                                     <property name="text" translatable="yes">500</property>
                                     <property name="shadow_type">out</property>
-                                    <property name="invisible_char_set">True</property>
                                     <property name="primary_icon_activatable">False</property>
                                     <property name="secondary_icon_activatable">False</property>
                                     <property name="primary_icon_sensitive">True</property>
@@ -616,7 +608,6 @@ the website extraction method can fix some station list update errors.</small
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="invisible_char">●</property>
-                                    <property name="invisible_char_set">True</property>
                                     <property name="primary_icon_stock">gtk-save-as</property>
                                     <property name="primary_icon_activatable">False</property>
                                     <property name="secondary_icon_activatable">False</property>
@@ -686,7 +677,6 @@ the website extraction method can fix some station list update errors.</small
                                     <property name="editable">False</property>
                                     <property name="invisible_char">●</property>
                                     <property name="width_chars">20</property>
-                                    <property name="invisible_char_set">True</property>
                                     <property name="primary_icon_stock">gtk-home</property>
                                     <property name="primary_icon_activatable">False</property>
                                     <property name="secondary_icon_activatable">False</property>
@@ -804,7 +794,6 @@ or internal functions. Changes take effect after restarting streamtuner2.</prope
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
                                     <property name="invisible_char">●</property>
-                                    <property name="invisible_char_set">True</property>
                                     <property name="primary_icon_activatable">False</property>
                                     <property name="secondary_icon_activatable">False</property>
                                     <property name="primary_icon_sensitive">True</property>
@@ -1093,230 +1082,15 @@ Plugins</property>
     <signal name="delete-event" handler="search_cancel" swapped="no"/>
     <child internal-child="vbox">
       <object class="GtkVBox" id="dialog-vbox1">
-        <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="spacing">2</property>
         <child internal-child="action_area">
           <object class="GtkHButtonBox" id="dialog-action_area1">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="layout_style">end</property>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">False</property>
-            <property name="pack_type">end</property>
-            <property name="position">0</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkVBox" id="vbox1">
-            <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="spacing">20</property>
-            <child>
-              <object class="GtkLabel" id="label2">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="label" translatable="yes"><b><big>search</big></b></property>
-                <property name="use_markup">True</property>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <object class="GtkHBox" id="hbox1">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <child>
-                  <object class="GtkLabel" id="label1sd">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="ypad">10</property>
-                    <property name="label" translatable="yes">for</property>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">0</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEntry" id="search_full">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="has_focus">True</property>
-                    <property name="is_focus">True</property>
-                    <property name="tooltip_text" translatable="yes">A single word to search for in all stations.</property>
-                    <property name="invisible_char">●</property>
-                    <property name="activates_default">True</property>
-                    <property name="primary_icon_activatable">False</property>
-                    <property name="secondary_icon_activatable">False</property>
-                    <property name="primary_icon_sensitive">True</property>
-                    <property name="secondary_icon_sensitive">True</property>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkLabel" id="label4sd">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">2</property>
-                  </packing>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">2</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkHBox" id="hbox2">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <child>
-                  <object class="GtkLabel" id="label3sd">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="xpad">10</property>
-                    <property name="ypad">10</property>
-                    <property name="label" translatable="yes">in</property>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">0</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkRadioButton" id="search_dialog_all">
-                    <property name="label" translatable="yes">all channels</property>
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="receives_default">False</property>
-                    <property name="active">True</property>
-                    <property name="draw_indicator">True</property>
-                    <property name="group">search_dialog_current</property>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkRadioButton" id="search_dialog_current">
-                    <property name="label" translatable="yes">just current</property>
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="receives_default">False</property>
-                    <property name="active">True</property>
-                    <property name="draw_indicator">True</property>
-                    <property name="group">search_dialog_all</property>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">2</property>
-                  </packing>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">3</property>
-              </packing>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <object class="GtkHBox" id="hbox3">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="spacing">20</property>
-                <child>
-                  <object class="GtkButton" id="cache_search">
-                    <property name="label" translatable="yes">Cache _find</property>
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <property name="tooltip_text" translatable="yes">Start searching for above search term in the currently loaded station lists. Doesn't find *new* information, just looks through the known data.</property>
-                    <property name="image">image1</property>
-                    <property name="relief">half</property>
-                    <property name="use_underline">True</property>
-                    <accelerator key="Return" signal="activate"/>
-                    <signal name="clicked" handler="search_go" swapped="no"/>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">0</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkButton" id="server_search">
-                    <property name="label" translatable="yes">Server _search</property>
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="has_focus">True</property>
-                    <property name="is_focus">True</property>
-                    <property name="can_default">True</property>
-                    <property name="has_default">True</property>
-                    <property name="receives_default">True</property>
-                    <property name="tooltip_text" translatable="yes">Instead of doing a cache search, go through the search functions on the directory service homepages. (UNIMPLEMENTED)</property>
-                    <property name="image">image2</property>
-                    <property name="use_underline">True</property>
-                    <signal name="clicked" handler="search_srv" swapped="no"/>
-                  </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">1</property>
-                  </packing>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">5</property>
-              </packing>
-            </child>
           </object>
           <packing>
             <property name="expand">True</property>
             <property name="fill">True</property>
-            <property name="position">1</property>
+            <property name="position">0</property>
           </packing>
         </child>
       </object>
@@ -1408,639 +1182,40 @@ Plugins</property>
     <signal name="delete-event" handler="timer_cancel" swapped="no"/>
     <child internal-child="vbox">
       <object class="GtkVBox" id="dialog-vbox2">
-        <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="spacing">2</property>
         <child internal-child="action_area">
           <object class="GtkHButtonBox" id="dialog-action_area2">
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="GtkWindow" id="url_action_container">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkVBox" id="url_action_list">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkLabel" id="url_action_1_online_forum">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="layout_style">end</property>
-            <child>
-              <object class="GtkButton" id="timer_cancel">
-                <property name="label" translatable="yes">cancel</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <signal name="clicked" handler="timer_cancel" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkButton" id="timer_ok">
-                <property name="label" translatable="yes">ok</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <signal name="clicked" handler="timer_ok" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">1</property>
-              </packing>
-            </child>
+            <property name="label" translatable="yes">http://sourceforge.net/projects/streamtuner2/forums/forum/1173108</property>
           </object>
           <packing>
-            <property name="expand">False</property>
+            <property name="expand">True</property>
             <property name="fill">True</property>
-            <property name="pack_type">end</property>
             <property name="position">0</property>
           </packing>
         </child>
         <child>
-          <object class="GtkTable" id="table2">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="n_rows">3</property>
-            <property name="n_columns">3</property>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <object class="GtkEntry" id="timer_value">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="invisible_char">●</property>
-                <property name="text" translatable="yes">Fri,Sat 20:00-21:00</property>
-                <property name="primary_icon_activatable">False</property>
-                <property name="secondary_icon_activatable">False</property>
-                <property name="primary_icon_sensitive">True</property>
-                <property name="secondary_icon_sensitive">True</property>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="right_attach">2</property>
-                <property name="top_attach">1</property>
-                <property name="bottom_attach">2</property>
-              </packing>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-            <child>
-              <placeholder/>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">True</property>
-            <property name="fill">True</property>
-            <property name="position">1</property>
-          </packing>
-        </child>
-      </object>
-    </child>
-    <action-widgets>
-      <action-widget response="0">timer_cancel</action-widget>
-      <action-widget response="0">timer_ok</action-widget>
-    </action-widgets>
-  </object>
-  <object class="GtkWindow" id="url_action_container">
-    <property name="can_focus">False</property>
-    <child>
-      <object class="GtkVBox" id="url_action_list">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <child>
-          <object class="GtkLabel" id="url_action_1_online_forum">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="label" translatable="yes">http://sourceforge.net/projects/streamtuner2/forums/forum/1173108</property>
-          </object>
-          <packing>
-            <property name="expand">True</property>
-            <property name="fill">True</property>
-            <property name="position">0</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkLabel" id="url_action_2_fossil_wiki">
+          <object class="GtkLabel" id="url_action_2_fossil_wiki">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="label" translatable="yes">http://fossil.include-once.org/streamtuner2/</property>
@@ -2656,6 +1831,56 @@ Plugins</property>
                               </object>
                             </child>
                             <child>
+                              <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Channel tab position</property>
+                                <property name="use_underline">True</property>
+                                <child type="submenu">
+                                  <object class="GtkMenu" id="menu4">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs1">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Top</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_top" swapped="no"/>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs2">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Left</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_left" swapped="no"/>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs3">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Bottom</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_bottom" swapped="no"/>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs4">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Right</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_right" swapped="no"/>
+                                      </object>
+                                    </child>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                            <child>
                               <object class="GtkMenuItem" id="menuitem15savestates">
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
diff --git a/gtk3.xml b/gtk3.xml
index a55dca2..6c15bc2 100644
--- a/gtk3.xml
+++ b/gtk3.xml
@@ -1898,6 +1898,42 @@ Plugins</property>
               <placeholder/>
             </child>
             <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
               <object class="GtkEntry" id="timer_value">
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
@@ -2015,6 +2051,12 @@ Plugins</property>
             <child>
               <placeholder/>
             </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
           </object>
           <packing>
             <property name="expand">True</property>
@@ -2643,6 +2685,56 @@ Plugins</property>
                               </object>
                             </child>
                             <child>
+                              <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Channel tab position</property>
+                                <property name="use_underline">True</property>
+                                <child type="submenu">
+                                  <object class="GtkMenu" id="menu4">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs1">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Top</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_top" swapped="no"/>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs2">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Left</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_left" swapped="no"/>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs3">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Bottom</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_bottom" swapped="no"/>
+                                      </object>
+                                    </child>
+                                    <child>
+                                      <object class="GtkMenuItem" id="menu_entry_switch_channel_tabs4">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="label" translatable="yes">Right</property>
+                                        <property name="use_underline">True</property>
+                                        <signal name="activate" handler="menu_notebook_pos_right" swapped="no"/>
+                                      </object>
+                                    </child>
+                                  </object>
+                                </child>
+                              </object>
+                            </child>
+                            <child>
                               <object class="GtkMenuItem" id="menuitem15savestates">
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
diff --git a/mygtk.py b/mygtk.py
index c3d5f69..d127abe 100644
--- a/mygtk.py
+++ b/mygtk.py
@@ -301,9 +301,12 @@ class mygtk:
                 # gtk.Notebook
                 if t == gtk.Notebook:
                     r[wn]["page"] = w.get_current_page()
+                    r[wn]["tab_pos"] = int(w.get_tab_pos())
             #print(r)
             return r
 
+        gtk_position_type_enum = [gtk.POS_LEFT, gtk.POS_RIGHT, gtk.POS_TOP, gtk.POS_BOTTOM]
+
 
         #-- restore window and widget properties
         #
@@ -341,6 +344,8 @@ class mygtk:
                     # gtk.Notebook
                     if method == "page":
                         w.set_current_page(args)
+                    if method == "tab_pos":
+                        w.set_tab_pos(r[wn]["tab_pos"])
 
             pass
 
diff --git a/st2.py b/st2.py
index 79d8cec..5ec8fb8 100755
--- a/st2.py
+++ b/st2.py
@@ -5,10 +5,10 @@
 # title: streamtuner2
 # description: Directory browser for internet radio / audio streams
 # depends: pygtk | pygi, threading, pyquery, kronos, requests
-# version: 2.1.2
+# version: 2.1.3
 # author: mario salzer
 # license: public domain
-# url: http://freshmeat.net/projects/streamtuner2
+# url: http://freshcode.club/projects/streamtuner2
 # config: <env name="http_proxy" value="" description="proxy for HTTP access" />  <env name="XDG_CONFIG_HOME" description="relocates user .config subdirectory" />
 # category: multimedia
 # 
@@ -96,7 +96,7 @@ from channels import *
 import favicon
 
 
-__version__ = "2.1.2"
+__version__ = "2.1.3"
 
 
 # this represents the main window
@@ -193,6 +193,10 @@ class StreamTunerTwo(gtk.Builder):
                 "menu_toolbar_size_small": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)),
                 "menu_toolbar_size_medium": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_DND)),
                 "menu_toolbar_size_large": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_DIALOG)),
+                "menu_notebook_pos_top": lambda w: self.notebook_channels.set_tab_pos(2),
+                "menu_notebook_pos_left": lambda w: self.notebook_channels.set_tab_pos(0),
+                "menu_notebook_pos_right": lambda w: self.notebook_channels.set_tab_pos(1),
+                "menu_notebook_pos_bottom": lambda w: self.notebook_channels.set_tab_pos(3),
                 # win_config
                 "menu_properties": config_dialog.open,
                 "config_cancel": config_dialog.hide,
@@ -234,8 +238,6 @@ class StreamTunerTwo(gtk.Builder):
             self.win_streamtuner2.show()
             
 
-          
-
         #-- Shortcut for glade.get_widget()
         # Allows access to widgets as direct attributes instead of using .get_widget()
         # Also looks in self.channels[] for the named channel plugins
@@ -245,29 +247,24 @@ class StreamTunerTwo(gtk.Builder):
             else:
                 return self.get_object(name)   # or gives an error if neither exists
 
-
-        # custom-named widgets are available from .widgets{} not via .get_widget()
+        # Custom-named widgets are available from .widgets{} not via .get_widget()
         def get_widget(self, name):
             if name in self.widgets:
                 return self.widgets[name]
             else:
                 return gtk.Builder.get_object(self, name)
                 
-
-
-                
-        # returns the currently selected directory/channel object
+        # returns the currently selected directory/channel object (remembered position)
         def channel(self):
             return self.channels[self.current_channel]
 
-            
+        # returns the currently selected directory/channel object (from gtk)
         def current_channel_gtk(self):
             i = self.notebook_channels.get_current_page()
             try: return self.channel_names[i]
             except: return "bookmarks"
 
-
-        # notebook tab clicked
+        # Notebook tab clicked
         def channel_switch(self, notebook, page, page_num=0, *args):
 
             # can be called from channelmenu as well:
@@ -285,14 +282,12 @@ class StreamTunerTwo(gtk.Builder):
             except:
                 __print__(dbg.INIT, "channel .first_show() initialization error")
 
-
-        # convert ListStore iter to row number
+        # Convert ListStore iter to row number
         def rowno(self):
             (model, iter) = self.model_iter()
             return model.get_path(iter)[0]
 
-
-        # currently selected entry in stations list, return complete data dict
+        # Currently selected entry in stations list, return complete data dict
         def row(self):
             return self.channel().stations() [self.rowno()]
 
@@ -300,44 +295,37 @@ class StreamTunerTwo(gtk.Builder):
         # return ListStore object and Iterator for currently selected row in gtk.TreeView station list
         def model_iter(self):
             return self.channel().gtk_list.get_selection().get_selected()
-
             
-        # fetches a single varname from currently selected station entry
+        # Fetches a single varname from currently selected station entry
         def selected(self, name="url"):
             return self.row().get(name)
 
-                
 
-
-
-        # play button
+                
+        # Play button
         def on_play_clicked(self, widget, event=None, *args):
             row = self.row()
             if row:
                 self.channel().play(row)
                 [callback(row) for callback in self.hooks["play"]]
 
-
-        # streamripper
+        # Recording: invoke streamripper for current stream URL
         def on_record_clicked(self, widget):
             row = self.row()
             action.record(row.get("url"), row.get("format", "audio/mpeg"), "url/direct", row=row)
 
-
-        # browse stream
+        # Open stream homepage in web browser
         def on_homepage_stream_clicked(self, widget):
             url = self.selected("homepage")             
             action.browser(url)
 
-             
-        # browse channel
+        # Browse to channel homepage (double click on notebook tab)
         def on_homepage_channel_clicked(self, widget, event=2):
             if event == 2 or event.type == gtk.gdk._2BUTTON_PRESS:
                 __print__(dbg.UI, "dblclick")
                 action.browser(self.channel().homepage)            
 
-
-        # reload stream list in current channel-category
+        # Reload stream list in current channel-category
         def on_reload_clicked(self, widget=None, reload=1):
             __print__(dbg.UI, "reload", reload, self.current_channel, self.channels[self.current_channel], self.channel().current)
             category = self.channel().current
@@ -345,30 +333,26 @@ class StreamTunerTwo(gtk.Builder):
                 lambda: (  self.channel().load(category,reload), reload and self.bookmarks.heuristic_update(self.current_channel,category)  )
             )
 
-            
-        # thread a function, add to worker pool (for utilizing stop button)
+        # Thread a function, add to worker pool (for utilizing stop button)
         def thread(self, target, *args):
             thread = Thread(target=target, args=args)
             thread.start()
             self.working.append(thread)
 
-
-        # stop reload/update threads
+        # Stop reload/update threads
         def on_stop_clicked(self, widget):
             while self.working:
                 thread = self.working.pop()
                 thread.stop()
 
-        
-        # click in category list
+        # Click in category list
         def on_category_clicked(self, widget, event, *more):
             category = self.channel().currentcat()
             __print__(dbg.UI, "on_category_clicked", category, self.current_channel)
             self.on_reload_clicked(None, reload=0)
             pass
 
-
-        # add current selection to bookmark store
+        # Add current selection to bookmark store
         def bookmark(self, widget):
             self.bookmarks.add(self.row())
             # code to update current list (set icon just in on-screen liststore, it would be updated with next display() anyhow - and there's no need to invalidate the ls cache, because that's referenced by model anyhow)
@@ -380,19 +364,16 @@ class StreamTunerTwo(gtk.Builder):
             # refresh bookmarks tab
             self.bookmarks.load(self.bookmarks.default)
 
-
-        # reload category tree
+        # Reload category tree
         def update_categories(self, widget):
             Thread(target=self.channel().reload_categories).start()
-            
 
-        # menu invocation: refresh favicons for all stations in current streams category
+        # Menu invocation: refresh favicons for all stations in current streams category
         def update_favicons(self, widget):
             entries = self.channel().stations()
             favicon.download_all(entries)
 
-
-        # save a file            
+        # Save stream to file (.m3u)
         def save_as(self, widget):
             row = self.row()
             default_fn = row["title"] + ".m3u"
@@ -401,23 +382,24 @@ class StreamTunerTwo(gtk.Builder):
                 action.save(row, fn)
             pass
 
-
-        # save current stream URL into clipboard
+        # Save current stream URL into clipboard
         def menu_copy(self, w):
             gtk.clipboard_get().set_text(self.selected("url"))
 
-
-        # remove an entry
+        # Remove a stream entry
         def delete_entry(self, w):
             n = self.rowno()
             del self.channel().stations()[ n ]
             self.channel().switch()
             self.channel().save()
 
-
-        # stream right click
+        # Richt clicking a stream opens an action content menu
         def station_context_menu(self, treeview, event):
             return station_context_menu(treeview, event) # wrapper to the static function
+
+        # Alternative Notebook channel tabs between TOP and LEFT position
+        def switch_notebook_tabs_position(self, w, pos):
+            self.notebook_channels.set_tab_pos(pos);
             
 
 
@@ -1190,6 +1172,7 @@ if __name__ == "__main__":
         # run
         gui_startup(100/100.0)
         gtk.main()
+        __print__(dbg.PROC, r" gtk_main_quit ")
         
         
     # invoke command-line interface

-- 
streamtuner2 packaging



More information about the pkg-multimedia-commits mailing list