├── __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 |
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 |
5 |
--------------------------------------------------------------------------------
/images/channel-logo.svg:
--------------------------------------------------------------------------------
1 |
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 | 
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 |
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 |
33 |
34 |
35 |
40 |
41 |
42 |
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 |
854 |
--------------------------------------------------------------------------------