initial commit
This commit is contained in:
parent
be8e7fc98c
commit
8903eae460
1
.gitignore
vendored
1
.gitignore
vendored
@ -58,3 +58,4 @@ docs/_build/
|
|||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
15
Pipfile
Normal file
15
Pipfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.python.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
pygobject = "*"
|
||||||
|
jupyterlab = "*"
|
||||||
|
ipykernel = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
autopep8 = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.8"
|
21
src/gtk_mediathek_player/gst_pipeline.py
Normal file
21
src/gtk_mediathek_player/gst_pipeline.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import gi
|
||||||
|
gi.require_version('Gst', '1.0')
|
||||||
|
|
||||||
|
from gi.repository import Gst
|
||||||
|
|
||||||
|
def init_gst() -> None:
|
||||||
|
if not init_gst.gst_initialized:
|
||||||
|
Gst.init(None)
|
||||||
|
Gst.init_check(None)
|
||||||
|
init_gst.gst_initialized = True
|
||||||
|
|
||||||
|
init_gst.gst_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
def create_pipeline():
|
||||||
|
init_gst()
|
||||||
|
|
||||||
|
pipeline = Gst.Pipeline()
|
||||||
|
|
||||||
|
return pipeline
|
||||||
|
|
97
src/gtk_mediathek_player/gst_widget.py
Normal file
97
src/gtk_mediathek_player/gst_widget.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
from gi.repository import Gtk, Gst
|
||||||
|
|
||||||
|
from . import gst_pipeline
|
||||||
|
|
||||||
|
|
||||||
|
class GstWidget(Gtk.Box):
|
||||||
|
def __init__(self):
|
||||||
|
Gtk.Box.__init__(self)
|
||||||
|
|
||||||
|
self._state = Gst.State.NULL
|
||||||
|
|
||||||
|
self.connect('realize', self._on_realize)
|
||||||
|
|
||||||
|
def _on_realize(self, widget):
|
||||||
|
self._build_sink_and_widget()
|
||||||
|
widget.pack_start(self._sinkwidget, True, True, 0)
|
||||||
|
self._create_player()
|
||||||
|
self._bus = self._player.get_bus()
|
||||||
|
self._bus.add_signal_watch()
|
||||||
|
self._bus.connect("message::state-changed", self.on_state_changed)
|
||||||
|
#self.load_from_uri("https://pdvideosdaserste-a.akamaihd.net/int/2020/05/20/412d8633-dfaf-438c-93d2-f81914d44945/960-1_679663.mp4")
|
||||||
|
self._sinkwidget.show()
|
||||||
|
#self.play()
|
||||||
|
#self._pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
|
def _build_pipeline(self):
|
||||||
|
self._pipeline = Gst.Pipeline()
|
||||||
|
|
||||||
|
bin = Gst.parse_bin_from_description('videotestsrc', True)
|
||||||
|
|
||||||
|
factory = self._pipeline.get_factory()
|
||||||
|
gtksink = factory.make('gtksink')
|
||||||
|
|
||||||
|
self._pipeline.add(bin)
|
||||||
|
|
||||||
|
self._pipeline.add(gtksink)
|
||||||
|
|
||||||
|
bin.link(gtksink)
|
||||||
|
|
||||||
|
return gtksink
|
||||||
|
|
||||||
|
def _build_sink_and_widget(self) -> None:
|
||||||
|
gtkglsink = Gst.ElementFactory.make("gtkglsink")
|
||||||
|
|
||||||
|
# TODO: fallback if not graphic acceleration is available
|
||||||
|
|
||||||
|
sinkbin = Gst.ElementFactory.make("glsinkbin")
|
||||||
|
sinkbin.set_property("sink", gtkglsink)
|
||||||
|
|
||||||
|
self._sink = sinkbin
|
||||||
|
|
||||||
|
self._sinkwidget = gtkglsink.get_property("widget")
|
||||||
|
|
||||||
|
def _create_player(self) -> None:
|
||||||
|
self._player = Gst.ElementFactory.make("playbin", "player")
|
||||||
|
self._player.set_property("video-sink", self._sink)
|
||||||
|
self._state = Gst.State.NULL
|
||||||
|
|
||||||
|
|
||||||
|
def load_from_uri(self, url:str) -> None:
|
||||||
|
self.stop()
|
||||||
|
self._player.set_property("uri", url)
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
self._player.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self._player.set_state(Gst.State.PAUSED)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._player.set_state(Gst.State.NULL)
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def get_duration(self):
|
||||||
|
return self._player.query_duration(Gst.Format.TIME)[1] / Gst.SECOND
|
||||||
|
|
||||||
|
def get_position(self):
|
||||||
|
return self._player.query_position(Gst.Format.TIME)[1] / Gst.SECOND
|
||||||
|
|
||||||
|
def set_position(self, position):
|
||||||
|
if self._state == Gst.State.PLAYING or self._state == Gst.State.PAUSED:
|
||||||
|
self._player.seek_simple(
|
||||||
|
Gst.Format.TIME,
|
||||||
|
Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
|
||||||
|
position * Gst.SECOND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def on_state_changed(self, bus, msg):
|
||||||
|
old, new, pending = msg.parse_state_changed()
|
||||||
|
if not msg.src == self._player:
|
||||||
|
# not from the playbin, ignore
|
||||||
|
return
|
||||||
|
|
||||||
|
self._state = new
|
110
src/gtk_mediathek_player/main_window.py
Normal file
110
src/gtk_mediathek_player/main_window.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import gi
|
||||||
|
|
||||||
|
from gtk_mediathek_player.tools import new_button_with_icon, new_radio_button_with_icon
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, Gio
|
||||||
|
|
||||||
|
from . import player_widget
|
||||||
|
from . import search_widget
|
||||||
|
from . import tools
|
||||||
|
from . import mediathek_request as mr
|
||||||
|
|
||||||
|
class MainApp(Gtk.Window):
|
||||||
|
def __init__(self):
|
||||||
|
Gtk.Window.__init__(self, title="Gtk Mediathek Player")
|
||||||
|
|
||||||
|
self.set_default_size(800, 600)
|
||||||
|
|
||||||
|
self._main_container = Gtk.Overlay()
|
||||||
|
|
||||||
|
self._main_stack = Gtk.Stack()
|
||||||
|
|
||||||
|
self._fullscreen = False
|
||||||
|
|
||||||
|
self._player_widget = self._create_player_widget()
|
||||||
|
self._search_widget = self._create_search_widget()
|
||||||
|
|
||||||
|
self._main_stack.add_named(self._player_widget, "player")
|
||||||
|
self._main_stack.add_named(self._search_widget, "search")
|
||||||
|
|
||||||
|
self._main_stack.show_all()
|
||||||
|
|
||||||
|
self.set_active_pane("search")
|
||||||
|
|
||||||
|
|
||||||
|
self._main_container.add(self._main_stack)
|
||||||
|
|
||||||
|
self._headerbar = self._create_headerbar(True)
|
||||||
|
|
||||||
|
|
||||||
|
#self._main_container.add_overlay(self._create_fullscreen_bar())
|
||||||
|
|
||||||
|
self.add(self._main_container)
|
||||||
|
|
||||||
|
|
||||||
|
def on_search(self, _ = None):
|
||||||
|
if self.get_active_pane() == "player":
|
||||||
|
self._player_widget.stop()
|
||||||
|
self.set_active_pane("search")
|
||||||
|
|
||||||
|
def _create_context_switch(self):
|
||||||
|
|
||||||
|
self._search_radio = new_button_with_icon("system-search")
|
||||||
|
|
||||||
|
self._search_radio.connect("clicked", self.on_search)
|
||||||
|
|
||||||
|
# more to come...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _create_headerbar(self, main_bar = True):
|
||||||
|
headerbar = Gtk.HeaderBar()
|
||||||
|
|
||||||
|
fullscreen = tools.new_button_with_icon("view-fullscreen")
|
||||||
|
|
||||||
|
headerbar.pack_end(fullscreen)
|
||||||
|
|
||||||
|
if main_bar:
|
||||||
|
|
||||||
|
|
||||||
|
self._create_context_switch()
|
||||||
|
|
||||||
|
headerbar.pack_start(self._search_radio)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
headerbar.set_show_close_button(True)
|
||||||
|
headerbar.props.title = "Main Window"
|
||||||
|
self.set_titlebar(headerbar)
|
||||||
|
return headerbar
|
||||||
|
|
||||||
|
|
||||||
|
def _create_fullscreen_bar(self):
|
||||||
|
self._revealer = Gtk.Revealer()
|
||||||
|
self._revealer.add(self._create_headerbar(False))
|
||||||
|
self.set_valign(Gtk.Align.START)
|
||||||
|
return self._revealer
|
||||||
|
|
||||||
|
|
||||||
|
def _create_player_widget(self):
|
||||||
|
return player_widget.PlayerWidget()
|
||||||
|
|
||||||
|
def _create_search_widget(self):
|
||||||
|
return search_widget.SearchWidget(self)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.connect("destroy", Gtk.main_quit)
|
||||||
|
self.show_all()
|
||||||
|
Gtk.main()
|
||||||
|
|
||||||
|
def set_active_pane(self, name: str):
|
||||||
|
self._main_stack.set_visible_child_name(name)
|
||||||
|
|
||||||
|
def get_active_pane(self):
|
||||||
|
return self._main_stack.get_visible_child_name()
|
||||||
|
|
||||||
|
def start_player(self, uri: str):
|
||||||
|
if self.get_active_pane() != "player":
|
||||||
|
self.set_active_pane("player")
|
||||||
|
self._player_widget.play_from_uri(uri)
|
185
src/gtk_mediathek_player/mediathek_request.py
Normal file
185
src/gtk_mediathek_player/mediathek_request.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class MediathekViewWebAnswer(object):
|
||||||
|
def __init__(self, answer_json: str):
|
||||||
|
|
||||||
|
def get_field(field: str):
|
||||||
|
if field in answer_json:
|
||||||
|
return answer_json[field]
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._raw = answer_json
|
||||||
|
|
||||||
|
self._channel = get_field('channel')
|
||||||
|
self._topic = get_field('topic')
|
||||||
|
self._title = get_field('title')
|
||||||
|
self._description = get_field('description')
|
||||||
|
self._timestamp = get_field('timestamp')
|
||||||
|
self._duration = get_field('duration')
|
||||||
|
self._size = get_field('size')
|
||||||
|
self._url_website = get_field('url_website')
|
||||||
|
self._url_subtitle = get_field('url_subtitle')
|
||||||
|
self._filmlisteTimestamp = get_field('filmlisteTimestamp')
|
||||||
|
self._id = get_field('id')
|
||||||
|
self._url_video = get_field('url_video')
|
||||||
|
self._url_video_low = get_field('url_video_low')
|
||||||
|
self._url_video_hd = get_field('url_video_hd')
|
||||||
|
|
||||||
|
def get_best_vid_url(self) -> str:
|
||||||
|
if self._url_video_hd is not None:
|
||||||
|
return self._url_video_hd
|
||||||
|
|
||||||
|
if self._url_video is not None:
|
||||||
|
return self._url_video
|
||||||
|
|
||||||
|
if self._url_video_low is not None:
|
||||||
|
return self._url_video_low
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_lowest_vid_url(self) -> str:
|
||||||
|
if self._url_video_low is not None:
|
||||||
|
return self._url_video_low
|
||||||
|
|
||||||
|
if self._url_video is not None:
|
||||||
|
return self._url_video
|
||||||
|
|
||||||
|
if self._url_video_hd is not None:
|
||||||
|
return self._url_video_hd
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
def get_site(self):
|
||||||
|
return self._url_website
|
||||||
|
|
||||||
|
def get_title(self):
|
||||||
|
return self._title
|
||||||
|
|
||||||
|
def get_channel(self):
|
||||||
|
return self._channel
|
||||||
|
|
||||||
|
def get_topic(self):
|
||||||
|
return self._topic
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
return self._description
|
||||||
|
|
||||||
|
|
||||||
|
class MediathekViewWebRequest(object):
|
||||||
|
@staticmethod
|
||||||
|
def from_serialization(
|
||||||
|
d: dict,
|
||||||
|
request_url="https://mediathekviewweb.de/api/query"
|
||||||
|
) -> 'MediathekViewWebRequest':
|
||||||
|
|
||||||
|
json_query = d['queries'][0]
|
||||||
|
sort_by = d['sortBy']
|
||||||
|
sort_order = d['sortOrder']
|
||||||
|
future = d['future']
|
||||||
|
offset = d['offset']
|
||||||
|
size = d['size']
|
||||||
|
|
||||||
|
query = json_query['query']
|
||||||
|
fields = json_query['fields']
|
||||||
|
|
||||||
|
search_channel = "channel" in fields
|
||||||
|
search_topic = "topic" in fields
|
||||||
|
search_title = "title" in fields
|
||||||
|
|
||||||
|
return MediathekViewWebRequest(
|
||||||
|
query=query,
|
||||||
|
search_title=search_title,
|
||||||
|
search_channel=search_channel,
|
||||||
|
search_topic=search_topic,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
|
future=future,
|
||||||
|
size=size,
|
||||||
|
offset=offset,
|
||||||
|
request_url=request_url
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json(
|
||||||
|
js: str,
|
||||||
|
request_url="https://mediathekviewweb.de/api/query"
|
||||||
|
) -> 'MediathekViewWebRequest':
|
||||||
|
return MediathekViewWebRequest.from_serialization(
|
||||||
|
d=json.loads(js),
|
||||||
|
request_url=request_url
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
query: str,
|
||||||
|
search_title: bool = True,
|
||||||
|
search_channel: bool = False,
|
||||||
|
search_topic: bool = False,
|
||||||
|
sort_by: str = 'timestamp',
|
||||||
|
sort_order: str = 'desc',
|
||||||
|
future: bool = False,
|
||||||
|
size: int = 10,
|
||||||
|
offset: int = 0,
|
||||||
|
request_url="https://mediathekviewweb.de/api/query"):
|
||||||
|
|
||||||
|
assert search_title or search_channel or search_topic
|
||||||
|
assert sort_order == 'desc' or sort_order == 'asc'
|
||||||
|
|
||||||
|
self._query = query
|
||||||
|
self._fields = []
|
||||||
|
if search_title:
|
||||||
|
self._fields.append("title")
|
||||||
|
if search_channel:
|
||||||
|
self._fields.append("channel")
|
||||||
|
if search_topic:
|
||||||
|
self._fields.append("topic")
|
||||||
|
|
||||||
|
self._sort_by = sort_by
|
||||||
|
self._sort_order = sort_order
|
||||||
|
self._future = future
|
||||||
|
self._size = size
|
||||||
|
self._offset = offset
|
||||||
|
self._request_url = request_url
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
query = {
|
||||||
|
'queries': [
|
||||||
|
{
|
||||||
|
'fields': self._fields,
|
||||||
|
'query': self._query
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
'sortBy': self._sort_by,
|
||||||
|
'sortOrder': self._sort_order,
|
||||||
|
'future': self._future,
|
||||||
|
'offset': self._offset,
|
||||||
|
'size': self._size
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps(self.serialize())
|
||||||
|
|
||||||
|
def perform_request(self):
|
||||||
|
headers = {'Content-type': 'text/plain'}
|
||||||
|
req = requests.post(self._request_url,
|
||||||
|
data=self.to_json(), headers=headers)
|
||||||
|
|
||||||
|
if not req.ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
answer = json.loads(req.text)
|
||||||
|
|
||||||
|
if 'result' in answer:
|
||||||
|
result = answer['result']
|
||||||
|
|
||||||
|
if 'results' in result:
|
||||||
|
results = result['results']
|
||||||
|
return [MediathekViewWebAnswer(i) for i in results]
|
||||||
|
|
||||||
|
return None
|
112
src/gtk_mediathek_player/player_widget.py
Normal file
112
src/gtk_mediathek_player/player_widget.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
from . import tools
|
||||||
|
from . import gst_widget
|
||||||
|
from gi.repository import Gtk, GLib, Gst
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
gi.require_version('Gst', '1.0')
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerWidget(Gtk.Overlay):
|
||||||
|
@staticmethod
|
||||||
|
def create_player_for_uri(uri:str):
|
||||||
|
widget = PlayerWidget()
|
||||||
|
widget.play_from_uri(uri)
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
Gtk.Overlay.__init__(self)
|
||||||
|
self._videoarea = gst_widget.GstWidget()
|
||||||
|
|
||||||
|
self._create_controls()
|
||||||
|
|
||||||
|
self.add(self._videoarea)
|
||||||
|
|
||||||
|
self.add_overlay(self._controls)
|
||||||
|
|
||||||
|
self._duration = None
|
||||||
|
|
||||||
|
self._is_update = False
|
||||||
|
|
||||||
|
GLib.timeout_add_seconds(1, self.update_controls)
|
||||||
|
|
||||||
|
self._slider.connect("value-changed", self._on_user_slider_change)
|
||||||
|
|
||||||
|
def update_controls(self):
|
||||||
|
|
||||||
|
self._is_update = True
|
||||||
|
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
if state == Gst.State.PLAYING or state == Gst.State.PAUSED:
|
||||||
|
self._duration = self._videoarea.get_duration()
|
||||||
|
|
||||||
|
self._slider.set_range(0, self._duration)
|
||||||
|
|
||||||
|
if self._duration is not None:
|
||||||
|
|
||||||
|
self._slider.set_value(self._videoarea.get_position())
|
||||||
|
|
||||||
|
self._is_update = False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _on_user_slider_change(self, range):
|
||||||
|
if self._is_update:
|
||||||
|
return
|
||||||
|
value = self._slider.get_value()
|
||||||
|
self._videoarea.set_position(value)
|
||||||
|
|
||||||
|
def _create_controls(self) -> Gtk.Box:
|
||||||
|
self._slider = Gtk.Scale.new_with_range(orientation=Gtk.Orientation.HORIZONTAL,
|
||||||
|
min=0,
|
||||||
|
max=100,
|
||||||
|
step=1)
|
||||||
|
|
||||||
|
self._slider.props.draw_value = False
|
||||||
|
|
||||||
|
self._play_button = tools.new_button_with_icon('media-playback-start')
|
||||||
|
self._stop_button = tools.new_button_with_icon('media-playback-stop')
|
||||||
|
|
||||||
|
self._controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
|
||||||
|
self._controls.pack_start(self._play_button,
|
||||||
|
expand=False,
|
||||||
|
fill=False,
|
||||||
|
padding=0)
|
||||||
|
|
||||||
|
self._controls.pack_start(self._stop_button,
|
||||||
|
expand=False,
|
||||||
|
fill=False,
|
||||||
|
padding=0)
|
||||||
|
|
||||||
|
self._controls.pack_end(self._slider,
|
||||||
|
expand=True,
|
||||||
|
fill=True,
|
||||||
|
padding=0)
|
||||||
|
|
||||||
|
self._controls.set_valign(Gtk.Align.END)
|
||||||
|
|
||||||
|
self._play_button.connect("clicked", self.play)
|
||||||
|
self._stop_button.connect("clicked", self.pause)
|
||||||
|
|
||||||
|
def play_from_uri(self, uri: str):
|
||||||
|
self._videoarea.load_from_uri(uri)
|
||||||
|
self.play()
|
||||||
|
|
||||||
|
def play(self, _=None):
|
||||||
|
if self.get_state() != Gst.State.PLAYING:
|
||||||
|
self._videoarea.play()
|
||||||
|
|
||||||
|
def stop(self, _=None):
|
||||||
|
state = self.get_state()
|
||||||
|
if state == Gst.State.PLAYING or state == Gst.State.PAUSED:
|
||||||
|
self._videoarea.stop()
|
||||||
|
self._duration = None
|
||||||
|
|
||||||
|
def pause(self, _=None):
|
||||||
|
if self.get_state() == Gst.State.PLAYING:
|
||||||
|
self._videoarea.pause()
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
return self._videoarea.get_state()
|
129
src/gtk_mediathek_player/search_widget.py
Normal file
129
src/gtk_mediathek_player/search_widget.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
from gi.repository import Gtk, GLib
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
|
||||||
|
from . import tools
|
||||||
|
from . import mediathek_request as mr
|
||||||
|
|
||||||
|
class SearchWidget(Gtk.Box):
|
||||||
|
def __init__(self, parent: 'main_window.MainApp') -> None:
|
||||||
|
Gtk.Box.__init__(self, orientation = Gtk.Orientation.VERTICAL)
|
||||||
|
|
||||||
|
self._search_bar = Gtk.SearchBar()
|
||||||
|
self._search_entry = Gtk.SearchEntry()
|
||||||
|
self._search_bar.add(self._search_entry)
|
||||||
|
self.pack_start(self._search_bar, False, False, 0)
|
||||||
|
|
||||||
|
self._search_bar.show_now()
|
||||||
|
self._search_bar.show_all()
|
||||||
|
self._search_bar.set_search_mode(True)
|
||||||
|
|
||||||
|
self._search_entry.connect("activate", self.on_search)
|
||||||
|
|
||||||
|
self._main_window = parent
|
||||||
|
self._current_search_cards = []
|
||||||
|
self._search_card_container = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
|
||||||
|
|
||||||
|
self.pack_end(self._search_card_container, False, True, 10)
|
||||||
|
|
||||||
|
def dialog_response(self, widget, response_id):
|
||||||
|
# if the button clicked gives response OK (-5)
|
||||||
|
if response_id == Gtk.ResponseType.OK:
|
||||||
|
pass
|
||||||
|
# if the messagedialog is destroyed (by pressing ESC)
|
||||||
|
elif response_id == Gtk.ResponseType.DELETE_EVENT:
|
||||||
|
pass
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
def display_warning(self, message):
|
||||||
|
messagedialog = Gtk.MessageDialog(parent=self,
|
||||||
|
flags=Gtk.DialogFlags.MODAL,
|
||||||
|
type=Gtk.MessageType.WARNING,
|
||||||
|
buttons=Gtk.ButtonsType.OK,
|
||||||
|
message_format=message)
|
||||||
|
# connect the response (of the button clicked) to the function
|
||||||
|
# dialog_response()
|
||||||
|
messagedialog.connect("response", self.dialog_response)
|
||||||
|
# show the messagedialog
|
||||||
|
messagedialog.show()
|
||||||
|
|
||||||
|
def create_result_card(self, answer:mr.MediathekViewWebAnswer) -> Gtk.Box:
|
||||||
|
|
||||||
|
url = answer.get_best_vid_url()
|
||||||
|
title = answer.get_title()
|
||||||
|
description = answer.get_description()
|
||||||
|
|
||||||
|
if len(description) > 50:
|
||||||
|
description = description[:50] + "..."
|
||||||
|
|
||||||
|
play_button = tools.new_button_with_icon("media-playback-start")
|
||||||
|
|
||||||
|
def on_click(_ = None):
|
||||||
|
if url is not None:
|
||||||
|
self._main_window.start_player(url)
|
||||||
|
|
||||||
|
play_button.connect("clicked", on_click)
|
||||||
|
|
||||||
|
title_label = Gtk.Label()
|
||||||
|
title_label.set_markup(f"<b>{title}</b>")
|
||||||
|
title_label.set_justify(Gtk.Justification.LEFT)
|
||||||
|
title_label.set_xalign(0)
|
||||||
|
description_label = Gtk.Label()
|
||||||
|
description_label.set_markup(description)
|
||||||
|
description_label.set_justify(Gtk.Justification.LEFT)
|
||||||
|
description_label.set_xalign(0)
|
||||||
|
|
||||||
|
text_box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
|
||||||
|
|
||||||
|
text_box.pack_start(title_label, True, False, 0)
|
||||||
|
text_box.pack_start(description_label, True, False, 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
card_content = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL)
|
||||||
|
card_content.pack_start(text_box, True, True, 0)
|
||||||
|
card_content.pack_start(play_button, False, False, 0)
|
||||||
|
|
||||||
|
card = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
|
||||||
|
card.pack_start(card_content, True, True, 0)
|
||||||
|
card.pack_end(Gtk.Separator(orientation = Gtk.Orientation.HORIZONTAL), False, False, 10)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return card
|
||||||
|
|
||||||
|
def clean_results(self):
|
||||||
|
for result in self._current_search_cards:
|
||||||
|
self._search_card_container.remove(result)
|
||||||
|
|
||||||
|
self._current_search_cards = []
|
||||||
|
|
||||||
|
def on_search(self, _) -> None:
|
||||||
|
search_text = self._search_entry.get_text()
|
||||||
|
|
||||||
|
req = mr.MediathekViewWebRequest(query=search_text)
|
||||||
|
|
||||||
|
answers = req.perform_request()
|
||||||
|
|
||||||
|
if answers is None:
|
||||||
|
self.display_warning("error while performing the search request")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(answers) == 0:
|
||||||
|
self.display_warning("no results found")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.clean_results()
|
||||||
|
for answer in answers:
|
||||||
|
#print(answer)
|
||||||
|
card = self.create_result_card(answer)
|
||||||
|
self._current_search_cards.append(card)
|
||||||
|
self._search_card_container.pack_start(card, False, False, 0)
|
||||||
|
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
19
src/gtk_mediathek_player/tools.py
Normal file
19
src/gtk_mediathek_player/tools.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
|
||||||
|
def new_button_with_icon(freedesktop_icon: str) -> Gtk.Button:
|
||||||
|
button = Gtk.Button()
|
||||||
|
icon = Gtk.Image.new_from_icon_name(freedesktop_icon, Gtk.IconSize.BUTTON)
|
||||||
|
button.props.image = icon
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
def new_radio_button_with_icon(freedesktop_icon: str,
|
||||||
|
ref_widget=None) -> Gtk.RadioButton:
|
||||||
|
if ref_widget is None:
|
||||||
|
button = Gtk.RadioButton()
|
||||||
|
else:
|
||||||
|
button = Gtk.RadioButton.new_from_widget(ref_widget)
|
||||||
|
icon = Gtk.Image.new_from_icon_name(freedesktop_icon, Gtk.IconSize.LARGE_TOOLBAR)
|
||||||
|
button.props.image = icon
|
||||||
|
return button
|
232
testing/testing.ipynb
Normal file
232
testing/testing.ipynb
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.8.5-final"
|
||||||
|
},
|
||||||
|
"orig_nbformat": 2,
|
||||||
|
"kernelspec": {
|
||||||
|
"name": "Python 3.8.5 64-bit ('mediathek-viewer': pipenv)",
|
||||||
|
"display_name": "Python 3.8.5 64-bit ('mediathek-viewer': pipenv)",
|
||||||
|
"metadata": {
|
||||||
|
"interpreter": {
|
||||||
|
"hash": "263d32d413daecf6c7bf0192414d6289b163e9f1c6898c3837ddf25533151cb9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 2,
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "stream",
|
||||||
|
"name": "stdout",
|
||||||
|
"text": [
|
||||||
|
"/home/jonas/Dokumente/gitRepos/mediathek-viewer/src\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"cd ../src"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import gi\n",
|
||||||
|
"from gtk_mediathek_player import main_window as mw\n",
|
||||||
|
"from gi.repository import Gtk, Gio, Gst\n",
|
||||||
|
"\n",
|
||||||
|
"gi.require_version('Gtk', '3.0')\n",
|
||||||
|
"gi.require_version('Gst', '1.0')\n",
|
||||||
|
"\n",
|
||||||
|
"Gst.init(None)\n",
|
||||||
|
"Gst.init_check(None)\n",
|
||||||
|
"\n",
|
||||||
|
"win = mw.MainApp()\n",
|
||||||
|
"#Gtk.Window.fullscreen(win)\n",
|
||||||
|
"win.run()\n",
|
||||||
|
"\n",
|
||||||
|
"\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"<Gtk.Separator object at 0x7f20efbc5a40 (GtkSeparator at 0x1e10990)>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"Gtk.Separator(orientation = Gtk.Orientation.HORIZONTAL)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"Gtk.Align."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"bar = Gtk.SearchBar()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"bar."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"''"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"search_bar.get_text()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"gtkglsink = Gst.ElementFactory.make(\"gtkglsink\")\n",
|
||||||
|
"glsinkbin = Gst.ElementFactory.make(\"glsinkbin\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 12,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"glsinkbin.set_property(\"sink\", gtkglsink)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 13,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"__gi__.GstGtkGLSink"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 13
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"type(gtkglsink)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 14,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"<__gi__.GstGtkGLSink object at 0x7f2e5f1b1900 (GstGtkGLSink at 0x2686190)>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 14
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"gtkglsink"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"player = Gst.ElementFactory.make(\"playbin\", \"player\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"output_type": "execute_result",
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"<__gi__.GstPlayBin object at 0x7f0b3559cfc0 (GstPlayBin at 0x37fe8f0)>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"execution_count": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"Gst.State.NU"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user