#!/usr/bin/python from gi.repository import Gtk as gtk from gi.repository import GLib, Gio, GObject, Notify, GdkPixbuf, Gdk from gi.repository import AppIndicator3 as appindicator import os import time import urllib import json import sys import webbrowser import threading class Twitch: def fetch_followed_channels(self, username): """Fetch user followed channels and return a list with channel names.""" try: self.followed_channels = [] self.f = urllib.urlopen("https://api.twitch.tv/kraken/users/{0}/follows/channels?client_id=oe77z9pq798tln7ngil0exwr0mun4hj&direction=DESC&limit=100&offset=0&sortby=created_at".format(username)) self.data = json.loads(self.f.read()) # Return 404 if user does not exist try: if (self.data["status"] == 404): return 404 except KeyError: pass self.pages = (self.data['_total'] - 1) / 100 for page in range(0, self.pages + 1): if page != 0: self.f = urllib.urlopen("https://api.twitch.tv/kraken/users/{0}/follows/channels?client_id=oe77z9pq798tln7ngil0exwr0mun4hj&direction=DESC&limit=100&offset={1}&sortby=created_at".format(username, (page * 100))) self.data = json.loads(self.f.read()) for channel in self.data['follows']: self.followed_channels.append(channel['channel']['name']) return self.followed_channels except IOError: return None def fetch_live_streams(self, channels): """Fetches live streams data from Twitch, and returns as list of dictionaries""" try: self.channels_count = len(channels) self.live_streams = [] self.pages = (self.channels_count - 1) / 75 for page in range(0, self.pages + 1): self.offset = (page * 75) + 75 if (self.offset % 75 > 0): self.offset = self.channels_count self.channels_offset = channels[(page * 75):self.offset] self.f = urllib.urlopen("https://api.twitch.tv/kraken/streams?client_id=oe77z9pq798tln7ngil0exwr0mun4hj&channel={0}".format(','.join(self.channels_offset))) self.data = json.loads(self.f.read()) for stream in self.data['streams']: # For some reason sometimes stream status and game is not present in # twitch API. try: self.status = stream['channel']['status'] except KeyError: self.status = "" st = { 'name': stream['channel']['display_name'], 'status': self.status, 'image': stream['channel']['logo'], 'url': "http://www.twitch.tv/%s" % stream['channel']['name'] } self.live_streams.append(st) return self.live_streams except IOError: return None class Indicator(): SETTINGS_KEY = "apps.twitch-indicator" LIVE_STREAMS = [] def __init__(self): self.timeout_thread = None # Setup applet icon depending on DE self.desktop_env = os.environ.get('DESKTOP_SESSION') if self.desktop_env == "pantheon": self.applet_icon = "twitch-elementary" elif self.desktop_env == "mate": self.applet_icon = "twitch-mate" else: self.applet_icon = "twitch-ubuntu" # Create applet self.a = appindicator.Indicator.new( 'Twitch indicator', 'indicator-messages', appindicator.IndicatorCategory.APPLICATION_STATUS ) self.a.set_status(appindicator.IndicatorStatus.ACTIVE) self.a.set_icon_theme_path("/usr/lib/twitch-indicator/") self.a.set_icon(self.applet_icon) # Load settings self.settings = Gio.Settings.new(self.SETTINGS_KEY) # Setup menu self.menu = gtk.Menu() self.menuItems = [ gtk.MenuItem('Check now'), gtk.SeparatorMenuItem(), gtk.MenuItem('Settings'), gtk.MenuItem('Quit') ] self.menuItems[0].connect('activate', self.refresh_streams_init, [True]) self.menuItems[-2].connect('activate', self.settings_dialog) self.menuItems[-1].connect('activate', self.quit) for i in self.menuItems: self.menu.append(i) self.a.set_menu(self.menu) self.menu.show_all() self.refresh_streams_init(None) def open_link(self, widget, url): """Opens link in a default browser.""" webbrowser.open_new_tab(url) def refresh_streams_init(self, widget, button_activate=False): """Initializes thread for stream refreshing.""" self.t = threading.Thread(target=self.refresh_streams) self.t.daemon = True self.t.start() if (button_activate is False): self.timeout_thread = threading.Timer(self.settings.get_int("refresh-interval")*60, self.refresh_streams_init, [None]) self.timeout_thread.start() def settings_dialog(self, widget): """Shows applet settings dialog.""" self.dialog = gtk.Dialog( "Settings", None, 0, (gtk.STOCK_CANCEL, gtk.ResponseType.CANCEL, gtk.STOCK_OK, gtk.ResponseType.OK) ) self.builder = gtk.Builder() self.builder.add_from_file("/usr/lib/twitch-indicator/twitch-indicator.glade") self.builder.get_object("twitch_username").set_text(self.settings.get_string("twitch-username")) self.builder.get_object("show_notifications").set_active(self.settings.get_boolean("enable-notifications")) self.builder.get_object("refresh_interval").set_value(self.settings.get_int("refresh-interval")) self.box = self.dialog.get_content_area() self.box.add(self.builder.get_object("grid1")) self.response = self.dialog.run() if self.response == gtk.ResponseType.OK: self.settings.set_string("twitch-username", self.builder.get_object("twitch_username").get_text()) self.settings.set_boolean("enable-notifications", self.builder.get_object("show_notifications").get_active()) self.settings.set_int("refresh-interval", self.builder.get_object("refresh_interval").get_value_as_int()) elif self.response == gtk.ResponseType.CANCEL: pass self.dialog.destroy() def disable_menu(self): """Disables check now button.""" self.menu.get_children()[0].set_sensitive(False) self.menu.get_children()[0].set_label("Checking...") def enable_menu(self): """Enables check now button.""" self.menu.get_children()[0].set_sensitive(True) self.menu.get_children()[0].set_label("Check now") def add_streams_menu(self, streams): """Adds streams list to menu.""" # Remove live streams menu if already exists if (len(self.menuItems) > 4): self.menuItems.pop(2) self.menuItems.pop(1) # Create menu self.streams_menu = gtk.Menu() self.menuItems.insert(2, gtk.MenuItem("Live channels ({0})".format(len(streams)))) self.menuItems.insert(3, gtk.SeparatorMenuItem()) self.menuItems[2].set_submenu(self.streams_menu) # Order streams by alphabetical order self.streams_ordered = sorted(streams, key=lambda k: k["name"].lower()) for index, stream in enumerate(self.streams_ordered): self.streams_menu.append(gtk.MenuItem(stream["name"])) self.streams_menu.get_children()[index].connect('activate', self.open_link, stream["url"]) for i in self.streams_menu.get_children(): i.show() # Refresh all menu by removing and re-adding menu items for i in self.menu.get_children(): self.menu.remove(i) for i in self.menuItems: self.menu.append(i) self.menu.show_all() def refresh_streams(self): """Refreshes live streams list. Also pushes notifications when needed.""" GLib.idle_add(self.disable_menu) if (self.settings.get_string("twitch-username") == ""): GLib.idle_add(self.abort_refresh, "Twitch.tv username is not set", "Setup your username in settings") return # Create twitch instance and fetch followed channels. self.tw = Twitch() self.followed_channels = self.tw.fetch_followed_channels(self.settings.get_string("twitch-username")) # Does user exist? if self.followed_channels == 404: GLib.idle_add(self.abort_refresh, "Cannot retrieve followed channels from Twitch.tv", "User does not exist.") return if self.followed_channels == None: GLib.idle_add(self.abort_refresh, "Cannot retrieve channel list from Twitch.tv", "Retrying in {0} minutes...".format(self.settings.get_int("refresh-interval"))) return self.live_streams = self.tw.fetch_live_streams(self.followed_channels) if self.live_streams == None: GLib.idle_add(self.abort_refresh, "Cannot retrieve live streams from Twitch.tv", "Retrying in {0} minutes...".format(self.settings.get_int("refresh-interval"))) return # Update menu with live streams GLib.idle_add(self.add_streams_menu, self.live_streams) # Re-enable "Check now" button GLib.idle_add(self.enable_menu) # Check which streams were not live before, create separate list for # notifications and update main livestreams list. # We check live streams by URL, because sometimes Twitch API does not show # stream status, which makes notifications appear even if stream has been # live before. self.notify_list = list(self.live_streams) for x in self.LIVE_STREAMS: for y in self.live_streams: if x["url"] == y["url"]: self.notify_list[:] = [d for d in self.notify_list if d.get('url') != y["url"]] break self.LIVE_STREAMS = self.live_streams # Push notifications of new streams if (self.settings.get_boolean("enable-notifications")): self.push_notifications(self.notify_list) def abort_refresh(self, message, description): """Updates menu with failure state message.""" # Remove previous message if already exists if (len(self.menuItems) > 4): self.menuItems.pop(2) self.menuItems.pop(1) self.menuItems.insert(2, gtk.MenuItem(message)) self.menuItems.insert(3, gtk.SeparatorMenuItem()) self.menuItems[2].set_sensitive(False) # Re-enable "Check now" button self.menuItems[0].set_sensitive(True) self.menuItems[0].set_label("Check now") # Refresh all menu items for i in self.menu.get_children(): self.menu.remove(i) for i in self.menuItems: self.menu.append(i) self.menu.show_all() # Push notification Notify.init("image") self.n = Notify.Notification.new(message, description, "error" ).show() def push_notifications(self, streams): """Pushes notifications of every stream, passed as a list of dictionaries.""" try: for stream in streams: self.image = gtk.Image() # Show default if channel owner has not set his avatar if (stream["image"] == None): self.response = urllib.urlopen("http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png") else: self.response = urllib.urlopen(stream["image"]) self.loader = GdkPixbuf.PixbufLoader.new() self.loader.write(self.response.read()) self.loader.close() Notify.init("image") self.n = Notify.Notification.new("%s just went LIVE!" % stream["name"], stream["status"], "", ) self.n.set_icon_from_pixbuf(self.loader.get_pixbuf()) self.n.show() except IOError: return def main(self): """Main indicator function.""" gtk.main() def quit(self, item): """Quits the applet.""" self.timeout_thread.cancel() gtk.main_quit() if __name__=="__main__": Gdk.threads_init() gui = Indicator() gui.main()