Index: channels/__init__.py
==================================================================
--- channels/__init__.py
+++ channels/__init__.py
@@ -7,16 +7,17 @@
# version: 1.5
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
-# bookmarks.py configwin.py streamedit.py history.py search.py links.py
-# internet_radio.py itunes.py jamendo.py live365.py global_key.py
-# modarchive.py myoggradio.py punkcast.py radiobrowser.py radiotray.py
-# shoutcast.py surfmusik.py timer.py tunein.py xiph.py youtube.py
-# exportcat.py useragentswitcher.py somafm.py dnd.py ubuntuusers.py
-# dirble.py filtermusic.py
+# bookmarks.py, configwin.py, dirble.py, dnd.py, exportcat.py,
+# filtermusic.py, global_key.py, history.py, internet_radio.py,
+# itunes.py, jamendo.py, links.py, live365.py, modarchive.py,
+# myoggradio.py, pluginmanager2.py, radiobrowser.py, radionomy.py,
+# radiotray.py, search.py, shoutcast.py, somafm.py, streamedit.py,
+# surfmusik.py, timer.py, tunein.py, ubuntuusers.py,
+# useragentswitcher.py, xiph.py, youtube.py
# config: -
# priority: core
#
# GenericChannel implements the basic GUI functions and defines
# the default channel data structure. It implements fallback logic
DELETED channels/file.py
Index: channels/file.py
==================================================================
--- channels/file.py
+++ channels/file.py
@@ -1,203 +0,0 @@
-# api: streamtuner2
-# title: File browser
-# description: Displays mp3/oggs or m3u/pls files from local media file directories.
-# type: channel
-# category: local
-# version: 0.2
-# priority: optional
-# status: unsupported
-# depends: python:mutagen, python:id3
-# config:
-# { name: file_browser_dir, type: text, value: "$XDG_MUSIC_DIR, ~/MP3", description: "List of directories to scan for audio files." },
-# { name: file_browser_ext, type: text, value: "mp3,ogg, m3u,pls,xspf, avi,flv,mpg,mp4", description: "File type/extension filter." },
-# png:
-# iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wUFDQsK23vYngAAA6lJREFUOMtFz8tu1FYAxvH/sc+xxx7bM8lkElAgISoV4qIgpEaomwg2bMqiQmo3lfowPElVVbQr3gBVqGJRGi6VEERFDROS0Ewmc/N47Bkf+7gLevke4Kfv
-# L374/vufxm/ffjnudJQoisoVonLDcN+sr39z7uXLXzfOn6cRhmSTCf3TUwa9HoN+H601ynXxwhA7ivhdCH48PkZOut0vdh48cKtej9C2iSyLsF7/xLpx4/5eu/1LZzSiaQx1z8O5cIHapUvVmpRkaUqcJMRpSjydmnEcP/2zKH6WOs/7mdZBVVXIqsKuKk4nE4qdnTueUncCpZh5HoHj4AqBVRQIrUFrLGPwlaLhOLiWtX93OPxKIuX+0HHWJ5ZF03VxpaRfFLRmM1rTKUGrRX1piUYQEIUhfhRRC0OqOMZxHIIwJIgiHFj77OnT+7KEd0Wt
-# tl0Kgev7NHyf5apCTSaoLEMlCZdu3uT85ib1hQWi1VWcMCTb3cXxPJxmExlF2LWaMFpvS4TYcx2nmpalsKoKJQSNMCQ6cwZPCMxgwKTbpfPoETWlWDx3jtblyyghEFpjSYmwbYQx6DyPJUJ0bKUKU5Yqn83QSqGVolQKGQSEGxssrK3RffyY6Zs3zK9epb6wgBWGCCFQjkOlFFVRoIfDkTRwYIRIDTR0nqPnc7RSFP/AhW2jfJ/WlSvQ7zPe3SU9OECdPYuwbUrXxSiFsW2KOB7IaZYdp3keV9DQ8znacSj+RaWkqtVACExR0NrcZPriBflw
-# SJqmWO32R9BxMEAxnfatJE37cZb1jWVRaP3x4XxOkecIy0J6HibPmY1GqHqdpa0tbNdlvLNDfnJCOR5jJhPMYECZpj2JlPEwSbqBEFTG/Jd85fZtNm/dIjk6Itnbw19e5tN79yiHQ5JnzxgfHDBrNvGjCOM4lElCkWWn1ufb2/M4TQ9LIQAoiwJTlrQ3NrCVQjoOw04Hf2UF5ftI3wchsOt1Zu/eoTsd9KtX5E+eYCaTnux2u1VRVe8N/y/PMn57+JBka4v04IDs8JDD+RxGI8RoxPz1a6yyJJ/PGT9/rivXzZRSp3m9/kYAtFdWvm3PZt+1
-# kkQEtk1g2wRSEihV+badR7Va2vS8ceR5A9uYrimKv/LZ7MhU1VF7efnD2sWL3cb6+slzrT9IgKXFxT8W07S33GpZywsLHxzLep/Gcac3Gu0H9fp7Z3X1KLp27WT9+vXh8epq8vXdu7N+FPG61UIXBXZZAmCVJX8DADze5LjPkMQAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDUtMDVUMTU6MTA6NTkrMDI6MDBD/PY6AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA1LTA1VDE1OjEwOjU5KzAyOjAwMqFOhgAAAABJRU5ErkJggg==
-# png-orig:
-# https://openclipart.org/detail/168001/folder-icon-red-music
-#
-# Local file browser. Presents files from configured directories.
-# This is not what streamtuner2 is meant for. Therefore this is
-# an optional plugin, and not overly well integrated.
-#
-# Bugs:
-# Only loads directories on startup. Doesn't work when post-activated
-# per pluginmanager2 for instance. And LANG=C breaks it on startup,
-# if media directories contain anything but ASCII filenames.
-
-
-# modules
-import os
-import re
-
-from channels import *
-from config import *
-
-# ID3 libraries
-try:
- from mutagen import File as get_meta
-except:
- try:
- from ID3 import ID3
- log.INFO("Just basic ID3 support")
- get_meta = lambda fn: dict([(k.lower(),v) for k,v in ID3(fn).iteritems()])
- except:
- log.INIT("You are out of luck in regards to mp3 browsing. No ID3 support.")
- get_meta = lambda *x: {}
-
-
-# work around mutagens difficult interface
-def mutagen_postprocess(d):
- if d.get("TIT2"):
- return {
- "encoder": d["TENC"][0],
- "title": d["TIT2"][0],
- "artist": d["TPE1"][0],
-# "tyer?????????????": d["TYER"][0],
-# "track": d["TRCK"][0],
- "album": d["TALB"][0],
- }
- else:
- return d
-
-
-
-
-# file browser / mp3 directory listings
-class file (ChannelPlugin):
-
- # data
- listtype = "href"
- streams = {}
- categories = []
- dir = []
- ext = []
-
- # display
- datamap = [ # coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ...
- ["", 20, ["state", str, "pixbuf", {}], ],
- ["Genre", 65, ['genre', str, "t", {"editable":8}], ],
- ["File", 160, ["filename", str, "t", {"strikethrough":10, "cell-background":11, "cell-background-set":12}], ],
- ["Title", 205, ["title", str, "t", {"editable":8}], ],
- ["Artist", 125, ["artist", str, "t", {"editable":8}], ],
- ["Album", 125, ["album", str, "t", {"editable":8}], ],
- ["Bitrate", 35, ["bitrate", int, "t", {}], ],
- ["Format", 50, ["format", str, None, {}], ],
- [False, 0, ["editable", bool, None, {}], ],
- [False, 0, ["favourite", bool, None, {}], ],
- [False, 0, ["deleted", bool, None, {}], ],
- [False, 0, ["search_col", str, None, {}], ],
- [False, 0, ["search_set", bool, None, {}], ],
- ]
- rowmap = []
-
-
-
- # prepare
- def __init__(self, parent):
-
- # data dirs
- self.dir = [self.env_dir(s) for s in conf.file_browser_dir.split(",")]
- self.ext = [s.strip() for s in conf.file_browser_ext.split(",")]
- # first run
- if not self.categories or not self.streams:
- self.scan_dirs()
-
- # draw gtk lists
- ChannelPlugin.__init__(self, parent)
-
- # make editable
- #{editable:8}
-
- # add custom context menu
- #self.gtk_list.connect('button-press-event', self.context_menu)
-
-
- # Interpolate $VARS and XDG_SPECIAL_DIRS
- def env_dir(self, path):
- path = path.strip()
- env = self.fvars()
- # Replace $XDG_ ourselfes and normal $ENV vars per expandvars (because os.environ.update() doesn't do)
- path = re.sub("\$(XDG\w+)", lambda m: env.get(m.group(1), m.group(0)), path)
- path = os.path.expandvars(path)
- return os.path.expanduser(path)
-
- # Read user-dirs config
- def fvars(self, fn="$HOME/.config/user-dirs.dirs"):
- fn = os.path.expandvars(fn)
- src = open(fn, "r").read() if os.path.exists(fn) else ""
- env = re.findall('^(\w+)=[\"\']?(.+?)[\"\']?', src, re.M) # pyxdg: Your move.
- return dict(env)
-
-
- # don't load cache file
- cache = lambda *x: None
-
-
- # read dirs
- def scan_dirs(self):
- self.categories = []
-
- # add main directory
- for main in self.dir:
- if os.path.exists(main):
- self.categories.append(main)
-
- # prepare subdirectories list
- sub = []
- self.categories.append(sub)
-
- # look through
- for dir, subdirs, files in os.walk(main):
- name = os.path.basename(dir)
- sfx = ""
- while name+sfx in self.categories:
- sfx = str(int(sfx)+1) if sfx else "2"
- name += sfx
-
- # files in subdir
- if files:
- sub.append(name)
- self.streams[name] = [self.file_entry(fn, dir) for fn in files if self.we_like_that_extension(fn)]
-
- # plant a maindir reference to shortname
- main_base = os.path.basename(main)
- if self.streams.get(main_base):
- self.streams[main] = self.streams[main_base]
-
-
- # extract meta data
- def file_entry(self, fn, dir):
- # basic data
- meta = {
- "title": fn,
- "filename": fn,
- "url": "file://" + dir + "/" + fn,
- "genre": "",
- "format": self.mime_fmt(fn[-3:]),
- "editable": True,
- }
- # add ID3
- meta.update(mutagen_postprocess(get_meta(dir + "/" + fn) or {}))
- return meta
-
- # check fn for .ext
- def we_like_that_extension(self, fn):
- return fn[-3:] in self.ext
-
-
-
- # same as init
- def update_categories(self):
- self.scan_dirs()
-
-
- # same as init
- def update_streams(self, cat, x=0):
- self.scan_dirs()
- return self.streams.get(os.path.basename(cat))
-
-
DELETED channels/punkcast.py
Index: channels/punkcast.py
==================================================================
--- channels/punkcast.py
+++ channels/punkcast.py
@@ -1,91 +0,0 @@
-
-# api: streamtuner2
-# title: PunkCast
-# description: Online video site that covered NYC artists. Not updated anymore.
-# type: channel
-# category: video
-# version: 0.2
-# url: http://www.punkcast.com/
-# png:
-# iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAyxJREFUOI0FwUlvG2UAgOH3++abxfsy2dtmcRsnghpBKyKhQA7lgsShPwEk/gVcKn4HN+6AynKIkEgFUkFF
-# okrlpEkap2nqxHFsx/bYHs/m4XmE0s1Yyhjf8/niy6/49utv+PPf57x4sk3083cUzZgwZWElE9hL60yyc1zU39A8PsRxhijLMBFahO/5dFotwvGIeDxkOHJwNYvCwgyRYeElslTPu7ivTnFaTU6dkG4E
-# ytANoskYgMb5W64b51jEuJ6Lq3Rmy2tEUcz1WR1pGGiyiIhMDOlQGHZRQRAQRR4CqJ2eUNvbJcAiEYz45NMtKrdvkc8a1G5M81e1RuiGjDpt1CTASKeQytDxowmmZdLpdDl8+QJLBqTxKSYkc0afbOyz
-# OD/F/fIynhA0wwChazhIpOuNYCJI6hpxPOHiqk1K8+ldthDumFhXnLUHjEZj9DggKyWfbWywPpXHVqBEOKayXubDyl1qx0f0eh1E6GLaRX7aO8NLJjG9PvNLt0kpuJWeMHPTxnan6V81UWmpYacTVN57
-# H9uewu01+OPJDle9AY//O+Dj++9yr3wHUnl8PyCTyzL2PE5aPUgYaFlNPDKigGe7VW6U1iDwcXuXzBbz+K5PThMEvQ6xjJjEAi2Z4Wn1mP03FyR1DaU0jUbnGj81IZXLkckoSpUV7JTB0HF4urvH3IMN
-# jG6XbGGWemeAkc+hNEE0DtCSmngUCkFSixn321iWyVzGImEIVssllAh5e9kkCDV2T05pD4dMTeWpv6ohwghp6opCOOHh1kfcXZzm8S/bPNs/ZOC4mELj4eYGWiT54Z896qMJ9vws4XBE3lRIBeqmDmk/
-# Qmnw+YNNlsur/PrbDqv5NFm7SHMwpNbuMggClmYL5JWkqytErKGPPVQmilhLSpbtBE1nxMFhjedHrymkNUrv3KHTuqLvjWl7MQevL+gNXaqNLlGzSyUvUVuEzMzkWVhZYqd6xPc//o6MBfWrAZcdBxnH
-# zOcy/F1rsf3yFFmtEcWwOZ2h1R8hP5jATGmRQE+wXz0gjiMWihnO2z1qjS6ZQoF76yuU7DSxFFhJC10phmHIhVT8D1yefHn5PzXrAAAAAElFTkSuQmCC
-# priority: obsolete
-# config: { name: punkcast_img, type: boolean, value: 0, description: Load banners. (Channel - Update favicons) }
-#
-# Punkcast is no longer updated. This plugin is kept for
-# historic reasons. It was one of the default streamtuner1
-# channels.
-
-
-import re
-import ahttp
-from config import conf
-import action
-from channels import *
-from config import *
-
-
-# basic.ch broadcast archive
-class punkcast (ChannelPlugin):
-
- # keeps category titles->urls
- catmap = {}
- categories = ["list"]
- titles = dict(playing=False, listeners=False, bitrate=False, homepage=False)
-
-
- # don't do anything
- def update_categories(self):
- pass
-
-
- # get list
- def update_streams(self, cat):
-
- rx_link = re.compile("""
-
- .*? ALT="([^<">]+)"
- """, re.S|re.X)
-
- entries = []
-
- #-- all from frontpage
- html = ahttp.get("http://www.punkcast.com/")
- for uu in rx_link.findall(html):
- (homepage, id, title) = uu
- entries.append({
- "genre": "%s" % id,
- "title": title,
- "playing": "PUNKCAST #%s" % id,
- "format": "audio/mpeg",
- "url": "none:",
- "homepage": homepage,
- "img": "http://punkcast.com/%s/PUNK%s.jpg" % (id, id) if conf.punkcast_img else None,
- })
-
- # done
- return entries
-
-
- # special handler for play
- def play(self, row):
-
- rx_sound = re.compile("""(http://[^"<>]+[.](mp3|ogg|m3u|pls|ram))""")
- html = ahttp.get(row["homepage"])
-
- # look up ANY audio url
- for uu in rx_sound.findall(html):
- log.DATA( uu )
- (url, fmt) = uu
- action.play(url, self.mime_fmt(fmt), "srv")
- return
-
- # or just open webpage
- action.browser(row["homepage"])
-
Index: channels/xiph.py
==================================================================
--- channels/xiph.py
+++ channels/xiph.py
@@ -6,11 +6,11 @@
# url: http://dir.xiph.org/
# version: 0.5
# category: radio
# config:
# { name: xiph_min_bitrate, value: 64, type: int, description: "Minimum bitrate; filter lesser quality streams.", category: filter }
-# { name: xiph_source, value: cache, type: select, select: "cache=JSON cache srv|xml=Clunky XML blob|web=Forbidden fruits", description: "Source for station list extraction." }
+# { name: xiph_source, value: web, type: select, select: "cache=JSON cache srv|xml=Clunky XML blob|web=Forbidden fruits", description: "Source for station list extraction." }
# priority: standard
# png:
# iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAg5JREFUOI2lk1tIE2AUx3+7CG1tlmlG1rSEHrKgEUF7yO40taQiRj10I4qKkOaT4hIUItuTkC8hpJAQtJCICrFpzEKw
# h61eQorGNBOTzbEt16ZrnR5Wq3mZD/3heziX//983znngyyov+eSbHEA5WKBhs4BKVy9gsqajqwiCwo0dA5IQX5u2s4moliMPPV1nCeDzxgNBFDHE2wsKMPzsGVefobjcnO7RMfeMuL341ZBrNEGRmPqqjdvsbbf
# w7irO4Oj+rdywNNNucmERsLUVndR8uYRU13PCew6hpgP8W02xMpIsik++qk5oweW6y3yob8WnXacZDKJWh1Cp4OtRUHsh19TUlUGViv09RGqKAenU5QnLKm+rK88LjgcUnxmr/h8iNO5XYJBRAQZ/qiVeptGWjty
@@ -17,14 +17,14 @@
# 5cClDWLwugQRIRiU5UdPCoD6S89jhV6pks9WG6fuwtBtF5v72vC1v+B86SsM+jD56hjnyiM0lRrAbofeXjQJLdE/78jbXSU5166I6f5VeeDdKdq6GtlSd0QkVU+8XsQhlt9W6izbZ5aMKWgtp2WT/yUHd0xSYU7i
# dsPQ+1WMKIsJD08wEV2HGLeRyNMjawqRxhuKBfdgz1m7fI/4mVX+ZGxmgniOoJv+QZHGAMC7p60ZnHkC8HfzZmLTBCd9af9ccnqMc9HTdmFe4kLkJbH/4h0xVtcu+SP/C78AL6btab6woPcAAAAASUVORK5CYII=
#
# Xiph.org maintains the Ogg streaming standard and Vorbis,
# Opus, FLAC audio, and Theora video compression formats.
-# The ICEcast server is a modern alternative to SHOUTcast.
+# The ICEcast server is an open alternative to SHOUTcast.
#
# It also provides a directory listing of known internet
-# radio stations, only a handful of them using Ogg though.
+# radio stations; only a handful of them using Ogg though.
# The category list is hardwired in this plugin. And there
# are three station fetching modes now:
#
# → "JSON cache" retrieves a refurbished JSON station list,
# both sliceable genres and searchable.
@@ -32,12 +32,12 @@
# → "Clunky XML" fetches the olden YP.XML, which is really
# slow, then slices out genres. No search. With the secret
# "buffy" mode keeps all streams buffered.
#
# → "Forbidden Fruits" extracts from dir.xiph.org HTML pages,
-# with homepages and listener/max infos available. Search
-# is also possible.
+# with homepages and listener/max infos available. Also
+# enables live server searching.
#
# The bitrate filter can strip any low-quality entries, but
# retains `0` entries (which just lack meta information and
# aren't necessarily low-bitrate.)
ADDED contrib/file.py
Index: contrib/file.py
==================================================================
--- contrib/file.py
+++ contrib/file.py
@@ -0,0 +1,203 @@
+# api: streamtuner2
+# title: File browser
+# description: Displays mp3/oggs or m3u/pls files from local media file directories.
+# type: channel
+# category: local
+# version: 0.2
+# priority: optional
+# status: unsupported
+# depends: python:mutagen, python:id3
+# config:
+# { name: file_browser_dir, type: text, value: "$XDG_MUSIC_DIR, ~/MP3", description: "List of directories to scan for audio files." },
+# { name: file_browser_ext, type: text, value: "mp3,ogg, m3u,pls,xspf, avi,flv,mpg,mp4", description: "File type/extension filter." },
+# png:
+# iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wUFDQsK23vYngAAA6lJREFUOMtFz8tu1FYAxvH/sc+xxx7bM8lkElAgISoV4qIgpEaomwg2bMqiQmo3lfowPElVVbQr3gBVqGJRGi6VEERFDROS0Ewmc/N47Bkf+7gLevke4Kfv
+# L374/vufxm/ffjnudJQoisoVonLDcN+sr39z7uXLXzfOn6cRhmSTCf3TUwa9HoN+H601ynXxwhA7ivhdCH48PkZOut0vdh48cKtej9C2iSyLsF7/xLpx4/5eu/1LZzSiaQx1z8O5cIHapUvVmpRkaUqcJMRpSjydmnEcP/2zKH6WOs/7mdZBVVXIqsKuKk4nE4qdnTueUncCpZh5HoHj4AqBVRQIrUFrLGPwlaLhOLiWtX93OPxKIuX+0HHWJ5ZF03VxpaRfFLRmM1rTKUGrRX1piUYQEIUhfhRRC0OqOMZxHIIwJIgiHFj77OnT+7KEd0Wt
+# tl0Kgev7NHyf5apCTSaoLEMlCZdu3uT85ib1hQWi1VWcMCTb3cXxPJxmExlF2LWaMFpvS4TYcx2nmpalsKoKJQSNMCQ6cwZPCMxgwKTbpfPoETWlWDx3jtblyyghEFpjSYmwbYQx6DyPJUJ0bKUKU5Yqn83QSqGVolQKGQSEGxssrK3RffyY6Zs3zK9epb6wgBWGCCFQjkOlFFVRoIfDkTRwYIRIDTR0nqPnc7RSFP/AhW2jfJ/WlSvQ7zPe3SU9OECdPYuwbUrXxSiFsW2KOB7IaZYdp3keV9DQ8znacSj+RaWkqtVACExR0NrcZPriBflw
+# SJqmWO32R9BxMEAxnfatJE37cZb1jWVRaP3x4XxOkecIy0J6HibPmY1GqHqdpa0tbNdlvLNDfnJCOR5jJhPMYECZpj2JlPEwSbqBEFTG/Jd85fZtNm/dIjk6Itnbw19e5tN79yiHQ5JnzxgfHDBrNvGjCOM4lElCkWWn1ufb2/M4TQ9LIQAoiwJTlrQ3NrCVQjoOw04Hf2UF5ftI3wchsOt1Zu/eoTsd9KtX5E+eYCaTnux2u1VRVe8N/y/PMn57+JBka4v04IDs8JDD+RxGI8RoxPz1a6yyJJ/PGT9/rivXzZRSp3m9/kYAtFdWvm3PZt+1
+# kkQEtk1g2wRSEihV+badR7Va2vS8ceR5A9uYrimKv/LZ7MhU1VF7efnD2sWL3cb6+slzrT9IgKXFxT8W07S33GpZywsLHxzLep/Gcac3Gu0H9fp7Z3X1KLp27WT9+vXh8epq8vXdu7N+FPG61UIXBXZZAmCVJX8DADze5LjPkMQAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDUtMDVUMTU6MTA6NTkrMDI6MDBD/PY6AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA1LTA1VDE1OjEwOjU5KzAyOjAwMqFOhgAAAABJRU5ErkJggg==
+# png-orig:
+# https://openclipart.org/detail/168001/folder-icon-red-music
+#
+# Local file browser. Presents files from configured directories.
+# This is not what streamtuner2 is meant for. Therefore this is
+# an optional plugin, and not overly well integrated.
+#
+# Bugs:
+# Only loads directories on startup. Doesn't work when post-activated
+# per pluginmanager2 for instance. And LANG=C breaks it on startup,
+# if media directories contain anything but ASCII filenames.
+
+
+# modules
+import os
+import re
+
+from channels import *
+from config import *
+
+# ID3 libraries
+try:
+ from mutagen import File as get_meta
+except:
+ try:
+ from ID3 import ID3
+ log.INFO("Just basic ID3 support")
+ get_meta = lambda fn: dict([(k.lower(),v) for k,v in ID3(fn).iteritems()])
+ except:
+ log.INIT("You are out of luck in regards to mp3 browsing. No ID3 support.")
+ get_meta = lambda *x: {}
+
+
+# work around mutagens difficult interface
+def mutagen_postprocess(d):
+ if d.get("TIT2"):
+ return {
+ "encoder": d["TENC"][0],
+ "title": d["TIT2"][0],
+ "artist": d["TPE1"][0],
+# "tyer?????????????": d["TYER"][0],
+# "track": d["TRCK"][0],
+ "album": d["TALB"][0],
+ }
+ else:
+ return d
+
+
+
+
+# file browser / mp3 directory listings
+class file (ChannelPlugin):
+
+ # data
+ listtype = "href"
+ streams = {}
+ categories = []
+ dir = []
+ ext = []
+
+ # display
+ datamap = [ # coltitle width [ datasrc key, type, renderer, attrs ] [cellrenderer2], ...
+ ["", 20, ["state", str, "pixbuf", {}], ],
+ ["Genre", 65, ['genre', str, "t", {"editable":8}], ],
+ ["File", 160, ["filename", str, "t", {"strikethrough":10, "cell-background":11, "cell-background-set":12}], ],
+ ["Title", 205, ["title", str, "t", {"editable":8}], ],
+ ["Artist", 125, ["artist", str, "t", {"editable":8}], ],
+ ["Album", 125, ["album", str, "t", {"editable":8}], ],
+ ["Bitrate", 35, ["bitrate", int, "t", {}], ],
+ ["Format", 50, ["format", str, None, {}], ],
+ [False, 0, ["editable", bool, None, {}], ],
+ [False, 0, ["favourite", bool, None, {}], ],
+ [False, 0, ["deleted", bool, None, {}], ],
+ [False, 0, ["search_col", str, None, {}], ],
+ [False, 0, ["search_set", bool, None, {}], ],
+ ]
+ rowmap = []
+
+
+
+ # prepare
+ def __init__(self, parent):
+
+ # data dirs
+ self.dir = [self.env_dir(s) for s in conf.file_browser_dir.split(",")]
+ self.ext = [s.strip() for s in conf.file_browser_ext.split(",")]
+ # first run
+ if not self.categories or not self.streams:
+ self.scan_dirs()
+
+ # draw gtk lists
+ ChannelPlugin.__init__(self, parent)
+
+ # make editable
+ #{editable:8}
+
+ # add custom context menu
+ #self.gtk_list.connect('button-press-event', self.context_menu)
+
+
+ # Interpolate $VARS and XDG_SPECIAL_DIRS
+ def env_dir(self, path):
+ path = path.strip()
+ env = self.fvars()
+ # Replace $XDG_ ourselfes and normal $ENV vars per expandvars (because os.environ.update() doesn't do)
+ path = re.sub("\$(XDG\w+)", lambda m: env.get(m.group(1), m.group(0)), path)
+ path = os.path.expandvars(path)
+ return os.path.expanduser(path)
+
+ # Read user-dirs config
+ def fvars(self, fn="$HOME/.config/user-dirs.dirs"):
+ fn = os.path.expandvars(fn)
+ src = open(fn, "r").read() if os.path.exists(fn) else ""
+ env = re.findall('^(\w+)=[\"\']?(.+?)[\"\']?', src, re.M) # pyxdg: Your move.
+ return dict(env)
+
+
+ # don't load cache file
+ cache = lambda *x: None
+
+
+ # read dirs
+ def scan_dirs(self):
+ self.categories = []
+
+ # add main directory
+ for main in self.dir:
+ if os.path.exists(main):
+ self.categories.append(main)
+
+ # prepare subdirectories list
+ sub = []
+ self.categories.append(sub)
+
+ # look through
+ for dir, subdirs, files in os.walk(main):
+ name = os.path.basename(dir)
+ sfx = ""
+ while name+sfx in self.categories:
+ sfx = str(int(sfx)+1) if sfx else "2"
+ name += sfx
+
+ # files in subdir
+ if files:
+ sub.append(name)
+ self.streams[name] = [self.file_entry(fn, dir) for fn in files if self.we_like_that_extension(fn)]
+
+ # plant a maindir reference to shortname
+ main_base = os.path.basename(main)
+ if self.streams.get(main_base):
+ self.streams[main] = self.streams[main_base]
+
+
+ # extract meta data
+ def file_entry(self, fn, dir):
+ # basic data
+ meta = {
+ "title": fn,
+ "filename": fn,
+ "url": "file://" + dir + "/" + fn,
+ "genre": "",
+ "format": self.mime_fmt(fn[-3:]),
+ "editable": True,
+ }
+ # add ID3
+ meta.update(mutagen_postprocess(get_meta(dir + "/" + fn) or {}))
+ return meta
+
+ # check fn for .ext
+ def we_like_that_extension(self, fn):
+ return fn[-3:] in self.ext
+
+
+
+ # same as init
+ def update_categories(self):
+ self.scan_dirs()
+
+
+ # same as init
+ def update_streams(self, cat, x=0):
+ self.scan_dirs()
+ return self.streams.get(os.path.basename(cat))
+
+
ADDED contrib/punkcast.py
Index: contrib/punkcast.py
==================================================================
--- contrib/punkcast.py
+++ contrib/punkcast.py
@@ -0,0 +1,91 @@
+
+# api: streamtuner2
+# title: PunkCast
+# description: Online video site that covered NYC artists. Not updated anymore.
+# type: channel
+# category: video
+# version: 0.2
+# url: http://www.punkcast.com/
+# png:
+# iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAyxJREFUOI0FwUlvG2UAgOH3++abxfsy2dtmcRsnghpBKyKhQA7lgsShPwEk/gVcKn4HN+6AynKIkEgFUkFF
+# okrlpEkap2nqxHFsx/bYHs/m4XmE0s1Yyhjf8/niy6/49utv+PPf57x4sk3083cUzZgwZWElE9hL60yyc1zU39A8PsRxhijLMBFahO/5dFotwvGIeDxkOHJwNYvCwgyRYeElslTPu7ivTnFaTU6dkG4E
+# ytANoskYgMb5W64b51jEuJ6Lq3Rmy2tEUcz1WR1pGGiyiIhMDOlQGHZRQRAQRR4CqJ2eUNvbJcAiEYz45NMtKrdvkc8a1G5M81e1RuiGjDpt1CTASKeQytDxowmmZdLpdDl8+QJLBqTxKSYkc0afbOyz
+# OD/F/fIynhA0wwChazhIpOuNYCJI6hpxPOHiqk1K8+ldthDumFhXnLUHjEZj9DggKyWfbWywPpXHVqBEOKayXubDyl1qx0f0eh1E6GLaRX7aO8NLJjG9PvNLt0kpuJWeMHPTxnan6V81UWmpYacTVN57
+# H9uewu01+OPJDle9AY//O+Dj++9yr3wHUnl8PyCTyzL2PE5aPUgYaFlNPDKigGe7VW6U1iDwcXuXzBbz+K5PThMEvQ6xjJjEAi2Z4Wn1mP03FyR1DaU0jUbnGj81IZXLkckoSpUV7JTB0HF4urvH3IMN
+# jG6XbGGWemeAkc+hNEE0DtCSmngUCkFSixn321iWyVzGImEIVssllAh5e9kkCDV2T05pD4dMTeWpv6ohwghp6opCOOHh1kfcXZzm8S/bPNs/ZOC4mELj4eYGWiT54Z896qMJ9vws4XBE3lRIBeqmDmk/
+# Qmnw+YNNlsur/PrbDqv5NFm7SHMwpNbuMggClmYL5JWkqytErKGPPVQmilhLSpbtBE1nxMFhjedHrymkNUrv3KHTuqLvjWl7MQevL+gNXaqNLlGzSyUvUVuEzMzkWVhZYqd6xPc//o6MBfWrAZcdBxnH
+# zOcy/F1rsf3yFFmtEcWwOZ2h1R8hP5jATGmRQE+wXz0gjiMWihnO2z1qjS6ZQoF76yuU7DSxFFhJC10phmHIhVT8D1yefHn5PzXrAAAAAElFTkSuQmCC
+# priority: obsolete
+# config: { name: punkcast_img, type: boolean, value: 0, description: Load banners. (Channel - Update favicons) }
+#
+# Punkcast is no longer updated. This plugin is kept for
+# historic reasons. It was one of the default streamtuner1
+# channels.
+
+
+import re
+import ahttp
+from config import conf
+import action
+from channels import *
+from config import *
+
+
+# basic.ch broadcast archive
+class punkcast (ChannelPlugin):
+
+ # keeps category titles->urls
+ catmap = {}
+ categories = ["list"]
+ titles = dict(playing=False, listeners=False, bitrate=False, homepage=False)
+
+
+ # don't do anything
+ def update_categories(self):
+ pass
+
+
+ # get list
+ def update_streams(self, cat):
+
+ rx_link = re.compile("""
+
+ .*? ALT="([^<">]+)"
+ """, re.S|re.X)
+
+ entries = []
+
+ #-- all from frontpage
+ html = ahttp.get("http://www.punkcast.com/")
+ for uu in rx_link.findall(html):
+ (homepage, id, title) = uu
+ entries.append({
+ "genre": "%s" % id,
+ "title": title,
+ "playing": "PUNKCAST #%s" % id,
+ "format": "audio/mpeg",
+ "url": "none:",
+ "homepage": homepage,
+ "img": "http://punkcast.com/%s/PUNK%s.jpg" % (id, id) if conf.punkcast_img else None,
+ })
+
+ # done
+ return entries
+
+
+ # special handler for play
+ def play(self, row):
+
+ rx_sound = re.compile("""(http://[^"<>]+[.](mp3|ogg|m3u|pls|ram))""")
+ html = ahttp.get(row["homepage"])
+
+ # look up ANY audio url
+ for uu in rx_sound.findall(html):
+ log.DATA( uu )
+ (url, fmt) = uu
+ action.play(url, self.mime_fmt(fmt), "srv")
+ return
+
+ # or just open webpage
+ action.browser(row["homepage"])
+