Internet radio browser GUI for music/video streams from various directory services.

⌈⌋ ⎇ branch:  streamtuner2


bookmarks.py at [d3bb740f0b]

File channels/bookmarks.py artifact b8e07c8df6 part of check-in d3bb740f0b


# encoding: UTF-8
# api: streamtuner2
# title: Bookmarks
# description: For collecting favourites. And provides some feature/category plugins.
# type: channel
# version: 1.5
# category: builtin
# priority: core
# config: { name: like_my_bookmarks,  type: boolean, value: 0, description: "I like my bookmarks" }
# url: http://freshcode.club/projects/streamtuner2
# 
# Favourite lists.
#
# This module lists static content from ~/.config/streamtuner2/bookmarks.json.
# Any bookmarked station will appear with a star ★ icon in other channels.
#
# Some feature extensions inject custom subcategories here. For example the
# "search" feature adds its own result list here, as does the "timer" plugin.


from config import *
from uikit import *
from channels import *



# The bookmarks tab is a core feature and built into the GtkBuilder
# layout. Which is why it derives from GenericChannel, and requires
# less setup.
#
# 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 `parent.bookmarks` in the ST2 window and elsewhere.
#
class bookmarks(GenericChannel):

    # content
    listformat = "any"
    categories = ["favourite", ]  # timer, links, search, and links show up as needed
    streams = {
        "favourite": [
          { "genre": "Top 40", "title": "Frequence3", "playing": "The French hit web radio from Paris!", "format": "audio/mpeg",
            "url": "http://ice.stream.frequence3.net/frequence3-256.mp3", "homepage": "http://www.frequence3.fr/" }
        ],
        "search": [],
        "scripts": [],
        "timer": [],
        "history": []
    }
    default = "favourite"
    fixed_size = [32,24]
    reserved_names = ["favourite", "radiotray", "scripts", "search", "timer", "history", "links", "themes"] #+ self.parent.features.keys()


    # cache list, to determine if a PLS url is bookmarked
    urls = []

    def gui(self, parent):
        parent.notebook_channels.set_menu_label_text(parent.v_bookmarks, "bookmarks")
        self.update_categories()
        GenericChannel.gui(self, parent)
        uikit.tree_column(self.gtk_cat, "Group")

    # custom categories are shown as subfolder below `favourite`
    def update_categories(self):
        cust_cats = list(set(self.streams.keys()) - set(self.reserved_names))
        if len(self.categories) < 2 or type(self.categories[1]) is not list:
            self.categories.insert(1, [])
        self.categories[1] = cust_cats
        
    # but category sub-plugins might provide a hook
    category_plugins = {}
    def update_streams(self, cat):
        if cat in self.category_plugins:
            return self.category_plugins[cat].update_streams(cat) or []
        else:
            return self.streams.get(cat, [])

        
    # streams are already loaded at instantiation
    #def first_show(self):
    #    print "first_show", len(self.streams["favourite"])
    #    GenericChannel.first_show(self)


    # all entries just come from "bookmarks.json"
    def cache(self):
        # stream list
        cache = conf.load(self.module)
        if (cache):
            log.PROC("load bookmarks.json")
            self.streams = cache
        


    # save to cache file
    def save(self):
        conf.save(self.module, self.streams, nice=1)


    # checks for existence of an URL in bookmarks store,
    # this method is called by other channel modules' display() method
    def is_in(self, url, once=1):
        if (not self.urls):
            #@todo: traverse all categories?
            self.urls = [str(row.get("url","urn:x-streamtuner2:no")) for row in self.streams["favourite"]]
        return str(url) in self.urls


    # called from main window / menu / context menu,
    # when bookmark is to be added for a selected stream entry
    def add(self, row, target="favourite"):

        # Add / copy some row attributes
        row["favourite"] = 1
        if not row.get("favicon"):
            pass#   row["favicon"] = favicon.file(row.get("homepage"))
        if not row.get("listformat"):
            row["listformat"] = self.parent.channel().listformat
        if not len(row.get("extra", "")):
            row["extra"] = self.parent.channel().module

        # append to storage
        self.streams[target].append(row)
        self.save()
        self.load(target)
        self.urls.append(row["url"])


    # simplified gtk TreeStore display logic (just one category for the moment, always rebuilt)
    def load(self, category, force=False, y=None):
        self.streams[category] = self.update_streams(category)
        GenericChannel.load(self, category, force=False, y=y)


    # add a categories[]/streams{} subcategory, update treeview
    def add_category(self, cat, plugin=None):
        if cat not in self.categories: # add category if missing
            self.categories.append(cat)
            self.display_categories()
        if cat not in self.streams:
            self.streams[cat] = []
        if plugin:
            self.category_plugins[cat] = plugin


    # change cursor
    def set_category(self, cat):
        self.add_category(cat)
        self.gtk_cat.get_selection().select_path(str(self.categories.index(cat)))
        return self.currentcat()
        
        
    # update bookmarks from freshly loaded streams data
    def heuristic_update(self, updated_channel, updated_category):

        if not conf.heuristic_bookmark_update: return
        log.PROC("heuristic bookmark update")
        save = 0
        fav = self.streams["favourite"]

        # First we'll generate a list of current bookmark stream urls, and then
        # remove all but those from the currently UPDATED_channel + category.
        # 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.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

                # remove unchanged urls/rows
                else:
                    unchanged_urls = (row.get("url") for row in streams)
                    for url in unchanged_urls:
                        if url in check:
                            del check[url]
                            # directory duplicates could unset the check list here,
                            # so we later end up doing a deep comparison


        # now the real comparison,
        # where we compare station titles and homepage url to detect if a bookmark is an old entry
        for row in freshened_streams:
            url = row.get("url")
            
            # empty entry (google stations), or stream still in current favourites
            if not url or url in check:
                pass

            # need to search
            else:
                title = row.get("title")
                homepage = row.get("homepage")
                for i,old in enumerate(fav):

                    # skip if new url already in streams
                    if url == old.get("url"):
                        pass   # This is caused by channel duplicates with identical PLS links.
                    
                    # on exact matches (but skip if url is identical anyway)
                    elif title == old["title"] and homepage == old.get("homepage",homepage):
                        # update stream url
                        fav[i]["url"] = url
                        save = 1
                        
                    # more text similarity heuristics might go here
                    else:
                        pass
        
        # if there were changes
        if save: self.save()