initial commit

This commit is contained in:
Jonas Weinz 2020-10-18 20:19:26 +02:00
parent be8e7fc98c
commit 8903eae460
10 changed files with 921 additions and 0 deletions

1
.gitignore vendored
View File

@ -58,3 +58,4 @@ docs/_build/
# PyBuilder
target/
.vscode/

15
Pipfile Normal file
View 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"

View 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

View 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

View 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)

View 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

View 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()

View 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()

View 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
View 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"
]
}
]
}