├── __init__.py ├── apis ├── __init__.py ├── scores.py └── streams.py ├── handlers ├── __init__.py ├── cache.py ├── stream.py ├── channel.py ├── match.py ├── player.py └── data.py ├── helpers ├── __init__.py ├── gtk.py └── utils.py ├── widgets ├── __init__.py ├── filterbox.py ├── mpvbox.py ├── vlcbox.py ├── gstbox.py ├── streambox.py ├── channelbox.py └── matchbox.py ├── .gitignore ├── screenshot.jpg ├── icons ├── hicolor │ ├── 16x16 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 24x24 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 32x32 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 48x48 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 64x64 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 96x96 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 128x128 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 256x256 │ │ └── apps │ │ │ └── kickoff-player.png │ ├── 512x512 │ │ └── apps │ │ │ └── kickoff-player.png │ └── 1024x1024 │ │ └── apps │ │ └── kickoff-player.png ├── render-bitmaps.py └── src │ └── kickoff-player.svg ├── .editorconfig ├── images ├── acestream.svg ├── team-emblem.svg └── channel-logo.svg ├── kickoff-player.desktop ├── README.md ├── ui ├── styles.css ├── matches.ui ├── channels.ui ├── main.ui ├── match.ui └── player.ui ├── kickoff_player.py └── LICENSE.txt /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apis/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /helpers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /widgets/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ui~ 2 | *.ui# 3 | *test* 4 | *sample* 5 | *pycache* 6 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /icons/hicolor/16x16/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/16x16/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/24x24/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/24x24/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/32x32/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/32x32/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/48x48/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/48x48/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/64x64/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/64x64/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/96x96/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/96x96/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/128x128/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/128x128/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/256x256/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/256x256/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/512x512/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/512x512/apps/kickoff-player.png -------------------------------------------------------------------------------- /icons/hicolor/1024x1024/apps/kickoff-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonian/kickoff-player/HEAD/icons/hicolor/1024x1024/apps/kickoff-player.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /images/acestream.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kickoff-player.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xdg-open 2 | [Desktop Entry] 3 | Version=1.0 4 | Name=Kickoff Player 5 | GenericName=Media player 6 | Comment=Stream football matches and channels 7 | Exec=kickoff-player %u 8 | TryExec=kickoff-player 9 | Icon=kickoff-player 10 | Terminal=false 11 | Type=Application 12 | MimeType=x-scheme-handler/acestream; 13 | X-KDE-Protocols=acestream; 14 | -------------------------------------------------------------------------------- /images/team-emblem.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/channel-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Kickoff Player 2 | 3 | GTK3 player with MPV, VLC and GStreamer backends, for streaming Acestream sports channels. 4 | 5 | * Browse through popular football competitions 6 | * Watch Acestream sports channels 7 | * Watch football games in the built-in player 8 | * Open Acestream links 9 | 10 | ![Screenshot](https://raw.githubusercontent.com/jonian/kickoff-player/master/screenshot.jpg) 11 | 12 | ## Dependencies 13 | gtk3 gstreamer gst-plugins-bad python python-gobject python-dbus python-psutil python-pexpect python-peewee python-requests python-fuzzywuzzy python-levenshtein python-dateutil python-lxml acestream-engine 14 | 15 | ## Usage 16 | kickoff-player URL 17 | 18 | ## Packages 19 | Arch Linux: [AUR Package](https://aur.archlinux.org/packages/kickoff-player-git) 20 | 21 | ## License 22 | Kickoff Player is available as open source under the terms of the [GPLv3 License](http://www.gnu.org/licenses/gpl-3.0.en.html). 23 | -------------------------------------------------------------------------------- /widgets/filterbox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | 5 | from gi.repository import Gtk, GObject, Pango 6 | 7 | 8 | class FilterBox(Gtk.ListBoxRow): 9 | 10 | __gtype_name__ = 'FilterBox' 11 | 12 | filter_name = GObject.property(type=str, flags=GObject.PARAM_READWRITE) 13 | filter_all = GObject.property(type=str, flags=GObject.PARAM_READWRITE) 14 | 15 | def __init__(self, *args, **kwargs): 16 | Gtk.ListBoxRow.__init__(self, *args, **kwargs) 17 | 18 | self.filter_name = self.get_property('filter-name') 19 | self.filter_all = self.get_property('filter-all') 20 | self.filter_label = self.do_filter_label() 21 | self.filter_value = self.set_filter_value() 22 | 23 | self.connect('realize', self.on_filter_name_updated) 24 | self.connect('notify::filter_name', self.on_filter_name_updated) 25 | 26 | self.show() 27 | 28 | def set_filter_value(self): 29 | value = None if self.filter_name == self.filter_all else self.filter_name 30 | return value 31 | 32 | def on_filter_name_updated(self, *_args): 33 | self.update_filter_label() 34 | 35 | def do_filter_label(self): 36 | label = Gtk.Label(None) 37 | label.set_justify(Gtk.Justification.LEFT) 38 | label.set_halign(Gtk.Align.START) 39 | label.set_max_width_chars(25) 40 | label.set_ellipsize(Pango.EllipsizeMode.END) 41 | label.set_margin_top(10) 42 | label.set_margin_bottom(10) 43 | label.set_margin_left(10) 44 | label.set_margin_right(10) 45 | 46 | return label 47 | 48 | def update_filter_label(self): 49 | self.filter_label.set_label(self.filter_name) 50 | self.filter_label.show() 51 | self.add(self.filter_label) 52 | -------------------------------------------------------------------------------- /widgets/mpvbox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import mpv 3 | 4 | gi.require_version('Gtk', '3.0') 5 | 6 | from gi.repository import Gtk, GObject 7 | from helpers.gtk import add_widget_class 8 | 9 | 10 | class MpvBox(Gtk.Box): 11 | 12 | __gtype_name__ = 'MpvBox' 13 | 14 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 15 | 16 | def __init__(self, *args, **kwargs): 17 | Gtk.Box.__init__(self, *args, **kwargs) 18 | 19 | self.vid_url = None 20 | self.stopped = False 21 | 22 | self.canvas = Gtk.DrawingArea() 23 | self.pack_start(self.canvas, True, True, 0) 24 | 25 | self.player = mpv.MPV(ytdl=True, input_cursor=False, cursor_autohide=False) 26 | self.canvas.connect('realize', self.on_canvas_realize) 27 | self.canvas.connect('draw', self.on_canvas_draw) 28 | 29 | add_widget_class(self, 'player-video') 30 | 31 | def open(self, url): 32 | self.vid_url = url 33 | self.player.play(url) 34 | 35 | def play(self): 36 | if self.stopped: 37 | self.stopped = False 38 | self.player.play(self.vid_url) 39 | else: 40 | self.player._set_property('pause', False) 41 | 42 | self.callback('PLAYING') 43 | 44 | def pause(self): 45 | self.player._set_property('pause', True) 46 | self.callback('PAUSED') 47 | 48 | def stop(self): 49 | self.stopped = True 50 | self.player.command('stop') 51 | self.callback('STOPPED') 52 | 53 | def set_volume(self, volume): 54 | volume = int(round(volume * 100)) 55 | self.player._set_property('volume', volume) 56 | 57 | def on_canvas_realize(self, widget): 58 | self.player.wid = widget.get_property('window').get_xid() 59 | 60 | def on_canvas_draw(self, widget, cr): 61 | cr.set_source_rgb(0.0, 0.0, 0.0) 62 | cr.paint() 63 | -------------------------------------------------------------------------------- /widgets/vlcbox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import vlc 3 | 4 | gi.require_version('Gtk', '3.0') 5 | 6 | from gi.repository import Gtk, GObject 7 | from helpers.gtk import add_widget_class 8 | 9 | 10 | class VlcBox(Gtk.Box): 11 | 12 | __gtype_name__ = 'VlcBox' 13 | 14 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 15 | 16 | def __init__(self, *args, **kwargs): 17 | Gtk.Box.__init__(self, *args, **kwargs) 18 | 19 | self.canvas = Gtk.DrawingArea() 20 | self.pack_start(self.canvas, True, True, 0) 21 | 22 | self.instance = vlc.Instance() 23 | self.canvas.connect('realize', self.on_canvas_realized) 24 | self.canvas.connect('draw', self.on_canvas_draw) 25 | 26 | self.player = self.instance.media_player_new() 27 | self.player.video_set_scale(0) 28 | self.player.video_set_aspect_ratio('16:9') 29 | self.player.video_set_deinterlace('on') 30 | self.player.video_set_mouse_input(False) 31 | self.player.video_set_key_input(False) 32 | 33 | add_widget_class(self, 'player-video') 34 | 35 | def open(self, url): 36 | location = self.instance.media_new_location(url) 37 | self.player.set_media(location) 38 | 39 | self.play() 40 | 41 | def play(self): 42 | self.player.play() 43 | self.callback('PLAYING') 44 | 45 | def pause(self): 46 | self.player.pause() 47 | self.callback('PAUSED') 48 | 49 | def stop(self): 50 | self.player.stop() 51 | self.callback('STOPPED') 52 | 53 | def set_volume(self, volume): 54 | volume = int(round(volume * 100)) 55 | self.player.audio_set_volume(volume) 56 | 57 | def on_canvas_realized(self, widget): 58 | xid = widget.get_property('window').get_xid() 59 | self.player.set_xwindow(xid) 60 | 61 | def on_canvas_draw(self, widget, cr): 62 | cr.set_source_rgb(0.0, 0.0, 0.0) 63 | cr.paint() 64 | -------------------------------------------------------------------------------- /handlers/cache.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from playhouse.sqlite_ext import CharField, DateTimeField, IntegerField 4 | 5 | from peewee import IntegrityError, Model 6 | from helpers.utils import database_connection 7 | from helpers.utils import now 8 | 9 | 10 | class CacheHandler(object): 11 | 12 | def __init__(self): 13 | self.dbs = database_connection('cache.db') 14 | self.register_models() 15 | 16 | def register_models(self): 17 | self.dbs.connect() 18 | self.dbs.create_tables([Cacheable], safe=True) 19 | 20 | def get(self, key): 21 | try: 22 | item = Cacheable.get(key=key) 23 | except Cacheable.DoesNotExist: 24 | item = None 25 | 26 | return item 27 | 28 | def create(self, key, value, ttl=0): 29 | try: 30 | item = Cacheable.create(key=key, value=value.strip(), ttl=ttl) 31 | except IntegrityError: 32 | item = None 33 | 34 | return item 35 | 36 | def update(self, item, value, ttl=0): 37 | kwargs = { 38 | 'value': value.strip(), 39 | 'ttl': ttl, 40 | 'updated': now() 41 | } 42 | 43 | try: 44 | query = Cacheable.update(**kwargs).where(Cacheable.key == item.key) 45 | query.execute() 46 | except IntegrityError: 47 | pass 48 | 49 | return item 50 | 51 | def load(self, key): 52 | item = self.get(key) 53 | 54 | if self.is_valid(item): 55 | return item 56 | 57 | return None 58 | 59 | def save(self, key, value, ttl=0): 60 | item = self.get(key) 61 | 62 | if item is None: 63 | item = self.create(key, value, ttl) 64 | else: 65 | item = self.update(item, value, ttl) 66 | 67 | return item 68 | 69 | def is_valid(self, item): 70 | try: 71 | diff = (now() - item.updated).total_seconds() 72 | 73 | if abs(diff) < abs(item.ttl): 74 | return True 75 | except AttributeError: 76 | pass 77 | 78 | return False 79 | 80 | 81 | class Cacheable(Model): 82 | key = CharField(unique=True) 83 | value = CharField() 84 | ttl = IntegerField(default=0) 85 | created = DateTimeField(default=now()) 86 | updated = DateTimeField(default=now()) 87 | 88 | class Meta: 89 | database = database_connection('cache.db') 90 | 91 | @property 92 | 93 | def text(self): 94 | return '' if self.value is None else str(self.value) 95 | 96 | @property 97 | 98 | def json(self): 99 | data = '[]' if self.value is None else str(self.value) 100 | data = json.loads(data) 101 | 102 | return data 103 | -------------------------------------------------------------------------------- /widgets/gstbox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | gi.require_version('Gst', '1.0') 5 | 6 | from gi.repository import Gtk, Gst, GObject 7 | from helpers.gtk import add_widget_class 8 | 9 | Gst.init(None) 10 | 11 | 12 | class GstBox(Gtk.Box): 13 | 14 | __gtype_name__ = 'GstBox' 15 | 16 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 17 | 18 | def __init__(self, *args, **kwargs): 19 | Gtk.Box.__init__(self, *args, **kwargs) 20 | 21 | self.gtksink = Gst.ElementFactory.make('gtksink') 22 | self.swidget = self.gtksink.props.widget 23 | self.pack_start(self.swidget, True, True, 0) 24 | 25 | self.playbin = Gst.ElementFactory.make('playbin') 26 | self.playbin.set_property('video-sink', self.gtksink) 27 | self.playbin.set_property('force-aspect-ratio', True) 28 | 29 | self.dbus = self.playbin.get_bus() 30 | self.dbus.add_signal_watch() 31 | self.dbus.connect('message', self.on_dbus_message) 32 | 33 | add_widget_class(self, 'player-video') 34 | 35 | def open(self, url): 36 | self.playbin.set_state(Gst.State.NULL) 37 | self.playbin.set_property('uri', url) 38 | 39 | def play(self): 40 | self.playbin.set_state(Gst.State.PLAYING) 41 | self.callback('PLAYING') 42 | 43 | def pause(self): 44 | self.playbin.set_state(Gst.State.PAUSED) 45 | self.callback('PAUSED') 46 | 47 | def stop(self): 48 | self.playbin.set_state(Gst.State.NULL) 49 | self.callback('STOPPED') 50 | 51 | def set_volume(self, volume): 52 | self.playbin.set_property('volume', volume) 53 | 54 | def on_buffering(self, message): 55 | percent = int(message.parse_buffering()) 56 | self.callback('BUFFER', "%s%s" % (percent, '%')) 57 | 58 | if percent < 100: 59 | self.playbin.set_state(Gst.State.PAUSED) 60 | else: 61 | self.playbin.set_state(Gst.State.PLAYING) 62 | self.callback('PLAYING') 63 | 64 | def on_error(self, message): 65 | error = message.parse_error() 66 | self.playbin.set_state(Gst.State.READY) 67 | self.callback("%s.." % error[0].message) 68 | 69 | def on_eos(self): 70 | self.playbin.set_state(Gst.State.READY) 71 | self.callback('End of stream reached...') 72 | 73 | def on_clock_lost(self): 74 | self.playbin.set_state(Gst.State.PAUSED) 75 | self.playbin.set_state(Gst.State.PLAYING) 76 | 77 | def on_dbus_message(self, _bus, message): 78 | if message.type == Gst.MessageType.ERROR: 79 | self.on_error(message) 80 | elif message.type == Gst.MessageType.EOS: 81 | self.on_eos() 82 | elif message.type == Gst.MessageType.BUFFERING: 83 | self.on_buffering(message) 84 | elif message.type == Gst.MessageType.CLOCK_LOST: 85 | self.on_clock_lost() 86 | -------------------------------------------------------------------------------- /helpers/gtk.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gi 3 | 4 | gi.require_version('Gtk', '3.0') 5 | gi.require_version('Gdk', '3.0') 6 | gi.require_version('GLib', '2.0') 7 | 8 | from gi.repository import Gtk, Gdk, GLib, GdkPixbuf 9 | from helpers.utils import relative_path 10 | 11 | 12 | def add_widget_class(widget, classes): 13 | context = widget.get_style_context() 14 | 15 | if type(classes) not in (tuple, list): 16 | classes = classes.split(' ') 17 | 18 | for name in classes: 19 | context.add_class(name) 20 | 21 | 22 | def remove_widget_class(widget, classes): 23 | context = widget.get_style_context() 24 | 25 | if type(classes) not in (tuple, list): 26 | classes = classes.split(' ') 27 | 28 | for name in classes: 29 | context.remove_class(name) 30 | 31 | 32 | def add_widget_custom_css(widget, style): 33 | priority = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 34 | provider = Gtk.CssProvider() 35 | context = widget.get_style_context() 36 | filename = relative_path(style) 37 | 38 | if os.path.exists(filename): 39 | provider.load_from_path(filename) 40 | else: 41 | provider.load_from_data(style.encode()) 42 | 43 | context.add_provider(provider, priority) 44 | 45 | 46 | def add_custom_css(style): 47 | screen = Gdk.Screen.get_default() 48 | priority = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 49 | provider = Gtk.CssProvider() 50 | filename = relative_path(style) 51 | 52 | if os.path.exists(filename): 53 | provider.load_from_path(filename) 54 | else: 55 | provider.load_from_data(style.encode()) 56 | 57 | Gtk.StyleContext.add_provider_for_screen(screen, provider, priority) 58 | 59 | 60 | def remove_widget_children(widget): 61 | widget.foreach(lambda x: x.destroy()) 62 | 63 | 64 | def run_generator(function): 65 | gen = function() 66 | GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) 67 | 68 | 69 | def image_from_path(path, size=48, image=None): 70 | gimage = Gtk.Image() if image is None else image 71 | 72 | try: 73 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, size, size, True) 74 | gimage.set_from_pixbuf(pixbuf) 75 | except GLib.Error: 76 | pass 77 | 78 | return gimage 79 | 80 | 81 | def set_scroll_position(widget, value, direction='vertical', window=None): 82 | viewport = widget.get_ancestor(Gtk.Viewport) 83 | 84 | if direction == 'vertical': 85 | viewport.get_vadjustment().set_value(value) 86 | 87 | if direction == 'horizontal': 88 | viewport.get_hadjustment().set_value(value) 89 | 90 | if window is not None: 91 | window.queue_resize_no_redraw() 92 | 93 | 94 | def toggle_cursor(widget, hide=False): 95 | window = widget.get_window() 96 | 97 | if window: 98 | blank = Gdk.CursorType.BLANK_CURSOR 99 | cursor = Gdk.Cursor(blank) if hide else None 100 | GLib.idle_add(window.set_cursor, cursor) 101 | -------------------------------------------------------------------------------- /ui/styles.css: -------------------------------------------------------------------------------- 1 | /* General */ 2 | 3 | .category-sidebar { 4 | border-top-style: none; 5 | border-bottom-style: none; 6 | border-left-style: none; 7 | } 8 | 9 | .player-video, .player-toolbox { 10 | background: #000; 11 | } 12 | 13 | 14 | /* Events */ 15 | 16 | .event-item { 17 | background: @theme_base_color; 18 | border: 1px solid @borders; 19 | min-width: 450px; 20 | padding: 0; 21 | margin: 0; 22 | } 23 | 24 | .event-score, .event-today { 25 | font-weight: bold; 26 | padding: 5px 10px; 27 | background: @theme_bg_color; 28 | border-radius: 2px; 29 | border: 1px solid @borders; 30 | } 31 | 32 | .event-live { 33 | color: @theme_selected_fg_color; 34 | background: @theme_selected_bg_color; 35 | border: 1px solid @theme_selected_bg_color; 36 | } 37 | 38 | .event-item .team-emblem { 39 | min-width: 48px; 40 | min-height: 48px; 41 | } 42 | 43 | .event-item .team-name { 44 | font-size: 85%; 45 | } 46 | 47 | .event-item-streams { 48 | margin: 0; 49 | padding: 8px 10px 7px 12px; 50 | border-top: 1px solid @borders; 51 | background: rgba(0, 0, 0, 0.03); 52 | } 53 | 54 | .event-item-streams label { 55 | font-size: 80%; 56 | opacity: 0.8; 57 | } 58 | 59 | .event-item-counter { 60 | background: rgba(0, 0, 0, 0.12); 61 | padding: 5px; 62 | border-radius: 4px; 63 | min-width: 25px; 64 | font-weight: 600; 65 | } 66 | 67 | .event-item-counter.no-streams { 68 | background: rgba(0, 0, 0, 0.03); 69 | } 70 | 71 | 72 | /* Channels */ 73 | 74 | .channel-item { 75 | margin: 0; 76 | padding: 0; 77 | min-width: 300px; 78 | background: @theme_base_color; 79 | border: 1px solid @borders; 80 | } 81 | 82 | .channel-language { 83 | font-size: 80%; 84 | padding: 2px 4px; 85 | border-radius: 2px; 86 | min-width: 25px; 87 | border: 1px solid @borders; 88 | } 89 | 90 | .channel-streams { 91 | margin: 0; 92 | padding: 0; 93 | border-top: 1px solid @borders; 94 | background: rgba(0, 0, 0, 0.03); 95 | } 96 | 97 | .channel-stream-item { 98 | padding: 8px 10px 7px 12px; 99 | } 100 | 101 | .channel-stream-item + .channel-stream-item { 102 | border-left: 1px solid @borders; 103 | } 104 | 105 | 106 | /* Streams */ 107 | 108 | .event-streams { 109 | border-left: 1px solid @borders; 110 | border-right: 1px solid @borders; 111 | border-bottom: 1px solid @borders; 112 | } 113 | 114 | .stream-item { 115 | padding: 10px 12px 8px 15px; 116 | border-top: 1px solid @borders; 117 | } 118 | 119 | .stream-language { 120 | font-size: 80%; 121 | padding: 2px 4px; 122 | border-radius: 2px; 123 | min-width: 25px; 124 | border: 1px solid @borders; 125 | } 126 | 127 | .stream-rate { 128 | font-size: 80%; 129 | opacity: 0.8; 130 | } 131 | 132 | .stream-unknown { 133 | opacity: 0.5; 134 | } 135 | 136 | .streams-title { 137 | font-size: 130%; 138 | font-weight: 600; 139 | } 140 | 141 | .event-teams { 142 | padding-top: 15px; 143 | padding-bottom: 15px; 144 | } 145 | -------------------------------------------------------------------------------- /ui/matches.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 980 8 | 585 9 | True 10 | False 11 | 12 | 13 | True 14 | True 15 | 220 16 | 585 17 | 18 | 19 | True 20 | False 21 | 22 | 23 | True 24 | False 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | False 37 | True 38 | 0 39 | 40 | 41 | 42 | 43 | True 44 | True 45 | 760 46 | 585 47 | 48 | 49 | True 50 | False 51 | 52 | 53 | True 54 | False 55 | start 56 | 20 57 | 20 58 | 20 59 | 20 60 | True 61 | 20 62 | 20 63 | 2 64 | none 65 | 66 | 67 | 68 | 69 | 70 | 71 | True 72 | True 73 | 1 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ui/channels.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 980 8 | 585 9 | True 10 | False 11 | 12 | 13 | True 14 | True 15 | 220 16 | 585 17 | 18 | 19 | True 20 | False 21 | 22 | 23 | True 24 | False 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | False 37 | True 38 | 0 39 | 40 | 41 | 42 | 43 | True 44 | True 45 | 760 46 | 585 47 | 48 | 49 | True 50 | False 51 | 52 | 53 | True 54 | False 55 | start 56 | 20 57 | 20 58 | 20 59 | 20 60 | True 61 | 20 62 | 20 63 | 3 64 | none 65 | 66 | 67 | 68 | 69 | 70 | 71 | True 72 | True 73 | 1 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /handlers/stream.py: -------------------------------------------------------------------------------- 1 | import time 2 | import hashlib 3 | import pexpect 4 | 5 | from helpers.utils import in_thread, run_command, kill_proccess 6 | 7 | 8 | class StreamHandler(object): 9 | def __init__(self, player): 10 | self.player = player 11 | self.acestream = None 12 | self.url = None 13 | self.session = None 14 | 15 | def notify(self, message): 16 | messages = { 17 | 'starting': 'Starting stream engine...', 18 | 'running': 'Stream engine running...', 19 | 'error': 'Stream engine error!', 20 | 'playing': 'Playing', 21 | 'waiting': 'Waiting for response...', 22 | 'unavailable': 'Stream unavailable!' 23 | } 24 | message = messages[message] 25 | self.player.update_status(message) 26 | 27 | def open(self, url): 28 | self.player.url = None 29 | self.player.stop() 30 | 31 | self.player.loading = True 32 | in_thread(target=self.open_stream, args=[url]) 33 | 34 | def close(self): 35 | self.stop_acestream() 36 | 37 | def open_stream(self, url): 38 | self.close() 39 | self.notify('starting') 40 | 41 | if url.startswith('acestream://'): 42 | self.start_acestream(url) 43 | 44 | if not self.url is None: 45 | self.player.open(self.url) 46 | 47 | self.player.loading = False 48 | 49 | def start_acestream(self, url): 50 | engine = '/usr/bin/acestreamengine' 51 | client = '--client-console' 52 | 53 | try: 54 | self.acestream = run_command([engine, client]) 55 | self.notify('running') 56 | time.sleep(5) 57 | except FileNotFoundError: 58 | self.notify('error') 59 | self.stop_acestream() 60 | 61 | pid = url.split('://')[1] 62 | self.start_acestream_session(pid) 63 | 64 | def stop_acestream(self): 65 | if not self.acestream is None: 66 | self.acestream.kill() 67 | 68 | if not self.session is None: 69 | self.session.close() 70 | 71 | kill_proccess('acestreamengine') 72 | 73 | self.player.loading = False 74 | 75 | def start_acestream_session(self, pid): 76 | product_key = 'n51LvQoTlJzNGaFxseRK-uvnvX-sD4Vm5Axwmc4UcoD-jruxmKsuJaH0eVgE' 77 | session = pexpect.spawn('telnet localhost 62062') 78 | self.notify('waiting') 79 | 80 | try: 81 | session.timeout = 10 82 | session.sendline('HELLOBG version=3') 83 | session.expect('key=.*') 84 | 85 | request_key = session.after.decode('utf-8').split()[0].split('=')[1] 86 | signature = (request_key + product_key).encode('utf-8') 87 | signature = hashlib.sha1(signature).hexdigest() 88 | response_key = product_key.split('-')[0] + '-' + signature 89 | 90 | session.sendline('READY key=' + response_key) 91 | session.expect('AUTH.*') 92 | session.sendline('USERDATA [{"gender": "1"}, {"age": "3"}]') 93 | except (pexpect.TIMEOUT, pexpect.EOF): 94 | self.notify('error') 95 | self.stop_acestream() 96 | 97 | try: 98 | session.timeout = 30 99 | session.sendline('START PID ' + pid + ' 0') 100 | session.expect('http://.*') 101 | 102 | self.session = session 103 | self.url = session.after.decode('utf-8').split()[0] 104 | self.notify('playing') 105 | except (pexpect.TIMEOUT, pexpect.EOF): 106 | self.notify('unavailable') 107 | self.stop_acestream() 108 | -------------------------------------------------------------------------------- /kickoff_player.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import gi 4 | import signal 5 | import argparse 6 | 7 | gi.require_version('Gtk', '3.0') 8 | gi.require_version('GLib', '2.0') 9 | 10 | from gi.repository import Gtk, GLib 11 | 12 | from handlers.data import DataHandler, StaticStream 13 | from handlers.cache import CacheHandler 14 | from handlers.match import MatchHandler 15 | from handlers.channel import ChannelHandler 16 | from handlers.player import PlayerHandler 17 | 18 | from apis.scores import ScoresApi 19 | from apis.streams import StreamsApi 20 | 21 | from helpers.gtk import add_custom_css, relative_path 22 | 23 | 24 | class KickoffPlayer(object): 25 | 26 | def __init__(self, cache, data): 27 | GLib.set_prgname('kickoff-player') 28 | GLib.set_application_name('Kickoff Player') 29 | 30 | add_custom_css('ui/styles.css') 31 | 32 | self.argparse = argparse.ArgumentParser(prog='kickoff-player') 33 | self.argparse.add_argument('url', metavar='URL', nargs='?', default=None) 34 | 35 | self.cache = cache 36 | self.data = data 37 | 38 | self.scores_api = ScoresApi(self.data, self.cache) 39 | self.streams_api = StreamsApi(self.data, self.cache) 40 | 41 | self.main = Gtk.Builder() 42 | self.main.add_from_file(relative_path('ui/main.ui')) 43 | self.main.connect_signals(self) 44 | 45 | self.window = self.main.get_object('window_main') 46 | self.header_back = self.main.get_object('header_button_back') 47 | self.header_reload = self.main.get_object('header_button_reload') 48 | self.main_stack = self.main.get_object('stack_main') 49 | 50 | self.player_stack = self.main.get_object('stack_player') 51 | self.matches_stack = self.main.get_object('stack_matches') 52 | self.channels_stack = self.main.get_object('stack_channels') 53 | 54 | self.matches = MatchHandler(self) 55 | self.channels = ChannelHandler(self) 56 | self.player = PlayerHandler(self) 57 | 58 | GLib.timeout_add(2000, self.toggle_reload, True) 59 | self.open_stream_url() 60 | 61 | def run(self): 62 | self.window.show_all() 63 | Gtk.main() 64 | 65 | def quit(self): 66 | self.player.close() 67 | Gtk.main_quit() 68 | 69 | def open_stream_url(self): 70 | url = self.argparse.parse_args().url 71 | 72 | if url is not None: 73 | stream = StaticStream(url) 74 | self.player.open_stream(stream) 75 | 76 | self.set_stack_visible_child(self.player_stack) 77 | 78 | def toggle_reload(self, show): 79 | self.header_reload.set_sensitive(show) 80 | 81 | def get_stack_visible_child(self): 82 | return self.main_stack.get_visible_child() 83 | 84 | def set_stack_visible_child(self, widget): 85 | self.main_stack.set_visible_child(widget) 86 | 87 | def on_window_main_destroy(self, _event): 88 | self.quit() 89 | 90 | def on_window_main_key_release_event(self, widget, event): 91 | self.player.on_window_main_key_release_event(widget, event) 92 | 93 | def on_header_button_back_clicked(self, widget): 94 | self.matches.on_header_button_back_clicked(widget) 95 | 96 | def on_header_button_reload_clicked(self, widget): 97 | self.player.on_header_button_reload_clicked(widget) 98 | self.matches.on_header_button_reload_clicked(widget) 99 | self.channels.on_header_button_reload_clicked(widget) 100 | 101 | def on_stack_main_visible_child_notify(self, widget, params): 102 | self.matches.on_stack_main_visible_child_notify(widget, params) 103 | self.channels.on_stack_main_visible_child_notify(widget, params) 104 | 105 | 106 | if __name__ == '__main__': 107 | signal.signal(signal.SIGINT, signal.SIG_DFL) 108 | 109 | cache = CacheHandler() 110 | data = DataHandler() 111 | 112 | player = KickoffPlayer(cache, data) 113 | player.run() 114 | -------------------------------------------------------------------------------- /widgets/streambox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | 5 | from gi.repository import Gtk, GObject 6 | from helpers.gtk import add_widget_class, image_from_path 7 | 8 | 9 | class StreamBox(Gtk.Box): 10 | 11 | __gtype_name__ = 'StreamBox' 12 | 13 | stream = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 14 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 15 | compact = GObject.property(type=bool, default=False, flags=GObject.PARAM_READWRITE) 16 | 17 | def __init__(self, *args, **kwargs): 18 | Gtk.Box.__init__(self, *args, **kwargs) 19 | 20 | self.stream = self.get_property('stream') 21 | self.callback = self.get_property('callback') 22 | self.compact = self.get_property('compact') 23 | 24 | self.stream_name = self.do_stream_name() 25 | self.stream_rate = self.do_stream_rate() 26 | self.stream_logo = self.do_stream_logo() 27 | self.stream_lang = self.do_stream_language() 28 | self.play_button = self.do_play_button() 29 | 30 | self.set_orientation(Gtk.Orientation.HORIZONTAL) 31 | self.connect('realize', self.on_realized) 32 | self.connect('notify::stream', self.on_stream_updated) 33 | 34 | self.show() 35 | 36 | def on_realized(self, *_args): 37 | self.on_stream_updated(_args) 38 | 39 | self.pack_start(self.stream_lang, False, False, 0) 40 | self.pack_start(self.stream_logo, False, False, 1) 41 | self.pack_start(self.stream_name, False, False, 2) 42 | self.pack_end(self.play_button, False, False, 0) 43 | self.pack_end(self.stream_rate, False, False, 1) 44 | 45 | def on_stream_updated(self, *_args): 46 | self.update_stream_language() 47 | self.update_stream_logo() 48 | self.update_stream_name() 49 | self.update_play_button() 50 | self.update_stream_rate() 51 | 52 | def do_stream_logo(self): 53 | image = image_from_path(path='images/acestream.svg', size=16) 54 | image.set_halign(Gtk.Align.CENTER) 55 | image.set_valign(Gtk.Align.CENTER) 56 | image.set_margin_right(10) 57 | 58 | add_widget_class(image, 'stream-image') 59 | 60 | return image 61 | 62 | def update_stream_logo(self): 63 | logo = getattr(self.stream, 'logo') 64 | image_from_path(path=logo, size=16, image=self.stream_logo) 65 | self.stream_logo.show() 66 | 67 | def do_stream_language(self): 68 | label = Gtk.Label('Unknown') 69 | label.set_halign(Gtk.Align.START) 70 | label.set_margin_right(10) 71 | 72 | add_widget_class(label, 'stream-language') 73 | 74 | return label 75 | 76 | def update_stream_language(self): 77 | language = getattr(self.stream, 'language') 78 | self.stream_lang.set_label(language) 79 | 80 | if self.compact: 81 | self.stream_lang.hide() 82 | else: 83 | self.stream_lang.show() 84 | 85 | def do_stream_rate(self): 86 | label = Gtk.Label('0Kbps') 87 | label.set_halign(Gtk.Align.END) 88 | label.set_margin_right(10) 89 | 90 | add_widget_class(label, 'stream-rate') 91 | 92 | return label 93 | 94 | def update_stream_rate(self): 95 | ratio = "%sKbps" % str(getattr(self.stream, 'rate')) 96 | self.stream_rate.set_label(ratio) 97 | self.stream_rate.show() 98 | 99 | def do_stream_name(self): 100 | label = Gtk.Label('Unknown Channel') 101 | label.set_halign(Gtk.Align.START) 102 | label.set_margin_right(10) 103 | 104 | add_widget_class(label, 'stream-name') 105 | 106 | return label 107 | 108 | def update_stream_name(self): 109 | chan = getattr(self.stream, 'channel') 110 | name = 'Unknown Channel' if chan is None else getattr(chan, 'name') 111 | self.stream_name.set_label(name) 112 | 113 | if self.compact: 114 | self.stream_name.hide() 115 | else: 116 | self.stream_name.show() 117 | 118 | def do_play_button(self): 119 | kwargs = { 'icon_name': 'media-playback-start-symbolic', 'size': Gtk.IconSize.BUTTON } 120 | button = Gtk.Button.new_from_icon_name(**kwargs) 121 | button.set_halign(Gtk.Align.END) 122 | button.connect('clicked', self.on_play_button_clicked) 123 | 124 | add_widget_class(button, 'stream-play') 125 | 126 | return button 127 | 128 | def update_play_button(self): 129 | self.play_button.show() 130 | 131 | def on_play_button_clicked(self, _widget): 132 | self.callback(self.stream) 133 | -------------------------------------------------------------------------------- /handlers/channel.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import time 3 | 4 | gi.require_version('Gtk', '3.0') 5 | gi.require_version('GLib', '2.0') 6 | 7 | from gi.repository import Gtk, GLib 8 | from helpers.utils import in_thread, relative_path 9 | from helpers.gtk import remove_widget_children, set_scroll_position, run_generator 10 | 11 | from widgets.channelbox import ChannelBox 12 | from widgets.filterbox import FilterBox 13 | 14 | 15 | class ChannelHandler(object): 16 | 17 | def __init__(self, app): 18 | self.app = app 19 | self.stack = app.channels_stack 20 | self.filter = None 21 | 22 | self.channels = Gtk.Builder() 23 | self.channels.add_from_file(relative_path('ui/channels.ui')) 24 | self.channels.connect_signals(self) 25 | 26 | self.channels_box = self.channels.get_object('box_channels') 27 | self.stack.add_named(self.channels_box, 'channels_container') 28 | 29 | self.channels_filters = self.channels.get_object('list_box_channels_filters') 30 | self.channels_list = self.channels.get_object('flow_box_channels_list') 31 | self.channels_list.set_filter_func(self.on_channels_list_row_changed) 32 | 33 | GLib.idle_add(self.do_initial_setup) 34 | 35 | @property 36 | 37 | def visible(self): 38 | return self.app.get_stack_visible_child() == self.stack 39 | 40 | def initial_setup(self): 41 | if not self.app.data.load_channels(): 42 | self.update_channels_data() 43 | 44 | def do_initial_setup(self): 45 | in_thread(target=self.initial_setup) 46 | 47 | def do_channels_widgets(self): 48 | if not self.channels_filters.get_children(): 49 | run_generator(self.do_channels_filters) 50 | 51 | if not self.channels_list.get_children(): 52 | run_generator(self.do_channels_list) 53 | 54 | def update_channels_widgets(self): 55 | if self.channels_filters.get_children(): 56 | run_generator(self.update_channels_filters) 57 | 58 | if self.channels_list.get_children(): 59 | run_generator(self.update_channels_list) 60 | 61 | def update_channels_data(self): 62 | in_thread(target=self.do_update_channels_data) 63 | 64 | def do_update_channels_data(self): 65 | GLib.idle_add(self.app.toggle_reload, False) 66 | 67 | self.app.streams_api.save_channels() 68 | time.sleep(5) 69 | 70 | GLib.idle_add(self.do_channels_widgets) 71 | GLib.idle_add(self.update_channels_widgets) 72 | GLib.idle_add(self.app.toggle_reload, True) 73 | 74 | def do_channels_filters(self): 75 | filters = self.app.data.load_channels_filters() 76 | remove_widget_children(self.channels_filters) 77 | 78 | for filter_name in filters: 79 | self.do_filter_item(filter_name) 80 | yield True 81 | 82 | def do_filter_item(self, filter_name): 83 | filterbox = FilterBox(filter_name=filter_name, filter_all='All Languages') 84 | self.channels_filters.add(filterbox) 85 | 86 | if not self.channels_filters.get_selected_row(): 87 | self.channels_filters.select_row(filterbox) 88 | 89 | def update_channels_filters(self): 90 | filters = self.app.data.load_channels_filters() 91 | 92 | for item in self.channels_filters.get_children(): 93 | if item.filter_name not in filters: 94 | item.destroy() 95 | 96 | yield True 97 | 98 | def do_channels_list(self): 99 | channels = self.app.data.load_channels(True) 100 | remove_widget_children(self.channels_list) 101 | 102 | for channel in channels: 103 | self.do_channel_item(channel) 104 | yield True 105 | 106 | def do_channel_item(self, channel): 107 | channbox = ChannelBox(channel=channel, callback=self.app.player.open_stream) 108 | self.channels_list.add(channbox) 109 | 110 | def update_channels_list(self): 111 | channels = self.app.data.load_channels(True, True) 112 | 113 | for item in self.channels_list.get_children(): 114 | if item.channel.id in channels: 115 | updated = self.app.data.get_single('channel', { 'id': item.channel.id }) 116 | item.set_property('channel', updated) 117 | else: 118 | item.destroy() 119 | 120 | yield True 121 | 122 | def on_stack_main_visible_child_notify(self, _widget, _params): 123 | if self.visible: 124 | self.do_channels_widgets() 125 | 126 | def on_header_button_reload_clicked(self, _event): 127 | if self.visible: 128 | self.update_channels_data() 129 | 130 | def on_channels_list_row_changed(self, item): 131 | return self.filter is None or item.filter_name == self.filter 132 | 133 | def on_list_box_channels_filters_row_activated(self, _listbox, item): 134 | self.filter = item.filter_value 135 | 136 | self.channels_list.invalidate_filter() 137 | set_scroll_position(self.channels_list, 0, 'vertical', self.app.window) 138 | -------------------------------------------------------------------------------- /ui/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | False 8 | 980 9 | 585 10 | kickoff-player 11 | False 12 | 13 | 14 | 15 | 16 | True 17 | False 18 | True 19 | 20 | 21 | True 22 | True 23 | True 24 | 25 | 26 | 27 | True 28 | False 29 | go-previous-symbolic 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | False 38 | stack_main 39 | 40 | 41 | 42 | 43 | True 44 | False 45 | True 46 | True 47 | 48 | 49 | 50 | True 51 | False 52 | view-refresh-symbolic 53 | 54 | 55 | 56 | 57 | end 58 | 2 59 | 60 | 61 | 62 | 63 | 64 | 65 | 980 66 | 585 67 | True 68 | False 69 | 70 | 71 | 72 | True 73 | False 74 | 75 | 76 | 77 | 78 | 79 | stack_events 80 | Matches 81 | 82 | 83 | 84 | 85 | True 86 | False 87 | 88 | 89 | 90 | 91 | 92 | stack_channels 93 | Channels 94 | 1 95 | 96 | 97 | 98 | 99 | True 100 | False 101 | 102 | 103 | 104 | 105 | 106 | stack_player 107 | Player 108 | 2 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /widgets/channelbox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | 5 | from gi.repository import Gtk, GObject 6 | from widgets.streambox import StreamBox 7 | from helpers.gtk import add_widget_class, remove_widget_children, image_from_path 8 | 9 | 10 | class ChannelBox(Gtk.FlowBoxChild): 11 | 12 | __gtype_name__ = 'ChannelBox' 13 | __gsignals__ = { 'stream-activate': (GObject.SIGNAL_RUN_FIRST, None, (object,)) } 14 | 15 | channel = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 16 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 17 | filter_name = GObject.property(type=str, flags=GObject.PARAM_READWRITE) 18 | 19 | def __init__(self, *args, **kwargs): 20 | Gtk.FlowBoxChild.__init__(self, *args, **kwargs) 21 | 22 | self.channel = self.get_property('channel') 23 | self.callback = self.get_property('callback') 24 | self.filter_name = self.get_property('filter_name') 25 | 26 | self.outer_box = self.do_outer_box() 27 | self.header_box = self.do_header_box() 28 | self.streams_box = self.do_streams_box() 29 | 30 | self.set_valign(Gtk.Align.START) 31 | self.connect('realize', self.on_channel_updated) 32 | self.connect('realize', self.on_realize) 33 | self.connect('notify::channel', self.on_channel_updated) 34 | 35 | add_widget_class(self, 'channel-item') 36 | 37 | self.show() 38 | 39 | def on_realize(self, *_args): 40 | self.update_outer_box() 41 | 42 | self.outer_box.pack_start(self.header_box, True, True, 0) 43 | self.outer_box.pack_start(self.streams_box, True, True, 1) 44 | 45 | def on_channel_updated(self, *_args): 46 | self.filter_name = getattr(self.channel, 'language') 47 | self.update_header_box() 48 | self.update_streams_box() 49 | 50 | def do_outer_box(self): 51 | box = Gtk.Box() 52 | box.set_orientation(Gtk.Orientation.VERTICAL) 53 | 54 | return box 55 | 56 | def update_outer_box(self): 57 | self.add(self.outer_box) 58 | self.outer_box.show() 59 | 60 | def do_header_box(self): 61 | return ChannelHeaderBox(channel=self.channel) 62 | 63 | def update_header_box(self): 64 | self.header_box.set_property('channel', self.channel) 65 | 66 | def do_streams_box(self): 67 | return ChannelStreamsBox(channel=self.channel, callback=self.callback) 68 | 69 | def update_streams_box(self): 70 | self.streams_box.set_property('channel', self.channel) 71 | 72 | 73 | class ChannelHeaderBox(Gtk.Box): 74 | 75 | __gtype_name__ = 'ChannelHeaderBox' 76 | 77 | channel = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 78 | 79 | def __init__(self, *args, **kwargs): 80 | Gtk.Box.__init__(self, *args, **kwargs) 81 | 82 | self.channel = self.get_property('channel') 83 | self.channel_logo = self.do_channel_logo() 84 | self.channel_name = self.do_channel_name() 85 | self.channel_language = self.do_channel_language() 86 | 87 | self.set_orientation(Gtk.Orientation.VERTICAL) 88 | self.set_margin_top(10) 89 | self.set_margin_bottom(10) 90 | self.set_margin_left(10) 91 | self.set_margin_right(10) 92 | self.set_spacing(10) 93 | 94 | self.connect('realize', self.on_channel_updated) 95 | self.connect('realize', self.on_realize) 96 | self.connect('notify::channel', self.on_channel_updated) 97 | 98 | self.show() 99 | 100 | def on_realize(self, *_args): 101 | self.pack_start(self.channel_logo, False, False, 0) 102 | self.pack_start(self.channel_name, True, True, 1) 103 | self.pack_start(self.channel_language, True, True, 2) 104 | 105 | def on_channel_updated(self, *_args): 106 | self.update_channel_logo() 107 | self.update_channel_name() 108 | self.update_channel_language() 109 | 110 | def do_channel_logo(self): 111 | image = image_from_path(path='images/channel-logo.svg') 112 | image.set_halign(Gtk.Align.CENTER) 113 | image.set_valign(Gtk.Align.CENTER) 114 | 115 | return image 116 | 117 | def update_channel_logo(self): 118 | logo = getattr(self.channel, 'logo') 119 | image_from_path(path=logo, image=self.channel_logo) 120 | self.channel_logo.show() 121 | 122 | def do_channel_name(self): 123 | label = Gtk.Label('Unknown Channel') 124 | label.set_halign(Gtk.Align.CENTER) 125 | 126 | add_widget_class(label, 'channel-name') 127 | 128 | return label 129 | 130 | def update_channel_name(self): 131 | name = getattr(self.channel, 'name') 132 | self.channel_name.set_label(name) 133 | self.channel_name.show() 134 | 135 | def do_channel_language(self): 136 | label = Gtk.Label('Unknown') 137 | label.set_halign(Gtk.Align.CENTER) 138 | 139 | add_widget_class(label, 'channel-language') 140 | 141 | return label 142 | 143 | def update_channel_language(self): 144 | language = getattr(self.channel, 'language') 145 | self.channel_language.set_label(language) 146 | self.channel_language.show() 147 | 148 | 149 | class ChannelStreamsBox(Gtk.Box): 150 | 151 | __gtype_name__ = 'ChannelStreamsBox' 152 | 153 | channel = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 154 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 155 | 156 | def __init__(self, *args, **kwargs): 157 | Gtk.Box.__init__(self, *args, **kwargs) 158 | 159 | self.channel = self.get_property('channel') 160 | self.callback = self.get_property('callback') 161 | self.streams = None 162 | 163 | self.set_orientation(Gtk.Orientation.HORIZONTAL) 164 | self.set_homogeneous(True) 165 | 166 | self.connect('realize', self.on_channel_updated) 167 | self.connect('notify::channel', self.on_channel_updated) 168 | 169 | add_widget_class(self, 'channel-streams') 170 | 171 | self.show() 172 | 173 | def on_channel_updated(self, *_args): 174 | self.streams = getattr(self.channel, 'streams') 175 | self.update_channel_streams() 176 | 177 | def do_channel_streams(self): 178 | for stream in self.streams: 179 | streambox = StreamBox(stream=stream, callback=self.callback, compact=True) 180 | self.pack_start(streambox, True, True, 0) 181 | 182 | add_widget_class(streambox, 'channel-stream-item') 183 | 184 | def update_channel_streams(self): 185 | remove_widget_children(self) 186 | self.do_channel_streams() 187 | -------------------------------------------------------------------------------- /apis/scores.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from helpers.utils import format_date, tzone, today, user_data_dir, search_dict_key 4 | from helpers.utils import cached_request, download_file, batch, in_thread, thread_pool 5 | 6 | 7 | class ScoresApi: 8 | 9 | def __init__(self, data, cache): 10 | self.data = data 11 | self.cache = cache 12 | 13 | self.score_url = 'scores-api.onefootball.com/v1' 14 | self.sconf_url = 'config.onefootball.com/api/scoreconfig2' 15 | self.feedm_url = 'feedmonster.onefootball.com/feeds/il/en/competitions' 16 | self.image_url = 'images.onefootball.com/icons/teams' 17 | self.img_path = "%s/images/" % user_data_dir() 18 | 19 | self.create_images_folder() 20 | 21 | def get(self, url, base_url, key=None, **kwargs): 22 | kwargs = { 23 | 'url': url, 24 | 'cache': self.cache, 25 | 'base_url': base_url, 26 | 'json': True, 27 | 'ttl': kwargs.get('ttl', 1800), 28 | 'params': kwargs.get('params'), 29 | 'cache_key': kwargs.get('cache_key') 30 | } 31 | 32 | response = cached_request(**kwargs) 33 | response = response if response is not None else {} 34 | response = response if key is None else search_dict_key(response, key, []) 35 | 36 | return response 37 | 38 | def get_sections(self): 39 | return self.get(url='en.json', base_url=self.sconf_url, key='sections') 40 | 41 | def get_competitions(self): 42 | return self.get(url='en.json', base_url=self.sconf_url, key='competitions') 43 | 44 | def save_competitions(self): 45 | codes = self.get_sections() 46 | comps = self.get_competitions() 47 | items = [] 48 | 49 | for item in comps: 50 | try: 51 | items.append({ 52 | 'name': item['competitionName'], 53 | 'short_name': item['competitionShortName'], 54 | 'section_code': item['section'], 55 | 'section_name': self.section_name(codes, item['section']), 56 | 'season_id': item['seasonId'], 57 | 'api_id': item['competitionId'] 58 | }) 59 | except KeyError: 60 | pass 61 | 62 | self.data.set_multiple('competition', items, 'api_id') 63 | 64 | def get_competition_teams(self, competition): 65 | competid = str(competition.api_id) 66 | seasonid = str(competition.season_id) 67 | resource = '/'.join([competid, seasonid, 'teamsOverview.json']) 68 | response = self.get(url=resource, base_url=self.feedm_url, key='teams') 69 | 70 | return response 71 | 72 | def get_teams(self): 73 | comps = self.data.load_active_competitions(True) 74 | items = thread_pool(self.get_competition_teams, list(comps)) 75 | 76 | return items 77 | 78 | def save_teams(self): 79 | teams = self.get_teams() 80 | items = [] 81 | 82 | for item in teams: 83 | try: 84 | items.append({ 85 | 'name': item['name'], 86 | 'crest_url': self.crest_url(item), 87 | 'crest_path': self.crest_path(item), 88 | 'national': item['isNational'], 89 | 'api_id': item['idInternal'] 90 | }) 91 | except KeyError: 92 | pass 93 | 94 | self.data.set_multiple('team', items, 'api_id') 95 | 96 | def get_matchdays(self, comp_ids=None): 97 | kwargs = { 98 | 'base_url': self.score_url, 99 | 'url': 'en/search/matchdays', 100 | 'key': ['data', 'matchdays'], 101 | 'cache_key': 'competitions', 102 | 'ttl': 60, 103 | 'params': { 104 | 'competitions': comp_ids, 105 | 'since': today('%Y-%m-%d'), 106 | 'utc_offset': tzone('%z') 107 | } 108 | } 109 | 110 | response = self.get(**kwargs) 111 | combined = [] 112 | 113 | for item in response: 114 | for group in item['groups']: 115 | combined = combined + group['matches'] 116 | 117 | return combined 118 | 119 | def get_matches(self): 120 | settings = self.data.load_active_competitions() 121 | comp_ids = batch(settings, 2, ',') 122 | combined = thread_pool(self.get_matchdays, comp_ids) 123 | 124 | return combined 125 | 126 | def save_matches(self): 127 | matches = self.get_matches() 128 | items = [] 129 | 130 | for item in matches: 131 | try: 132 | competition = self.data.get_single('competition', { 'api_id': item['competition']['id'] }) 133 | home_team = self.data.get_single('team', { 'api_id': item['team_home']['id'] }) 134 | away_team = self.data.get_single('team', { 'api_id': item['team_away']['id'] }) 135 | form_date = format_date(date=item['kickoff'], localize=True) 136 | 137 | items.append({ 138 | 'date': form_date, 139 | 'minute': item['minute'], 140 | 'period': item['period'], 141 | 'home_team': home_team.id, 142 | 'away_team': away_team.id, 143 | 'score_home': item['score_home'], 144 | 'score_away': item['score_away'], 145 | 'competition': competition.id, 146 | 'api_id': item['id'] 147 | }) 148 | except (AttributeError, KeyError): 149 | pass 150 | 151 | self.data.set_multiple('fixture', items, 'api_id') 152 | 153 | def get_live(self): 154 | kwargs = { 155 | 'base_url': self.score_url, 156 | 'url': 'matches/updates', 157 | 'key': ['data', 'match_updates'], 158 | 'cache_key': 'live', 159 | 'ttl': 10 160 | } 161 | 162 | return self.get(**kwargs) 163 | 164 | def save_live(self): 165 | lives = self.get_live() 166 | items = [] 167 | 168 | for item in lives: 169 | try: 170 | items.append({ 171 | 'minute': item['minute'], 172 | 'period': item['period'], 173 | 'score_home': item['score_home'], 174 | 'score_away': item['score_away'], 175 | 'api_id': item['id'] 176 | }) 177 | except KeyError: 178 | pass 179 | 180 | self.data.set_multiple('fixture', items, 'api_id') 181 | 182 | def save_crests(self): 183 | teams = self.data.load_teams() 184 | 185 | for team in teams: 186 | in_thread(target=self.download_team_crest, args=[team]) 187 | 188 | def section_name(self, codes, code): 189 | name = list(filter(lambda ccode: ccode['key'] == code, codes)) 190 | return name[0]['title'] if len(name) else None 191 | 192 | def crest_url(self, team, size='56'): 193 | img = str(team['idInternal']) + '.png' 194 | url = 'http://' + '/'.join([self.image_url, size, img]) 195 | 196 | return url 197 | 198 | def crest_path(self, team): 199 | link = self.crest_url(team) 200 | path = self.img_path + str(link).split('/')[-1] 201 | 202 | return path 203 | 204 | def create_images_folder(self): 205 | if not os.path.exists(self.img_path): 206 | os.makedirs(self.img_path) 207 | 208 | def download_team_crest(self, team): 209 | link = team.crest_url 210 | path = team.crest_path 211 | 212 | if not os.path.exists(path): 213 | path = download_file(link, path) 214 | 215 | return path 216 | -------------------------------------------------------------------------------- /apis/streams.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | from lxml import html 3 | from fuzzywuzzy import fuzz 4 | from helpers.utils import cached_request, thread_pool, replace_all 5 | 6 | 7 | class StreamsApi: 8 | 9 | def __init__(self, data, cache): 10 | self.data = data 11 | self.cache = cache 12 | 13 | def get(self, url='', ttl=3600): 14 | base_url = 'livefootballol.me' 15 | response = cached_request(url=url, cache=self.cache, base_url=base_url, ttl=ttl) 16 | 17 | try: 18 | response = html.fromstring(response) 19 | except TypeError: 20 | response = None 21 | 22 | return response 23 | 24 | def get_channels_pages(self): 25 | data = self.get('channels') 26 | items = ['channels'] 27 | 28 | if data is not None: 29 | for page in data.xpath('//div[@id="system"]//div[@class="pagination"]//a[@class=""]'): 30 | items.append(page.get('href')) 31 | 32 | return items 33 | 34 | def get_channels_page_links(self, url): 35 | data = self.get(url) 36 | items = [] 37 | 38 | if data is not None: 39 | for channel in data.xpath('//div[@id="system"]//table//a[contains(@href, "acestream")]'): 40 | items.append(channel.get('href')) 41 | 42 | return items 43 | 44 | def get_channels_links(self): 45 | pages = self.get_channels_pages() 46 | items = thread_pool(self.get_channels_page_links, pages) 47 | 48 | return items 49 | 50 | def get_channel_details(self, url): 51 | data = self.get(url) 52 | items = [] 53 | 54 | if data is None: 55 | return items 56 | 57 | try: 58 | root = data.xpath('//div[@id="system"]//table')[0] 59 | name = root.xpath('.//td[text()="Name"]//following-sibling::td[1]')[0] 60 | lang = root.xpath('.//td[text()="Language"]//following-sibling::td[1]')[0] 61 | rate = root.xpath('.//td[text()="Bitrate"]//following-sibling::td[1]')[0] 62 | strm = root.xpath('.//a[starts-with(@href, "acestream:")]') 63 | 64 | name = name.text_content().strip() 65 | lang = lang.text_content().strip() 66 | rate = rate.text_content().strip() 67 | 68 | name = self.parse_name(name) 69 | lang = 'Unknown' if lang == '' or lang.isdigit() else lang 70 | lang = 'Bulgarian' if lang == 'Bulgaria' else lang 71 | rate = 0 if rate == '' else int(rate.replace('Kbps', '')) 72 | 73 | channel = { 'name': name, 'language': lang.title() } 74 | stream = { 'rate': rate, 'language': lang[:3].upper(), 'url': None, 'hd_url': None, 'host': 'Acestream' } 75 | 76 | for link in strm: 77 | href = link.get('href') 78 | text = link.getparent().text_content() 79 | 80 | if 'HD' in text: 81 | stream['hd_url'] = href 82 | else: 83 | stream['url'] = href 84 | 85 | if stream['url'] is not None and lang != 'Unknown': 86 | items.append({ 'channel': channel, 'stream': stream }) 87 | except (IndexError, ValueError): 88 | pass 89 | 90 | return items 91 | 92 | def get_channels(self): 93 | links = self.get_channels_links() 94 | items = thread_pool(self.get_channel_details, links) 95 | 96 | return items 97 | 98 | def save_channels(self): 99 | data = self.get_channels() 100 | items = [] 101 | 102 | for item in data: 103 | stream = item['stream'] 104 | channel = self.data.set_single('channel', item['channel'], 'name') 105 | ch_id = "%s_%s" % (channel.id, stream['host'].lower()) 106 | 107 | stream.update({ 'channel': channel.id, 'ch_id': ch_id }) 108 | items.append(stream) 109 | 110 | self.data.set_multiple('stream', items, 'ch_id') 111 | 112 | def get_events_page(self): 113 | data = self.get() 114 | page = None 115 | 116 | if data is not None: 117 | link = data.xpath('//div[@id="system"]//a[starts-with(@href, "/live-football")]') 118 | page = link[0].get('href') if len(link) else None 119 | 120 | return page 121 | 122 | def get_events_page_links(self): 123 | link = self.get_events_page() 124 | data = self.get(url=link, ttl=120) 125 | items = [] 126 | 127 | if data is not None: 128 | for link in data.xpath('//div[@id="system"]//list[1]//a[contains(@href, "/streaming/")]'): 129 | items.append(link.get('href')) 130 | 131 | return items 132 | 133 | def get_event_channels(self, url): 134 | data = self.get(url=url, ttl=60) 135 | items = [] 136 | 137 | if data is None: 138 | return items 139 | 140 | try: 141 | root = data.xpath('//div[@id="system"]//table')[0] 142 | comp = root.xpath('.//td[text()="Competition"]//following-sibling::td[1]')[0] 143 | team = root.xpath('.//td[text()="Match"]//following-sibling::td[1]')[0] 144 | 145 | comp = comp.text_content().strip() 146 | team = team.text_content().strip().split('-') 147 | home = team[0].strip() 148 | away = team[1].strip() 149 | 150 | event = { 'competition': comp, 'home': home, 'away': away } 151 | chann = [] 152 | 153 | for link in data.xpath('//div[@id="system"]//a[contains(@href, "/channels/")]'): 154 | name = link.text_content() 155 | name = self.parse_name(name) 156 | 157 | chann.append(name) 158 | 159 | if chann: 160 | items.append({ 'event': event, 'channels': chann }) 161 | except (IndexError, ValueError): 162 | pass 163 | 164 | return items 165 | 166 | def get_events(self): 167 | links = self.get_events_page_links() 168 | items = thread_pool(self.get_event_channels, links) 169 | 170 | return items 171 | 172 | def save_events(self): 173 | fixtures = self.data.load_fixtures(today_only=True) 174 | events = self.get_events() 175 | items = [] 176 | 177 | for fixture in fixtures: 178 | channels = self.get_fixture_channels(events, fixture) 179 | streams = self.data.get_multiple('stream', 'channel', channels) 180 | 181 | for stream in streams: 182 | items.append({ 183 | 'fs_id': "%s_%s" % (fixture.id, stream.id), 184 | 'fixture': fixture.id, 185 | 'stream': stream 186 | }) 187 | 188 | self.data.set_multiple('event', items, 'fs_id') 189 | 190 | def get_fixture_channels(self, events, fixture): 191 | chann = [] 192 | items = [] 193 | 194 | for item in events: 195 | evnt = item['event'] 196 | comp = fuzz.ratio(fixture.competition.name, evnt['competition']) 197 | home = fuzz.ratio(fixture.home_team.name, evnt['home']) 198 | away = fuzz.ratio(fixture.away_team.name, evnt['away']) 199 | comb = (comp + home + away) / 3 200 | 201 | items.append({ 'ratio': comb, 'channels': item['channels'] }) 202 | 203 | if items: 204 | sort = sorted(items, key=itemgetter('ratio'), reverse=True)[0] 205 | 206 | if sort['ratio'] > 70: 207 | chann = self.data.get_multiple('channel', 'name', sort['channels']) 208 | chann = [c.id for c in chann] 209 | 210 | return chann 211 | 212 | def parse_name(self, name): 213 | find = ['Acestream', 'AceStream'] 214 | name = replace_all(name, find, '').strip() 215 | 216 | return name 217 | -------------------------------------------------------------------------------- /helpers/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import socket 4 | import psutil 5 | import subprocess 6 | import threading 7 | 8 | from multiprocessing.pool import ThreadPool 9 | from datetime import datetime, timedelta, timezone 10 | from dateutil import parser 11 | from psutil import Popen, process_iter, cpu_count 12 | from requests import get 13 | from playhouse.sqliteq import SqliteQueueDatabase 14 | 15 | 16 | def relative_path(filepath): 17 | root = os.path.dirname(os.path.realpath(__file__)) 18 | root = os.path.dirname(root) 19 | 20 | return os.path.join(root, filepath) 21 | 22 | 23 | def user_data_dir(): 24 | path = "%s/.config/kickoff-player" % os.path.expanduser('~') 25 | 26 | if not os.path.exists(path): 27 | os.makedirs(path) 28 | 29 | return path 30 | 31 | 32 | def database_dir(db_name): 33 | db_dir = os.path.join(user_data_dir(), db_name) 34 | 35 | if not os.path.exists(db_dir): 36 | open(db_dir, 'w+') 37 | 38 | return db_dir 39 | 40 | 41 | def database_connection(db_name): 42 | db_dir = database_dir(db_name) 43 | db_conn = SqliteQueueDatabase(db_dir) 44 | 45 | return db_conn 46 | 47 | 48 | def gmtime(date_format=None, round_time=False): 49 | date = time.gmtime() 50 | date = datetime(*date[:6]) 51 | date = date if not round_time else round_datetime(date) 52 | date = date if date_format is None else date.strftime(date_format) 53 | 54 | return date 55 | 56 | 57 | def tzone(zone_format): 58 | return time.strftime(zone_format) 59 | 60 | 61 | def now(date_format=None): 62 | date = datetime.now() 63 | date = date if date_format is None else date.strftime(date_format) 64 | 65 | return date 66 | 67 | 68 | def today(date_format=None): 69 | date = datetime.today().date() 70 | date = date if date_format is None else date.strftime(date_format) 71 | 72 | return date 73 | 74 | 75 | def yesterday(date_format=None): 76 | date = datetime.today() - timedelta(days=1) 77 | date = date if date_format is None else date.strftime(date_format) 78 | 79 | return date 80 | 81 | 82 | def format_date(date, localize=False, date_format='%Y-%m-%d %H:%M:%S.%f'): 83 | date = parse_date(date, localize) 84 | date = datetime.strftime(date, date_format) 85 | 86 | return date 87 | 88 | 89 | def parse_date(date, localize=False): 90 | date = date if not isinstance(date, str) else parser.parse(date) 91 | date = date if not localize else date.replace(tzinfo=timezone.utc).astimezone(tz=None) 92 | 93 | return date 94 | 95 | 96 | def round_datetime(date, round_to=10): 97 | secs = (date - date.min).seconds 98 | rndg = (secs + round_to / 2 ) // round_to * round_to 99 | date = date + timedelta(0, rndg - secs, - date.microsecond) 100 | 101 | return date 102 | 103 | 104 | def query_date_range(kwargs, date=datetime.now()): 105 | min_d = date - timedelta(hours=3) 106 | max_d = date + timedelta(**kwargs) 107 | dates = [min_d, max_d] 108 | 109 | return dates 110 | 111 | 112 | def batch(iterable, size=1, delimiter=None): 113 | length = len(iterable) 114 | result = [] 115 | 116 | for ndx in range(0, length, size): 117 | subset = iterable[ndx:min(ndx + size, length)] 118 | subset = subset if delimiter is None else delimiter.join(subset) 119 | 120 | result.append(subset) 121 | 122 | return result 123 | 124 | 125 | def in_thread(**kwargs): 126 | thread = threading.Thread(**kwargs) 127 | thread.start() 128 | 129 | 130 | def thread_pool(callback, args, flatten=True): 131 | pool = ThreadPool(processes=cpu_count()) 132 | data = pool.map(callback, args) 133 | 134 | pool.close() 135 | pool.join() 136 | 137 | if flatten: 138 | data = flatten_list(data) 139 | 140 | return data 141 | 142 | 143 | def run_command(args, **kwargs): 144 | kwargs = merge_dicts({ 'stdout': subprocess.PIPE }, kwargs) 145 | command = Popen(args, **kwargs) 146 | 147 | return command 148 | 149 | 150 | def active_processes(): 151 | return process_iter() 152 | 153 | 154 | def kill_proccess(name): 155 | try: 156 | for process in active_processes(): 157 | if name in process.name(): 158 | process.kill() 159 | except (psutil.AccessDenied, psutil.NoSuchProcess): 160 | pass 161 | 162 | 163 | def flatten_list(iterable): 164 | if iterable and isinstance(iterable[0], list): 165 | iterable = [item for sublist in iterable for item in sublist] 166 | 167 | if iterable is None: 168 | iterable = [] 169 | 170 | return iterable 171 | 172 | 173 | def merge_dicts(first, second): 174 | merged = first.copy() 175 | merged.update(second) 176 | 177 | return merged 178 | 179 | 180 | def merge_dict_keys(iterable, key_name): 181 | if iterable and isinstance(iterable, list): 182 | iterable = [item[key_name] for item in iterable if item is not None] 183 | iterable = flatten_list(iterable) 184 | 185 | if iterable is None: 186 | iterable = [] 187 | 188 | return iterable 189 | 190 | 191 | def search_dict_key(iterable, keys, default=None): 192 | try: 193 | keys = keys if isinstance(keys, list) else [keys] 194 | 195 | for key in keys: 196 | iterable = iterable[key] 197 | except(KeyError, TypeError): 198 | iterable = default 199 | 200 | return iterable 201 | 202 | 203 | def replace_all(string, find, replace): 204 | for item in list(find): 205 | string = string.replace(item, replace) 206 | 207 | return string 208 | 209 | 210 | def cached_request(url, cache, params=None, callback=None, **kwargs): 211 | url = parse_url(url, kwargs.get('base_url')) 212 | cache_key = cache_key_from_url(url, params, kwargs.get('cache_key')) 213 | response = cache.load(cache_key) 214 | 215 | if response is None: 216 | try: 217 | response = get(url, params=params) 218 | 219 | if response.status_code != 200: 220 | return None 221 | 222 | response = response.text if callback is None else callback(response.text) 223 | response = cache.save(cache_key, response, kwargs.get('ttl', 300)) 224 | except socket.error: 225 | return None 226 | 227 | response = response.json if kwargs.get('json') else response.text 228 | return response 229 | 230 | 231 | def cache_key_from_url(url, params=None, cache_key=None): 232 | key = url.split('://')[1] 233 | 234 | if cache_key is not None: 235 | key = "%s:%s" % (key, search_dict_key(params, cache_key)) 236 | 237 | key = replace_all(key, ['www.', '.html', '.php'], '') 238 | key = replace_all(key, ['/', '?', '-', '_', '.', ',', ' '], ':') 239 | key = key.strip('/').strip(':').lower() 240 | 241 | return key 242 | 243 | 244 | def parse_url(url, base_url=None): 245 | part = str(url).strip().split('://') 246 | host = part[0] if len(part) > 1 else 'http' 247 | path = part[-1].strip('/') 248 | 249 | if base_url is not None and base_url not in path: 250 | url = "%s://%s/%s" % (host, base_url.split('://')[0], path) 251 | else: 252 | url = "%s://%s" % (host, path) 253 | 254 | return url 255 | 256 | 257 | def download_file(url, path, stream=False): 258 | try: 259 | response = get(url, stream=stream) 260 | path = os.path.normpath(path) 261 | folder = os.path.dirname(path) 262 | 263 | if response.status_code != 200: 264 | return None 265 | 266 | if not os.path.exists(folder): 267 | os.makedirs(folder) 268 | 269 | with open(path, 'wb') as filename: 270 | filename.write(response.content) 271 | except socket.error: 272 | return None 273 | 274 | return path 275 | -------------------------------------------------------------------------------- /icons/render-bitmaps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Legal Stuff: 4 | # 5 | # This file is part of the Paper Icon Theme and is free software; you can redistribute it and/or modify it under 6 | # the terms of the GNU Lesser General Public License as published by the Free Software 7 | # Foundation; version 3. 8 | # 9 | # This file is part of the Paper Icon Theme and is distributed in the hope that it will be useful, but WITHOUT 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | # details. 13 | # 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, see 16 | # 17 | # 18 | # Thanks to the GNOME icon developers for the original version of this script 19 | 20 | import os 21 | import sys 22 | import xml.sax 23 | import subprocess 24 | import argparse 25 | 26 | INKSCAPE = '/usr/bin/inkscape' 27 | OPTIPNG = '/usr/bin/optipng' 28 | MAINDIR = 'hicolor' 29 | SRC = 'src' 30 | SIZES = { 31 | 16: [16, 96], 32 | 24: [24, 96], 33 | 32: [32, 96], 34 | 48: [48, 96], 35 | 64: [48, 128], 36 | 96: [512, 18], 37 | 128: [512, 24], 38 | 256: [512, 48], 39 | 512: [512, 96], 40 | 1024: [512, 192] 41 | } 42 | 43 | inkscape_process = None 44 | 45 | def main(args, SRC): 46 | 47 | def optimize_png(png_file): 48 | if os.path.exists(OPTIPNG): 49 | process = subprocess.Popen([OPTIPNG, '-quiet', '-o7', png_file]) 50 | process.wait() 51 | 52 | def wait_for_prompt(process, command=None): 53 | if command is not None: 54 | process.stdin.write((command+'\n').encode('utf-8')) 55 | 56 | # This is kinda ugly ... 57 | # Wait for just a '>', or '\n>' if some other char appearead first 58 | output = process.stdout.read(1) 59 | if output == b'>': 60 | return 61 | 62 | output += process.stdout.read(1) 63 | while output != b'\n>': 64 | output += process.stdout.read(1) 65 | output = output[1:] 66 | 67 | def start_inkscape(): 68 | process = subprocess.Popen([INKSCAPE, '--shell'], bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 69 | wait_for_prompt(process) 70 | return process 71 | 72 | def inkscape_render_rect(icon_file, rect, dpi, output_file): 73 | global inkscape_process 74 | if inkscape_process is None: 75 | inkscape_process = start_inkscape() 76 | 77 | cmd = [icon_file, 78 | '--export-dpi', str(dpi), 79 | '-i', rect, 80 | '-e', output_file] 81 | wait_for_prompt(inkscape_process, ' '.join(cmd)) 82 | optimize_png(output_file) 83 | 84 | class ContentHandler(xml.sax.ContentHandler): 85 | ROOT = 0 86 | SVG = 1 87 | LAYER = 2 88 | OTHER = 3 89 | TEXT = 4 90 | def __init__(self, path, force=False, filter=None): 91 | self.stack = [self.ROOT] 92 | self.inside = [self.ROOT] 93 | self.path = path 94 | self.rects = [] 95 | self.state = self.ROOT 96 | self.chars = "" 97 | self.force = force 98 | self.filter = filter 99 | 100 | def endDocument(self): 101 | pass 102 | 103 | def startElement(self, name, attrs): 104 | if self.inside[-1] == self.ROOT: 105 | if name == "svg": 106 | self.stack.append(self.SVG) 107 | self.inside.append(self.SVG) 108 | return 109 | elif self.inside[-1] == self.SVG: 110 | if (name == "g" and ('inkscape:groupmode' in attrs) and ('inkscape:label' in attrs) 111 | and attrs['inkscape:groupmode'] == 'layer' and attrs['inkscape:label'].startswith('Baseplate')): 112 | self.stack.append(self.LAYER) 113 | self.inside.append(self.LAYER) 114 | self.context = None 115 | self.icon_name = None 116 | self.rects = [] 117 | return 118 | elif self.inside[-1] == self.LAYER: 119 | if name == "text" and ('inkscape:label' in attrs) and attrs['inkscape:label'] == 'context': 120 | self.stack.append(self.TEXT) 121 | self.inside.append(self.TEXT) 122 | self.text='context' 123 | self.chars = "" 124 | return 125 | elif name == "text" and ('inkscape:label' in attrs) and attrs['inkscape:label'] == 'icon-name': 126 | self.stack.append(self.TEXT) 127 | self.inside.append(self.TEXT) 128 | self.text='icon-name' 129 | self.chars = "" 130 | return 131 | elif name == "rect": 132 | self.rects.append(attrs) 133 | 134 | self.stack.append(self.OTHER) 135 | 136 | 137 | def endElement(self, name): 138 | stacked = self.stack.pop() 139 | if self.inside[-1] == stacked: 140 | self.inside.pop() 141 | 142 | if stacked == self.TEXT and self.text is not None: 143 | assert self.text in ['context', 'icon-name'] 144 | if self.text == 'context': 145 | self.context = self.chars 146 | elif self.text == 'icon-name': 147 | self.icon_name = self.chars 148 | self.text = None 149 | elif stacked == self.LAYER: 150 | assert self.icon_name 151 | assert self.context 152 | 153 | if self.filter is not None and not self.icon_name in self.filter: 154 | return 155 | 156 | print (self.context, self.icon_name) 157 | 158 | rects = {} 159 | 160 | for rect in self.rects: 161 | width = int(rect['width']) 162 | rects[width] = rect 163 | 164 | for size, value in SIZES.items(): 165 | src, dpi = value 166 | rect = rects[src] 167 | id = rect['id'] 168 | size_str = "%sx%s" % (size, size) 169 | 170 | dir = os.path.join(MAINDIR, size_str, self.context) 171 | outfile = os.path.join(dir, self.icon_name+'.png') 172 | if not os.path.exists(dir): 173 | os.makedirs(dir) 174 | # Do a time based check! 175 | if self.force or not os.path.exists(outfile): 176 | inkscape_render_rect(self.path, id, dpi, outfile) 177 | sys.stdout.write('.') 178 | else: 179 | stat_in = os.stat(self.path) 180 | stat_out = os.stat(outfile) 181 | if stat_in.st_mtime > stat_out.st_mtime: 182 | inkscape_render_rect(self.path, id, dpi, outfile) 183 | sys.stdout.write('.') 184 | else: 185 | sys.stdout.write('-') 186 | sys.stdout.flush() 187 | 188 | sys.stdout.write('\n') 189 | sys.stdout.flush() 190 | 191 | def characters(self, chars): 192 | self.chars += chars.strip() 193 | 194 | 195 | if not args.svg: 196 | if not os.path.exists(MAINDIR): 197 | os.mkdir(MAINDIR) 198 | print ('') 199 | print ('Rendering from SVGs in', SRC) 200 | print ('') 201 | for file in os.listdir(SRC): 202 | if file[-4:] == '.svg': 203 | file = os.path.join(SRC, file) 204 | handler = ContentHandler(file) 205 | xml.sax.parse(open(file), handler) 206 | print ('') 207 | else: 208 | file = os.path.join(SRC, args.svg + '.svg') 209 | 210 | if os.path.exists(os.path.join(file)): 211 | handler = ContentHandler(file, True, filter=args.filter) 212 | xml.sax.parse(open(file), handler) 213 | else: 214 | # icon not in this directory, try the next one 215 | pass 216 | 217 | parser = argparse.ArgumentParser(description='Render icons from SVG to PNG') 218 | 219 | parser.add_argument('svg', type=str, nargs='?', metavar='SVG', help="Optional SVG names (without extensions) to render. If not given, render all icons") 220 | parser.add_argument('filter', type=str, nargs='?', metavar='FILTER', help="Optional filter for the SVG file") 221 | 222 | args = parser.parse_args() 223 | SRC = os.path.join('.', SRC) 224 | main(args, SRC) 225 | -------------------------------------------------------------------------------- /handlers/match.py: -------------------------------------------------------------------------------- 1 | import gi 2 | import time 3 | 4 | gi.require_version('Gtk', '3.0') 5 | gi.require_version('GLib', '2.0') 6 | 7 | from gi.repository import Gtk, GLib 8 | from helpers.utils import now, in_thread, relative_path 9 | from helpers.gtk import remove_widget_children, set_scroll_position, run_generator 10 | 11 | from widgets.matchbox import MatchBox, MatchTeamsBox, MatchStreamBox 12 | from widgets.filterbox import FilterBox 13 | 14 | 15 | class MatchHandler(object): 16 | 17 | def __init__(self, app): 18 | self.app = app 19 | self.stack = app.matches_stack 20 | self.filter = None 21 | 22 | self.matches = Gtk.Builder() 23 | self.matches.add_from_file(relative_path('ui/matches.ui')) 24 | self.matches.connect_signals(self) 25 | 26 | self.matches_box = self.matches.get_object('box_matches') 27 | self.stack.add_named(self.matches_box, 'matches_container') 28 | 29 | self.matches_filters = self.matches.get_object('list_box_matches_filters') 30 | self.matches_list = self.matches.get_object('flow_box_matches_list') 31 | self.matches_list.set_filter_func(self.on_matches_list_row_changed) 32 | 33 | self.match = Gtk.Builder() 34 | self.match.add_from_file(relative_path('ui/match.ui')) 35 | self.match.connect_signals(self) 36 | 37 | self.match_box = self.match.get_object('box_match') 38 | self.stack.add_named(self.match_box, 'match_container') 39 | 40 | self.match_teams = self.match.get_object('box_match_teams') 41 | self.match_streams = self.match.get_object('list_box_match_streams') 42 | 43 | GLib.idle_add(self.do_initial_setup) 44 | GLib.idle_add(self.do_matches_widgets) 45 | 46 | GLib.timeout_add(5 * 60000, self.update_live_data) 47 | 48 | @property 49 | 50 | def visible(self): 51 | return self.app.get_stack_visible_child() == self.stack 52 | 53 | @property 54 | 55 | def in_match(self): 56 | return self.visible and self.stack.get_visible_child() == self.match_box 57 | 58 | @property 59 | 60 | def live_fixtures(self): 61 | fixtures = self.app.data.load_fixtures(current=True, today_only=True) 62 | return fixtures and fixtures[0].date <= now() 63 | 64 | def initial_setup(self): 65 | if not self.app.data.load_competitions(): 66 | self.update_competitions_data() 67 | self.update_teams_data() 68 | self.update_matches_data() 69 | 70 | def do_initial_setup(self): 71 | in_thread(target=self.initial_setup) 72 | 73 | def do_matches_widgets(self): 74 | if not self.matches_filters.get_children(): 75 | run_generator(self.do_matches_filters) 76 | 77 | if not self.matches_list.get_children(): 78 | run_generator(self.do_matches_list) 79 | 80 | def update_matches_widgets(self): 81 | if self.matches_filters.get_children(): 82 | run_generator(self.update_matches_filters) 83 | 84 | if self.matches_list.get_children(): 85 | run_generator(self.update_matches_list) 86 | 87 | def update_events_data(self): 88 | self.app.scores_api.save_matches() 89 | self.app.streams_api.save_events() 90 | self.app.scores_api.save_live() 91 | 92 | def update_competitions_data(self): 93 | if not self.app.data.load_competitions(): 94 | self.app.scores_api.save_competitions() 95 | 96 | def update_teams_data(self): 97 | if not self.app.data.load_teams(): 98 | self.app.scores_api.save_teams() 99 | self.app.scores_api.save_crests() 100 | 101 | def update_matches_data(self): 102 | in_thread(target=self.do_update_matches_data) 103 | 104 | def do_update_matches_data(self): 105 | GLib.idle_add(self.app.toggle_reload, False) 106 | 107 | self.update_events_data() 108 | time.sleep(5) 109 | 110 | GLib.idle_add(self.do_matches_widgets) 111 | GLib.idle_add(self.update_matches_widgets) 112 | 113 | if self.in_match: 114 | GLib.idle_add(self.update_match_details) 115 | 116 | GLib.idle_add(self.app.toggle_reload, True) 117 | 118 | def update_live_data(self): 119 | if self.live_fixtures: 120 | in_thread(target=self.do_update_live_data) 121 | 122 | return True 123 | 124 | def do_update_live_data(self): 125 | self.app.streams_api.save_events() 126 | self.app.scores_api.save_live() 127 | time.sleep(5) 128 | 129 | GLib.idle_add(self.update_matches_widgets) 130 | 131 | def do_matches_filters(self): 132 | filters = self.app.data.load_matches_filters() 133 | remove_widget_children(self.matches_filters) 134 | 135 | for filter_name in filters: 136 | self.do_filter_item(filter_name) 137 | yield True 138 | 139 | run_generator(self.update_matches_filters) 140 | 141 | def do_filter_item(self, filter_name): 142 | filterbox = FilterBox(filter_name=filter_name, filter_all='All Competitions') 143 | self.matches_filters.add(filterbox) 144 | 145 | if not self.matches_filters.get_selected_row(): 146 | self.matches_filters.select_row(filterbox) 147 | 148 | def update_matches_filters(self): 149 | active = self.app.data.load_matches_filters(True) 150 | filters = self.app.data.load_matches_filters() 151 | 152 | for item in self.matches_filters.get_children(): 153 | if item.filter_name not in filters: 154 | item.destroy() 155 | elif item.filter_name not in active: 156 | item.set_sensitive(False) 157 | else: 158 | item.set_sensitive(True) 159 | 160 | yield True 161 | 162 | def do_matches_list(self): 163 | fixtures = self.app.data.load_fixtures(True) 164 | remove_widget_children(self.matches_list) 165 | 166 | for fixture in fixtures: 167 | self.do_match_item(fixture) 168 | yield True 169 | 170 | def do_match_item(self, fixture): 171 | matchbox = MatchBox(fixture=fixture, callback=self.on_match_activated) 172 | self.matches_list.add(matchbox) 173 | 174 | def update_matches_list(self): 175 | fixtures = self.app.data.load_fixtures(True, True) 176 | 177 | for item in self.matches_list.get_children(): 178 | if item.fixture.id in fixtures: 179 | updated = self.app.data.get_single('fixture', { 'id': item.fixture.id }) 180 | item.set_property('fixture', updated) 181 | else: 182 | item.destroy() 183 | 184 | yield True 185 | 186 | def do_match_details(self, fixture): 187 | remove_widget_children(self.match_teams) 188 | remove_widget_children(self.match_streams) 189 | 190 | teambox = MatchTeamsBox(fixture=fixture) 191 | self.match_teams.pack_start(teambox, True, True, 0) 192 | 193 | if not fixture.events: 194 | streambox = MatchStreamBox(stream=None, callback=None) 195 | self.match_streams.add(streambox) 196 | 197 | for event in fixture.events: 198 | streambox = MatchStreamBox(stream=event.stream, callback=self.app.player.open_stream) 199 | self.match_streams.add(streambox) 200 | 201 | def update_match_details(self): 202 | fixture = self.match_teams.get_children()[0].fixture.id 203 | fixture = self.app.data.get_single('fixture', { 'id': fixture }) 204 | 205 | self.do_match_details(fixture) 206 | 207 | def on_header_button_reload_clicked(self, _widget): 208 | if self.visible: 209 | self.update_matches_data() 210 | 211 | def on_header_button_back_clicked(self, widget): 212 | self.stack.set_visible_child(self.matches_box) 213 | widget.hide() 214 | 215 | def on_stack_main_visible_child_notify(self, _widget, _params): 216 | if self.in_match: 217 | self.app.header_back.show() 218 | else: 219 | self.app.header_back.hide() 220 | 221 | def on_match_activated(self, fixture): 222 | self.stack.set_visible_child(self.match_box) 223 | self.do_match_details(fixture) 224 | 225 | self.app.header_back.show() 226 | 227 | def on_matches_list_row_changed(self, item): 228 | return self.filter is None or item.filter_name == self.filter 229 | 230 | def on_list_box_matches_filters_row_activated(self, _listbox, item): 231 | self.filter = item.filter_value 232 | 233 | self.matches_list.invalidate_filter() 234 | set_scroll_position(self.matches_list, 0, 'vertical', self.app.window) 235 | -------------------------------------------------------------------------------- /handlers/player.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gi 3 | import dbus 4 | 5 | gi.require_version('Gtk', '3.0') 6 | gi.require_version('Gdk', '3.0') 7 | gi.require_version('GLib', '2.0') 8 | 9 | from gi.repository import Gtk, Gdk, GLib 10 | from handlers.stream import StreamHandler 11 | from helpers.gtk import toggle_cursor 12 | from helpers.utils import relative_path 13 | 14 | display = os.environ.get('WAYLAND_DISPLAY') 15 | session = os.environ.get('XDG_SESSION_TYPE') 16 | 17 | if 'wayland' in (display or session): 18 | from widgets.gstbox import GstBox as VideoBox 19 | else: 20 | try: 21 | from widgets.mpvbox import MpvBox as VideoBox 22 | except (ModuleNotFoundError, ImportError): 23 | try: 24 | from widgets.vlcbox import VlcBox as VideoBox 25 | except (ModuleNotFoundError, ImportError): 26 | from widgets.gstbox import GstBox as VideoBox 27 | 28 | 29 | class PlayerHandler(object): 30 | 31 | def __init__(self, app): 32 | self.stream = StreamHandler(self) 33 | self.app = app 34 | self.stack = app.player_stack 35 | 36 | self.sesbus = dbus.SessionBus() 37 | self.isaver = self.get_isaver() 38 | self.cookie = None 39 | 40 | self.url = None 41 | self.xid = None 42 | 43 | self.loading = False 44 | self.cstream = None 45 | 46 | self.is_fullscreen = False 47 | self.toolbar_stick = True 48 | 49 | self.player = Gtk.Builder() 50 | self.player.add_from_file(relative_path('ui/player.ui')) 51 | self.player.connect_signals(self) 52 | 53 | self.overlay = self.player.get_object('overlay_player') 54 | self.stack.add_named(self.overlay, 'player_video') 55 | 56 | self.eventbox = self.player.get_object('eventbox_player') 57 | self.eventbox.connect('button-press-event', self.on_eventbox_button_press_event) 58 | self.eventbox.connect('motion-notify-event', self.on_eventbox_motion_notify_event) 59 | 60 | self.playbin = VideoBox(callback=self.update_status) 61 | self.eventbox.add(self.playbin) 62 | 63 | self.status = self.player.get_object('label_player_status') 64 | self.status.set_text('Not playing') 65 | 66 | self.toolbar = self.player.get_object('toolbox_player') 67 | self.volume_button = self.player.get_object('button_volume') 68 | self.full_button = self.player.get_object('button_fullscreen') 69 | self.restore_button = self.player.get_object('button_unfullscreen') 70 | self.play_button = self.player.get_object('button_play') 71 | self.pause_button = self.player.get_object('button_pause') 72 | self.stop_button = self.player.get_object('button_stop') 73 | self.toolbar_event = GLib.timeout_add(3000, self.toggle_toolbar, True) 74 | 75 | @property 76 | 77 | def visible(self): 78 | return self.app.get_stack_visible_child() == self.stack 79 | 80 | @property 81 | 82 | def actionable(self): 83 | return self.url is not None 84 | 85 | def open_stream(self, stream): 86 | self.cstream = stream 87 | self.stream.open(stream.url) 88 | 89 | self.toggle_controls() 90 | self.app.set_stack_visible_child(self.stack) 91 | 92 | def reload_stream(self): 93 | GLib.idle_add(self.app.toggle_reload, False) 94 | 95 | self.close_stream() 96 | self.open_stream(self.cstream) 97 | 98 | GLib.idle_add(self.app.toggle_reload, True) 99 | 100 | def close_stream(self): 101 | self.stop() 102 | self.stream.close() 103 | 104 | def open(self, url): 105 | self.url = url 106 | self.playbin.open(self.url) 107 | self.play() 108 | 109 | def close(self): 110 | self.url = None 111 | self.close_stream() 112 | 113 | def play(self): 114 | self.playbin.play() 115 | 116 | def pause(self): 117 | self.playbin.pause() 118 | 119 | def stop(self): 120 | self.playbin.stop() 121 | 122 | def set_volume(self, volume): 123 | self.playbin.set_volume(volume) 124 | 125 | def update_status(self, status, text=None): 126 | labels = { 127 | 'PLAYING': 'Playing', 128 | 'PAUSED': 'Paused', 129 | 'STOPPED': 'Stopped', 130 | 'BUFFER': 'Buffering' 131 | } 132 | 133 | if status in ['PLAYING', 'BUFFER']: 134 | self.toggle_buttons(True) 135 | else: 136 | self.toggle_buttons(False) 137 | 138 | status = labels.get(status, status) 139 | status = status if text is None else "%s %s" % (status, text) 140 | self.status.set_text(status) 141 | 142 | def toggle_buttons(self, playing=True): 143 | if playing: 144 | self.pause_button.set_sensitive(True) 145 | self.stop_button.set_sensitive(True) 146 | self.play_button.set_sensitive(False) 147 | else: 148 | self.pause_button.set_sensitive(False) 149 | self.stop_button.set_sensitive(False) 150 | self.play_button.set_sensitive(True) 151 | 152 | def toggle_fullscreen(self): 153 | if not self.visible: 154 | return 155 | 156 | if self.is_fullscreen: 157 | self.restore_button.hide() 158 | self.full_button.show() 159 | self.app.window.unfullscreen() 160 | self.uninhibit_ssaver() 161 | 162 | self.is_fullscreen = False 163 | else: 164 | self.full_button.hide() 165 | self.restore_button.show() 166 | self.app.window.fullscreen() 167 | self.inhibit_ssaver() 168 | 169 | self.is_fullscreen = True 170 | 171 | def toggle_toolbar(self, timer=True): 172 | visible = self.toolbar.is_visible() 173 | 174 | if not self.toolbar_stick and self.actionable: 175 | if timer and visible: 176 | self.toggle_controls(True) 177 | 178 | if not timer and not visible: 179 | self.toggle_controls(False) 180 | 181 | return timer 182 | 183 | def toggle_controls(self, hide=False): 184 | if hide: 185 | self.toolbar.hide() 186 | toggle_cursor(self.overlay, True) 187 | else: 188 | self.toolbar.show() 189 | toggle_cursor(self.overlay, False) 190 | 191 | def get_isaver(self): 192 | try: 193 | ssaver = self.sesbus.get_object('org.freedesktop.ScreenSaver', '/ScreenSaver') 194 | isaver = dbus.Interface(ssaver, dbus_interface='org.freedesktop.ScreenSaver') 195 | except dbus.exceptions.DBusException: 196 | isaver = None 197 | 198 | return isaver 199 | 200 | def inhibit_ssaver(self): 201 | if self.isaver is not None and self.cookie is None: 202 | self.cookie = self.isaver.Inhibit('kickoff-player', 'Playing video') 203 | 204 | def uninhibit_ssaver(self): 205 | if self.isaver is not None and self.cookie is not None: 206 | self.isaver.UnInhibit(self.cookie) 207 | self.cookie = None 208 | 209 | def on_stream_activated(self, _widget, stream): 210 | self.open_stream(stream) 211 | 212 | def on_button_play_clicked(self, _event): 213 | if not self.loading and self.actionable: 214 | self.play() 215 | self.set_volume(self.volume_button.get_value()) 216 | 217 | def on_button_pause_clicked(self, _event): 218 | if not self.loading and self.actionable: 219 | self.pause() 220 | 221 | def on_button_stop_clicked(self, _event): 222 | if not self.loading and self.actionable: 223 | self.stop() 224 | 225 | def on_button_volume_value_changed(self, _event, value): 226 | if not self.loading and self.actionable: 227 | self.set_volume(value) 228 | 229 | def on_window_main_key_release_event(self, _widget, event): 230 | if self.visible and Gdk.keyval_name(event.keyval) == 'F11': 231 | self.toggle_fullscreen() 232 | 233 | def on_header_button_reload_clicked(self, _event): 234 | if self.visible and self.cstream is not None: 235 | self.reload_stream() 236 | 237 | def on_button_fullscreen_clicked(self, _event): 238 | self.toggle_fullscreen() 239 | 240 | def on_button_unfullscreen_clicked(self, _event): 241 | self.toggle_fullscreen() 242 | 243 | def on_eventbox_button_press_event(self, _widget, event): 244 | if event.type == Gdk.EventType._2BUTTON_PRESS: 245 | self.toggle_fullscreen() 246 | 247 | def on_eventbox_motion_notify_event(self, _widget, _event): 248 | self.toolbar_stick = False 249 | 250 | GLib.source_remove(self.toolbar_event) 251 | GLib.idle_add(self.toggle_toolbar, False) 252 | 253 | self.toolbar_event = GLib.timeout_add(3000, self.toggle_toolbar, True) 254 | 255 | def on_toolbar_player_enter_notify_event(self, _widget, _event): 256 | self.toolbar_stick = True 257 | GLib.idle_add(self.toggle_toolbar, False) 258 | -------------------------------------------------------------------------------- /ui/match.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 980 8 | 585 9 | True 10 | False 11 | vertical 12 | 13 | 14 | True 15 | True 16 | 17 | 18 | True 19 | False 20 | 21 | 22 | 800 23 | True 24 | False 25 | center 26 | 40 27 | 40 28 | 40 29 | 40 30 | vertical 31 | 30 32 | 33 | 34 | True 35 | False 36 | vertical 37 | 5 38 | 39 | 40 | True 41 | False 42 | start 43 | Match Details 44 | 47 | 48 | 49 | False 50 | True 51 | 0 52 | 53 | 54 | 55 | 56 | True 57 | False 58 | False 59 | start 60 | Match updates are performed every 5 minutes. Use the refresh button to update manually. 61 | 62 | 63 | False 64 | True 65 | 1 66 | 67 | 68 | 69 | 70 | False 71 | True 72 | 0 73 | 74 | 75 | 76 | 77 | True 78 | False 79 | start 80 | True 81 | 82 | 83 | 84 | 88 | 89 | 90 | False 91 | True 92 | 1 93 | 94 | 95 | 96 | 97 | True 98 | False 99 | vertical 100 | 5 101 | 102 | 103 | True 104 | False 105 | start 106 | P2P Streams 107 | 110 | 111 | 112 | False 113 | True 114 | 0 115 | 116 | 117 | 118 | 119 | True 120 | False 121 | False 122 | start 123 | If there are no streams for this event, check back later. Usually 30 minutes before the event starts. 124 | 125 | 126 | False 127 | True 128 | 1 129 | 130 | 131 | 132 | 133 | False 134 | True 135 | 2 136 | 137 | 138 | 139 | 140 | True 141 | False 142 | none 143 | 146 | 147 | 148 | False 149 | True 150 | 3 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | True 160 | True 161 | 1 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /widgets/matchbox.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | 5 | from gi.repository import Gtk, GObject, Pango 6 | from widgets.streambox import StreamBox 7 | from helpers.gtk import add_widget_class, remove_widget_class 8 | from helpers.gtk import image_from_path, remove_widget_children 9 | 10 | 11 | class MatchBox(Gtk.FlowBoxChild): 12 | 13 | __gtype_name__ = 'MatchBox' 14 | 15 | fixture = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 16 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 17 | filter_name = GObject.property(type=str, flags=GObject.PARAM_READWRITE) 18 | 19 | def __init__(self, *args, **kwargs): 20 | Gtk.FlowBoxChild.__init__(self, *args, **kwargs) 21 | 22 | self.fixture = self.get_property('fixture') 23 | self.callback = self.get_property('callback') 24 | self.filter_name = self.get_property('filter-name') 25 | 26 | self.outer_box = self.do_outer_box() 27 | self.teams_box = self.do_teams_box() 28 | self.details_box = self.do_details_box() 29 | 30 | self.set_valign(Gtk.Align.START) 31 | self.connect('realize', self.on_fixture_updated) 32 | self.connect('realize', self.on_realize) 33 | self.connect('notify::fixture', self.on_fixture_updated) 34 | 35 | add_widget_class(self, 'event-item') 36 | 37 | self.show() 38 | 39 | def on_realize(self, *_args): 40 | self.update_outer_box() 41 | 42 | self.outer_box.pack_start(self.teams_box, True, True, 0) 43 | self.outer_box.pack_start(self.details_box, True, True, 1) 44 | 45 | def on_fixture_updated(self, *_args): 46 | self.filter_name = getattr(self.fixture, 'competition').name 47 | self.update_teams_box() 48 | self.update_details_box() 49 | 50 | def do_outer_box(self): 51 | box = Gtk.Box() 52 | box.set_orientation(Gtk.Orientation.VERTICAL) 53 | 54 | return box 55 | 56 | def update_outer_box(self): 57 | self.outer_box.show() 58 | self.add(self.outer_box) 59 | 60 | def do_teams_box(self): 61 | return MatchTeamsBox(fixture=self.fixture) 62 | 63 | def update_teams_box(self): 64 | self.teams_box.set_property('fixture', self.fixture) 65 | 66 | def do_details_box(self): 67 | return MatchDetailsBox(fixture=self.fixture, callback=self.callback) 68 | 69 | def update_details_box(self): 70 | self.details_box.set_property('fixture', self.fixture) 71 | 72 | 73 | class MatchTeamsBox(Gtk.Box): 74 | 75 | __gtype_name__ = 'MatchTeamsBox' 76 | 77 | fixture = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 78 | 79 | def __init__(self, *args, **kwargs): 80 | Gtk.Box.__init__(self, *args, **kwargs) 81 | 82 | self.fixture = self.get_property('fixture') 83 | 84 | self.home_crest = self.do_team_crest() 85 | self.home_name = self.do_team_name() 86 | self.home_box = self.do_team_box('home') 87 | 88 | self.away_crest = self.do_team_crest() 89 | self.away_name = self.do_team_name() 90 | self.away_box = self.do_team_box('away') 91 | 92 | self.score = self.do_score_label() 93 | self.score_box = self.do_score_box() 94 | 95 | self.set_orientation(Gtk.Orientation.HORIZONTAL) 96 | self.set_homogeneous(True) 97 | 98 | self.connect('realize', self.on_fixture_updated) 99 | self.connect('realize', self.on_realize) 100 | self.connect('notify::fixture', self.on_fixture_updated) 101 | 102 | self.show() 103 | 104 | def on_realize(self, *_args): 105 | self.pack_start(self.home_box, True, True, 0) 106 | self.pack_start(self.score_box, True, True, 1) 107 | self.pack_start(self.away_box, True, True, 2) 108 | 109 | def on_fixture_updated(self, *_args): 110 | self.update_team_crest('home') 111 | self.update_team_name('home') 112 | self.update_team_crest('away') 113 | self.update_team_name('away') 114 | self.update_score_label() 115 | 116 | def do_column_box(self): 117 | box = Gtk.Box() 118 | box.set_orientation(Gtk.Orientation.VERTICAL) 119 | box.set_spacing(10) 120 | box.set_margin_top(10) 121 | box.set_margin_bottom(10) 122 | box.set_margin_left(10) 123 | box.set_margin_right(10) 124 | box.show() 125 | 126 | return box 127 | 128 | def do_team_box(self, team): 129 | crest = getattr(self, "%s_crest" % team) 130 | tname = getattr(self, "%s_name" % team) 131 | 132 | box = self.do_column_box() 133 | box.pack_start(crest, True, True, 0) 134 | box.pack_start(tname, True, True, 1) 135 | box.show() 136 | 137 | return box 138 | 139 | def do_team_name(self): 140 | label = Gtk.Label('Team Name') 141 | label.set_max_width_chars(25) 142 | label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) 143 | label.set_margin_left(5) 144 | label.set_margin_right(5) 145 | 146 | add_widget_class(label, 'team-name') 147 | 148 | return label 149 | 150 | def update_team_name(self, team): 151 | tname = getattr(self.fixture, "%s_team" % team).name 152 | label = getattr(self, "%s_name" % team) 153 | label.set_label(tname) 154 | label.set_tooltip_text(tname) 155 | label.show() 156 | 157 | def do_team_crest(self): 158 | image = image_from_path(path='images/team-emblem.svg') 159 | image.set_halign(Gtk.Align.CENTER) 160 | image.set_valign(Gtk.Align.CENTER) 161 | 162 | return image 163 | 164 | def update_team_crest(self, team): 165 | crest = getattr(self.fixture, "%s_team" % team).crest 166 | image = getattr(self, "%s_crest" % team) 167 | image_from_path(path=crest, image=image) 168 | image.show() 169 | 170 | def do_score_box(self): 171 | box = self.do_column_box() 172 | box.pack_start(self.score, True, True, 0) 173 | box.show() 174 | 175 | return box 176 | 177 | def do_score_label(self): 178 | label = Gtk.Label('None') 179 | label.set_justify(Gtk.Justification.CENTER) 180 | label.set_halign(Gtk.Align.CENTER) 181 | label.set_valign(Gtk.Align.CENTER) 182 | 183 | add_widget_class(label, 'event-date') 184 | 185 | return label 186 | 187 | def update_score_label(self): 188 | if getattr(self.fixture, 'past'): 189 | add_widget_class(self.score, 'event-score') 190 | else: 191 | remove_widget_class(self.score, 'event-score') 192 | 193 | if getattr(self.fixture, 'today'): 194 | add_widget_class(self.score, 'event-today') 195 | else: 196 | remove_widget_class(self.score, 'event-today') 197 | 198 | if getattr(self.fixture, 'live'): 199 | add_widget_class(self.score, 'event-live') 200 | else: 201 | remove_widget_class(self.score, 'event-live') 202 | 203 | score = getattr(self.fixture, 'score') 204 | self.score.set_label(score) 205 | self.score.show() 206 | 207 | 208 | class MatchDetailsBox(Gtk.Box): 209 | 210 | __gtype_name__ = 'MatchDetailsBox' 211 | 212 | fixture = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 213 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 214 | 215 | def __init__(self, *args, **kwargs): 216 | Gtk.Box.__init__(self, *args, **kwargs) 217 | 218 | self.fixture = self.get_property('fixture') 219 | self.callback = self.get_property('callback') 220 | 221 | self.event_count = 0 222 | self.count_label = self.do_count_label() 223 | self.available_label = self.do_available_label() 224 | self.more_button = self.do_more_button() 225 | 226 | self.set_orientation(Gtk.Orientation.HORIZONTAL) 227 | self.connect('realize', self.on_fixture_updated) 228 | self.connect('realize', self.on_realize) 229 | self.connect('notify::fixture', self.on_fixture_updated) 230 | 231 | add_widget_class(self, 'event-item-streams') 232 | 233 | self.show() 234 | 235 | def on_realize(self, *_args): 236 | self.pack_start(self.count_label, False, False, 0) 237 | self.pack_start(self.available_label, False, False, 1) 238 | self.pack_end(self.more_button, False, False, 2) 239 | 240 | def on_fixture_updated(self, *_args): 241 | self.event_count = getattr(self.fixture, 'events').count() 242 | self.update_count_label() 243 | self.update_more_button() 244 | 245 | def do_available_label(self): 246 | label = Gtk.Label('Streams available') 247 | label.set_halign(Gtk.Align.START) 248 | label.show() 249 | 250 | return label 251 | 252 | def do_count_label(self): 253 | label = Gtk.Label('0') 254 | label.set_margin_right(10) 255 | 256 | add_widget_class(label, 'event-item-counter') 257 | 258 | return label 259 | 260 | def update_count_label(self): 261 | count = str(self.event_count) 262 | self.count_label.set_label(count) 263 | 264 | if self.event_count == 0: 265 | add_widget_class(self.count_label, 'no-streams') 266 | else: 267 | remove_widget_class(self.count_label, 'no-streams') 268 | 269 | self.count_label.show() 270 | 271 | def do_more_button(self): 272 | kwargs = { 'icon_name': 'media-playback-start-symbolic', 'size': Gtk.IconSize.BUTTON } 273 | button = Gtk.Button.new_from_icon_name(**kwargs) 274 | button.connect('clicked', self.on_more_button_clicked) 275 | 276 | add_widget_class(button, 'event-item-details') 277 | 278 | return button 279 | 280 | def update_more_button(self): 281 | if self.event_count == 0: 282 | self.more_button.set_opacity(0.5) 283 | else: 284 | self.more_button.set_opacity(1) 285 | 286 | self.more_button.show() 287 | 288 | def on_more_button_clicked(self, _widget): 289 | self.callback(self.fixture) 290 | 291 | 292 | class MatchStreamBox(Gtk.ListBoxRow): 293 | 294 | __gtype_name__ = 'MatchStreamBox' 295 | 296 | stream = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 297 | callback = GObject.property(type=object, flags=GObject.PARAM_READWRITE) 298 | 299 | def __init__(self, *args, **kwargs): 300 | Gtk.ListBoxRow.__init__(self, *args, **kwargs) 301 | 302 | self.stream = self.get_property('stream') 303 | self.callback = self.get_property('callback') 304 | 305 | self.connect('realize', self.on_fixture_updated) 306 | self.connect('notify::stream', self.on_fixture_updated) 307 | 308 | add_widget_class(self, 'stream-item') 309 | 310 | self.show() 311 | 312 | def on_fixture_updated(self, *_args): 313 | if self.stream is None: 314 | self.do_nostreams_label() 315 | else: 316 | self.update_stream_box() 317 | 318 | def do_nostreams_label(self): 319 | label = Gtk.Label('No streams available...') 320 | label.set_margin_top(7) 321 | label.set_margin_bottom(10) 322 | label.show() 323 | 324 | self.add(label) 325 | 326 | add_widget_class(label, 'stream-unknown') 327 | 328 | return label 329 | 330 | def do_stream_box(self): 331 | box = StreamBox(stream=self.stream, callback=self.callback, compact=False) 332 | self.add(box) 333 | 334 | return box 335 | 336 | def update_stream_box(self): 337 | remove_widget_children(self) 338 | self.do_stream_box() 339 | -------------------------------------------------------------------------------- /handlers/data.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from playhouse.sqlite_ext import CharField, IntegerField, BooleanField 4 | from playhouse.sqlite_ext import DateTimeField, ForeignKeyField 5 | 6 | from peewee import IntegrityError, Model 7 | from helpers.utils import database_connection, relative_path 8 | from helpers.utils import query_date_range, parse_date, format_date, now, today 9 | 10 | 11 | class DataHandler(object): 12 | 13 | def __init__(self): 14 | self.dbs = database_connection('data.db') 15 | self.register_models() 16 | 17 | @property 18 | 19 | def fx_query(self): 20 | first = Fixture.select().where(Fixture.date >= today()).order_by(Fixture.date).limit(1) 21 | odate = now() if not first else first[0].date 22 | limit = query_date_range({ 'days': 21 }, odate) 23 | query = (Fixture.date > limit[0]) & (Fixture.date < limit[1]) 24 | 25 | return query 26 | 27 | @property 28 | 29 | def fl_query(self): 30 | limit = query_date_range({ 'hours': 3 }) 31 | query = (Fixture.date > limit[0]) & (Fixture.date < limit[1]) 32 | 33 | return query 34 | 35 | def register_models(self): 36 | tables = [Setting, Competition, Team, Fixture, Channel, Stream, Event] 37 | 38 | self.dbs.connect() 39 | self.dbs.create_tables(tables, safe=True) 40 | 41 | def get_model(self, model): 42 | try: 43 | model = globals()[model.title()] 44 | except NameError: 45 | return None 46 | 47 | return model 48 | 49 | def get_single(self, model, kwargs): 50 | model = self.get_model(model) 51 | 52 | try: 53 | item = model.get(**kwargs) 54 | except model.DoesNotExist: 55 | item = None 56 | 57 | return item 58 | 59 | def create_single(self, model, kwargs): 60 | model = self.get_model(model) 61 | 62 | try: 63 | item = model.create(**kwargs) 64 | except IntegrityError: 65 | item = None 66 | 67 | return item 68 | 69 | def update_single(self, model, item, kwargs): 70 | model = self.get_model(model) 71 | 72 | try: 73 | kwargs['updated'] = now() 74 | 75 | query = model.update(**kwargs).where(model.id == item.id) 76 | query.execute() 77 | except IntegrityError: 78 | item = None 79 | 80 | return item 81 | 82 | def set_single(self, model, kwargs, main_key='id'): 83 | key = kwargs.get(main_key, None) 84 | 85 | if key is None: 86 | return None 87 | 88 | item = self.get_single(model, { main_key: key }) 89 | 90 | if item is None: 91 | item = self.create_single(model, kwargs) 92 | else: 93 | item = self.update_single(model, item, kwargs) 94 | 95 | return item 96 | 97 | def get_multiple(self, model, key, values): 98 | model = self.get_model(model) 99 | values = list(values) 100 | items = [] 101 | 102 | if not values: 103 | return items 104 | 105 | try: 106 | key = getattr(model, key) 107 | items = model.select().where(key << values) 108 | except model.DoesNotExist: 109 | pass 110 | 111 | return items 112 | 113 | def set_multiple(self, model, items, main_key): 114 | for item in items: 115 | self.set_single(model, item, main_key) 116 | 117 | def load_settings(self): 118 | return Setting.select() 119 | 120 | def load_active_competitions(self, records=False, name_only=False): 121 | default = '1 4 5 7 9 17 13 19 10 18 23 33'.split(' ') 122 | setting = self.get_single('setting', { 'key': 'competitions' }) 123 | setting = default if setting is None else setting 124 | 125 | if records or name_only: 126 | setting = self.load_competitions(ids=setting) 127 | 128 | if name_only: 129 | setting = list(sum(setting.select(Competition.name).tuples(), ())) 130 | 131 | return setting 132 | 133 | def load_competitions(self, current=False, name_only=False, ids=None): 134 | items = Competition.select() 135 | items = items.where(Competition.api_id << self.load_active_competitions()) 136 | items = items if not current else items.join(Fixture).where(self.fx_query) 137 | items = items.distinct().order_by(Competition.section_name, Competition.api_id) 138 | items = items if not name_only else list(sum(items.select(Competition.name).tuples(), ())) 139 | items = items if ids is None else items.where(Competition.api_id << ids) 140 | 141 | return items 142 | 143 | def load_teams(self): 144 | return Team.select() 145 | 146 | def load_fixtures(self, current=False, id_only=False, today_only=False): 147 | items = Fixture.select().distinct() 148 | items = items.order_by(Fixture.date, Fixture.competition) 149 | items = items.where(Fixture.competition << self.load_active_competitions(True)) 150 | items = items if not current else items.where(self.fx_query) 151 | items = items if not today_only else items.where(self.fl_query) 152 | items = items if not id_only else list(sum(items.select(Fixture.id).tuples(), ())) 153 | 154 | return items 155 | 156 | def load_languages(self): 157 | items = Channel.select(Channel.language).join(Stream) 158 | items = items.distinct().tuples() 159 | items = sorted(list(set(sum(items, ())))) 160 | 161 | return items 162 | 163 | def load_channels(self, active=False, id_only=False): 164 | items = Channel.select() 165 | items = items if not active else items.join(Stream) 166 | items = items.order_by(Channel.name).distinct() 167 | items = items if not id_only else list(sum(items.select(Channel.id).tuples(), ())) 168 | 169 | return items 170 | 171 | def load_matches_filters(self, current=False): 172 | filters = self.load_competitions(True, True) if current else self.load_active_competitions(True, True) 173 | filters = ['All Competitions'] + filters if filters else [] 174 | 175 | return filters 176 | 177 | def load_channels_filters(self): 178 | filters = self.load_languages() 179 | filters = ['All Languages'] + filters if filters else [] 180 | 181 | return filters 182 | 183 | 184 | class BasicModel(Model): 185 | 186 | class Meta: 187 | database = database_connection('data.db') 188 | 189 | def reload(self): 190 | return self.get(id=self.id) 191 | 192 | 193 | class Setting(BasicModel): 194 | key = CharField(unique=True) 195 | value = CharField() 196 | 197 | 198 | class Competition(BasicModel): 199 | name = CharField() 200 | short_name = CharField() 201 | section_code = CharField() 202 | section_name = CharField() 203 | season_id = IntegerField() 204 | api_id = IntegerField(unique=True) 205 | created = DateTimeField(default=now()) 206 | updated = DateTimeField(default=now()) 207 | 208 | @property 209 | 210 | def teams(self): 211 | fixtures = self.fixtures.select(Fixture.home_team, Fixture.away_team) 212 | fixtures = fixtures.distinct().tuples() 213 | team_ids = list(set(sum(fixtures, ()))) 214 | teams = Team.select().where(Team.id << team_ids) 215 | 216 | return teams 217 | 218 | @property 219 | 220 | def fixtures(self): 221 | return Fixture.select().where((Fixture.competition == self)) 222 | 223 | 224 | class Team(BasicModel): 225 | name = CharField() 226 | crest_url = CharField(null=True) 227 | crest_path = CharField(null=True) 228 | national = BooleanField() 229 | api_id = IntegerField(unique=True) 230 | created = DateTimeField(default=now()) 231 | updated = DateTimeField(default=now()) 232 | 233 | @property 234 | 235 | def competitions(self): 236 | fixtures = self.fixtures.select(Fixture.competition) 237 | fixtures = fixtures.distinct().tuples() 238 | comp_ids = list(set(sum(fixtures, ()))) 239 | competitions = Competition.select().where(Competition.id << comp_ids) 240 | 241 | return competitions 242 | 243 | @property 244 | 245 | def fixtures(self): 246 | query = (Fixture.home_team == self) | (Fixture.away_team == self) 247 | fixtures = Fixture.select().where(query) 248 | 249 | return fixtures 250 | 251 | @property 252 | 253 | def crest(self): 254 | stock = relative_path('images/team-emblem.svg') 255 | path = str(self.crest_path) 256 | path = path if os.path.exists(path) else stock 257 | 258 | return path 259 | 260 | 261 | class Fixture(BasicModel): 262 | date = DateTimeField() 263 | minute = IntegerField(null=True) 264 | period = CharField(null=True) 265 | home_team = ForeignKeyField(Team, related_name='home_team') 266 | away_team = ForeignKeyField(Team, related_name='away_team') 267 | score_home = IntegerField(null=True) 268 | score_away = IntegerField(null=True) 269 | competition = ForeignKeyField(Competition, related_name='competition') 270 | api_id = IntegerField(unique=True) 271 | created = DateTimeField(default=now()) 272 | updated = DateTimeField(default=now()) 273 | 274 | @property 275 | 276 | def events(self): 277 | return Event.select().where(Event.fixture == self) 278 | 279 | @property 280 | 281 | def live(self): 282 | pastp = ['PreMatch', 'FullTime', 'Postponed'] 283 | fdate = parse_date(date=self.date, localize=False).date() 284 | fdate = fdate == today() and self.period not in pastp 285 | 286 | return fdate 287 | 288 | @property 289 | 290 | def today(self): 291 | fdate = parse_date(date=self.date, localize=False).date() 292 | fdate = fdate == today() and self.period != 'Postponed' 293 | 294 | return fdate 295 | 296 | @property 297 | 298 | def past(self): 299 | return self.period == 'FullTime' 300 | 301 | @property 302 | 303 | def score(self): 304 | posts = format_date(date=self.date, date_format="%d/%m/%Y\nPostponed") 305 | times = format_date(date=self.date, date_format="%H:%M") 306 | dates = format_date(date=self.date, date_format="%d/%m/%Y\n%H:%M") 307 | score = str(self.score_home) + ' - ' + str(self.score_away) 308 | score = times if self.period == 'PreMatch' else score 309 | score = score if self.today or self.past else dates 310 | score = posts if self.period == 'Postponed' else score 311 | 312 | return score 313 | 314 | 315 | class Channel(BasicModel): 316 | name = CharField(unique=True) 317 | language = CharField() 318 | logo_url = CharField(null=True) 319 | logo_path = CharField(null=True) 320 | created = DateTimeField(default=now()) 321 | updated = DateTimeField(default=now()) 322 | 323 | @property 324 | 325 | def logo(self): 326 | stock = relative_path('images/channel-logo.svg') 327 | path = str(self.logo_path) 328 | path = path if os.path.exists(path) else stock 329 | 330 | return path 331 | 332 | @property 333 | 334 | def streams(self): 335 | streams = Stream.select().where(Stream.channel == self).limit(2) 336 | streams = streams.distinct().order_by(Stream.created) 337 | 338 | return streams 339 | 340 | 341 | class Stream(BasicModel): 342 | host = CharField() 343 | rate = IntegerField() 344 | language = CharField() 345 | url = CharField() 346 | hd_url = CharField(null=True) 347 | ch_id = CharField(unique=True) 348 | channel = ForeignKeyField(Channel, related_name='channel') 349 | watched = DateTimeField(null=True) 350 | created = DateTimeField(default=now()) 351 | updated = DateTimeField(default=now()) 352 | 353 | @property 354 | 355 | def logo(self): 356 | fname = str(self.host).lower() 357 | image = relative_path("images/%s.svg" % fname) 358 | 359 | return image 360 | 361 | 362 | class Event(BasicModel): 363 | fs_id = CharField(unique=True) 364 | fixture = ForeignKeyField(Fixture, related_name='fixture') 365 | stream = ForeignKeyField(Stream, related_name='stream') 366 | created = DateTimeField(default=now()) 367 | updated = DateTimeField(default=now()) 368 | 369 | 370 | class StaticStream(object): 371 | 372 | def __init__(self, url): 373 | self.url = url 374 | -------------------------------------------------------------------------------- /ui/player.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 980 8 | 585 9 | True 10 | False 11 | 12 | 13 | True 14 | False 15 | GDK_POINTER_MOTION_MASK | GDK_STRUCTURE_MASK 16 | True 17 | 18 | 19 | 20 | 21 | 22 | -1 23 | 24 | 25 | 26 | 27 | True 28 | False 29 | end 30 | vertical 31 | bottom 32 | 33 | 34 | True 35 | False 36 | end 37 | 38 | 39 | 40 | True 41 | False 42 | 43 | 44 | True 45 | False 46 | 47 | 48 | True 49 | False 50 | True 51 | True 52 | 53 | 54 | 55 | True 56 | False 57 | media-playback-pause-symbolic 58 | 59 | 60 | 63 | 64 | 65 | False 66 | True 67 | 0 68 | 69 | 70 | 71 | 72 | True 73 | True 74 | True 75 | 76 | 77 | 78 | True 79 | False 80 | 15 81 | 15 82 | media-playback-start-symbolic 83 | 84 | 85 | 88 | 89 | 90 | False 91 | True 92 | 1 93 | 94 | 95 | 96 | 97 | True 98 | False 99 | True 100 | True 101 | 102 | 103 | 104 | True 105 | False 106 | media-playback-stop-symbolic 107 | 108 | 109 | 112 | 113 | 114 | False 115 | True 116 | 2 117 | 118 | 119 | 122 | 123 | 124 | 125 | 126 | False 127 | True 128 | 129 | 130 | 131 | 132 | 100 133 | True 134 | False 135 | 136 | 137 | True 138 | False 139 | Not playing 140 | 141 | 142 | 143 | 144 | True 145 | True 146 | 147 | 148 | 149 | 150 | True 151 | False 152 | 5 153 | 154 | 155 | True 156 | True 157 | False 158 | True 159 | none 160 | vertical 161 | 1 162 | audio-volume-muted-symbolic 163 | audio-volume-high-symbolic 164 | audio-volume-low-symbolic 165 | audio-volume-medium-symbolic 166 | 167 | 168 | 169 | True 170 | True 171 | center 172 | center 173 | none 174 | 175 | 176 | 177 | 178 | True 179 | True 180 | center 181 | center 182 | none 183 | 184 | 185 | 186 | 187 | 188 | 189 | False 190 | True 191 | 192 | 193 | 194 | 195 | True 196 | False 197 | 198 | 199 | True 200 | False 201 | 202 | 203 | True 204 | True 205 | True 206 | True 207 | 208 | 209 | 210 | True 211 | False 212 | view-fullscreen-symbolic 213 | 214 | 215 | 218 | 219 | 220 | False 221 | True 222 | 0 223 | 224 | 225 | 226 | 227 | True 228 | True 229 | True 230 | 231 | 232 | 233 | True 234 | False 235 | view-restore-symbolic 236 | 237 | 238 | 241 | 242 | 243 | False 244 | True 245 | 1 246 | 247 | 248 | 251 | 252 | 253 | 254 | 255 | False 256 | True 257 | 258 | 259 | 263 | 264 | 265 | False 266 | True 267 | 0 268 | 269 | 270 | 273 | 274 | 275 | 1 276 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /icons/src/kickoff-player.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | Paper Icon Theme 27 | 65 | 76 | 91 | 92 | 94 | 97 | 101 | 105 | 106 | 114 | 118 | 119 | 127 | 131 | 132 | 140 | 144 | 145 | 155 | 165 | 175 | 185 | 195 | 205 | 215 | 225 | 235 | 245 | 255 | 265 | 275 | 285 | 295 | 296 | 298 | 299 | 301 | image/svg+xml 302 | 304 | 305 | 306 | Sam Hewitt 307 | 308 | 309 | 310 | 312 | Paper Icon Theme 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 340 | image/svg+xml 341 | 343 | 344 | 345 | 346 | 352 | 427 | 432 | 437 | 447 | 451 | 457 | 463 | 469 | 475 | 481 | 487 | 488 | 498 | 508 | 518 | 528 | 532 | 538 | 544 | 550 | 556 | 562 | 568 | 569 | 574 | 577 | 583 | 589 | 595 | 600 | 605 | 610 | 611 | 616 | 626 | 629 | 635 | 641 | 647 | 652 | 658 | 663 | 664 | 670 | 674 | 680 | 686 | 692 | 698 | 704 | 710 | 711 | 721 | 727 | 737 | 747 | 753 | 759 | 765 | 771 | 777 | 783 | 793 | 803 | 813 | 819 | 829 | 839 | 845 | 851 | 852 | 853 | 854 | --------------------------------------------------------------------------------