Index: PACKAGING ================================================================== --- PACKAGING +++ PACKAGING @@ -1,9 +1,10 @@ # pack: PACKAGING= This is a short summary for distribution package maintainers. -For regular end-user documentation please see the help/ pages. +For regular end-user documentation please see the README and +help/ pages. Structural changes from 2.1.5 onwards (2015-04-xx) -------------------------------------------------- @@ -42,21 +43,21 @@ → Help files still need to go to share/docs/streamtuner2/help/ unless you patch the source. → *.desktop as usual - → and `logo.png` is the pixmap/app icon + → and `icon.png` is the /share/pixmaps/ icon Removed ------- Most plugin PNGs may have been removed already. (Embedded binary data may violate some distro guidelines(?), but hey, fewer files are fewer files!) -And the streamtuner2.png logo is now source-embedded instead, +And the streamtuner2.png logo is now source-embedded instead; the `logo.py` module provides a `logo.png` base64-string. The old `gtk2.xml` file is gone. It probably became obsolete a long while back. The gtk3.xml is instead runtime-patched to work with PyGTK/gtk2. @@ -71,11 +72,11 @@ Also, hard dependencies are meanwhile: - gtk (>= 2.16) - pygtk [or python-gi for python3] - - python-requests (>> 1.2.0) + - python-requests (>= 2.0.0) - python-pyquery [though most plugins would work without] - and its implied python-lxml Optional dependencies (just affects a single plugin, which semi-gracefully disables itself): @@ -115,11 +116,11 @@ FPM/XPM packaging ----------------- You may have noticed (and scoffed at ;) the newer packaging method. It's now using http://fossil.include-once.org/xpm/ -with the `src` filter. (That's what the meta comment blocks +with the `-s src` filter. (That's what the meta comment blocks in the source modules were always meant for.) Simplifies DEB and RPM packaging, as well as PYZ generation. (They're all workable, but decidedly rather crude packages. So yes, proper distro packages are very much still needed.) Index: channels/__init__.py ================================================================== --- channels/__init__.py +++ channels/__init__.py @@ -104,10 +104,14 @@ empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")] # regex rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I) + + + #--------------------------- initialization -------------------------------- + # constructor def __init__(self, parent=None): #self.streams = {} @@ -127,36 +131,10 @@ # only if streamtuner2 is run in graphical mode if (parent): self.cache() self.gui(parent) pass - - - # These are all implemented in main (where they don't belong!) - def stations(self): - return self.streams.get(self.current, []) - def rowno(self): - pass - def row(self): - pass - - - # read previous channel/stream data, if there is any - def cache(self): - # stream list - cache = conf.load("cache/" + self.module) - if (cache): - self.streams = cache - # categories - 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 # initialize Gtk widgets / data objects def gui(self, parent): #print(self.module + ".gui()") @@ -187,10 +165,72 @@ uikit.columns(self.gtk_list, self.datamap, []) # add to main menu uikit.add_menu([parent.channelmenuitems], self.meta["title"], lambda w: parent.channel_switch_by_name(self.module) or 1) + + # Statusbar stub (defers to parent/main window, if in GUI mode) + def status(self, *v): + if self.parent: self.parent.status(*v) + else: __print__(dbg.INFO, "status():", *v) + + + + #--------------------- streams/model data accesss --------------------------- + + # Get list of stations in current category + def stations(self): + return self.streams.get(self.current, []) + + # Convert ListStore iter to row number + def rowno(self): + (model, iter) = self.model_iter() + return model.get_path(iter)[0] + + # Return ListStore object and Iterator for currently selected row in gtk.TreeView station list + def model_iter(self): + return self.gtk_list.get_selection().get_selected() + + # Currently selected entry in stations list, return complete data dict + def row(self): + return self.stations() [self.rowno()] + + # Fetches a single varname from currently selected station entry + def selected(self, name="url"): + return self.row().get(name) + + # Inject status icon into currently selected row (used by main.bookmark() call) + def row_icon(self, gtkIcon = gtk.STOCK_ABOUT): + try: + # Updates gtk_list store, set icon in current display. + # Since it is used by bookmarks, would be reshown with next display() anyhow, + # and there's no need to invalidate the ls cache, because that's referenced by model anyhow. + (model,iter) = self.model_iter() + model.set_value(iter, 0, gtkIcon) + except: + pass + + + + #------------------------ base implementations ----------------------------- + + # read previous channel/stream data, if there is any + def cache(self): + # stream list + cache = conf.load("cache/" + self.module) + if (cache): + self.streams = cache + # categories + 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 + # make private copy of .datamap and modify field (title= only ATM) def update_datamap(self, search="name", title=None): if self.datamap == GenericChannel.datamap: self.datamap = copy.deepcopy(self.datamap) @@ -441,22 +481,32 @@ #--------------------------- actions --------------------------------- - # invoke action.play, - # can be overridden to provide channel-specific "play" alternative - def play(self, row): - if row.get("url"): + # Invoke action.play() for current station. + # Can be overridden to provide channel-specific "play" alternative + def play(self): + row = self.row() + if row: + # playlist and audio type + audioformat = row.get("format", self.audioformat) + listformat = row.get("listformat", self.listformat) + # invoke audio player + action.play(row["url"], audioformat, listformat, row) + else: + self.status("No station selected for playing.") + return row - # parameters + # Start streamripper/youtube-dl/etc + def record(self): + row = self.row() + if row: audioformat = row.get("format", self.audioformat) listformat = row.get("listformat", self.listformat) - - # invoke audio player - action.play(row["url"], audioformat, listformat) - + action.record(row.get("url"), audioformat, listformat, row=row) + return row #--------------------------- utility functions ----------------------- Index: channels/bookmarks.py ================================================================== --- channels/bookmarks.py +++ channels/bookmarks.py @@ -30,11 +30,11 @@ # # Furthermore it pretty much only handles a static streams{} list. # Sub-plugins simply append a new category, and populate the streams # list themselves. # -# It's accessible as `main.bookmarks` in the ST2 window and elsewhere. +# It's accessible as `parent.bookmarks` in the ST2 window and elsewhere. # class bookmarks(GenericChannel): # desc module = "bookmarks" @@ -107,11 +107,11 @@ # normalize data (this row originated in a gtk+ widget) row["favourite"] = 1 if row.get("favicon"): row["favicon"] = favicon.file(row.get("homepage")) if not row.get("listformat"): - row["listformat"] = self.main.channel().listformat + row["listformat"] = self.parent.channel().listformat # append to storage self.streams["favourite"].append(row) self.save() self.load(self.default) @@ -156,11 +156,11 @@ # This step is most likely redundant, but prevents accidently re-rewriting # stations that are in two channels (=duplicates with different PLS urls). check = {"http//": "[row]"} check = dict((row.get("url", "http//"),row) for row in fav) # walk through all channels/streams - for chname,channel in self.main.channels.items(): + for chname,channel in self.parent.channels.items(): for cat,streams in channel.streams.items(): # keep the potentially changed rows if (chname == updated_channel) and (cat == updated_category): freshened_streams = streams Index: channels/exportcat.py ================================================================== --- channels/exportcat.py +++ channels/exportcat.py @@ -44,12 +44,11 @@ # Fetch streams from category, show "Save as" dialog, then convert URLs and export as playlist file def savewindow(self, *w): cn = self.parent.channel() source = cn.listformat streams = cn.streams[cn.current] - fn = uikit.save_file("Export category", None, "stationlist." + conf.export_format) + fn = uikit.save_file("Export category", None, "%s.%s.%s" % (cn.module, cn.current, conf.export_format)) __print__(dbg.PROC, "Exporting category to", fn) if fn: dest = re.findall("\.(m3u|pls|xspf|jspf|json|smil|wpl)8?$", fn)[0] - action.save_playlist(source="asis", multiply=False).store(rows=streams, fn=fn, dest=dest) + action.save_playlist(source="asis", multiply=False).file(rows=streams, fn=fn, dest=dest) pass - Index: st2.py ================================================================== --- st2.py +++ st2.py @@ -68,16 +68,17 @@ # This represents the main window, dispatches Gtk events, # and shares most application behaviour with the channel modules. class StreamTunerTwo(gtk.Builder): # object containers - widgets = {} # non-glade widgets (the manually instantiated ones) + widgets = {} # non-glade widgets (any manually instantiated ones) channels = {} # channel modules features = {} # non-channel plugins working = [] # threads hooks = { "play": [favicon.download_playing], # observers queue here + "record": [], "init": [], "config_load": [], "config_save": [], } meta = plugin_meta() @@ -260,41 +261,32 @@ # Mirror selected channel tab into main window title def update_title(self): self.win_streamtuner2.set_title("Streamtuner2 - %s" % self.channel().meta.get("title")) - # 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 + # Channel: row{} dict for current station def row(self): - return self.channel().stations() [self.rowno()] - - - # 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 + return self.channel().row() + + # Channel: fetch single varname from station row{} dict def selected(self, name="url"): return self.row().get(name) # 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"]] + self.status("Starting player...") + row = self.channel().play() + self.status("") + [callback(row) for callback in self.hooks["play"]] # 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) + self.status("Recording station...") + row = self.channel().record() + [callback(row) for callback in self.hooks["record"]] # Open stream homepage in web browser def on_homepage_stream_clicked(self, widget): url = self.selected("homepage") if url and len(url): action.browser(url) @@ -331,16 +323,11 @@ pass # 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) - try: - (model,iter) = self.model_iter() - model.set_value(iter, 0, gtk.STOCK_ABOUT) - except: - pass + self.channel().row_icon(gtk.STOCK_ABOUT) # refresh bookmarks tab self.bookmarks.load(self.bookmarks.default) # Reload category tree def update_categories(self, widget): @@ -357,23 +344,24 @@ default_fn = row["title"] + ".m3u" fn = uikit.save_file("Save Stream", None, default_fn, [(".m3u","*m3u"),(".pls","*pls"),(".xspf","*xspf"),(".jspf","*jspf"),(".smil","*smil"),(".asx","*asx"),("all files","*")]) if fn: source = row.get("listformat", self.channel().listformat) dest = (re.findall("\.(m3u|pls|xspf|jspf|json|smil|asx|wpl)8?$", fn) or ["pls"])[0] - action.save_playlist(source=source, multiply=True).store(rows=[row], fn=fn, dest=dest) + action.save_playlist(source=source, multiply=True).save(rows=[row], fn=fn, dest=dest) pass # Save current stream URL into clipboard def menu_copy(self, w): gtk.clipboard_get().set_text(self.selected("url")) # Remove a stream entry def delete_entry(self, w): - n = self.rowno() - del self.channel().stations()[ n ] - self.channel().switch() - self.channel().save() + cn = self.channel() + n = cn.rowno() + del cn.stations()[ n ] + cn.switch() + cn.save() # Alternative Notebook channel tabs between TOP and LEFT position def switch_notebook_tabs_position(self, w, pos): self.notebook_channels.set_tab_pos(pos);