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"]) +