Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
|
de02f43529 | ||
b7113b2e44 | |||
0a418e5836 | |||
03edf98b86 | |||
5a161c7e82 | |||
60ab02b4ba | |||
fc0a26c74e | |||
9857816402 | |||
|
e794b336ee | ||
|
ff9ac9fb6c | ||
|
733f8acee5 | ||
|
a5a2a5abb8 | ||
|
165c98175a | ||
|
0528d20f1d | ||
|
4f719ac781 | ||
|
30544ac026 | ||
|
564f64106a | ||
|
ee5aff33c1 | ||
6af864c5d6 | |||
|
d90de29d4f |
15
README.md
|
@ -1,5 +1,18 @@
|
||||||
# cast
|
# cast
|
||||||
|
|
||||||
This program is intended for use on linux phones, and allow watching video streams, either directly on the phone via gstreamer or through chrome cast.
|
This program is intended for use on linux phones, and allow watching video streams, either directly on the phone via vlc or through chromecast.
|
||||||
|
|
||||||
Currently Swedish Public Service TV is supported.
|
Currently Swedish Public Service TV is supported.
|
||||||
|
|
||||||
|
Install like this on debian/mobian:
|
||||||
|
|
||||||
|
```
|
||||||
|
wget -O - https://repo.mic.ke/PUBLIC.KEY | gpg --dearmor --output micke-archive-unstable.gpg && sudo mv micke-archive-unstable.gpg /usr/share/keyrings
|
||||||
|
sudo wget -O /etc/apt/sources.list.d/debian-micke-unstable.list https://repo.mic.ke/debian/debian-micke-unstable.list
|
||||||
|
sudo apt update && sudo apt install cast
|
||||||
|
```
|
||||||
|
|
||||||
|
And like this with pip on any other distribution:
|
||||||
|
```
|
||||||
|
python3 -m pip install git+https://code.smolnet.org/micke/cast.git
|
||||||
|
```
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 401 B |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
@ -7,5 +7,5 @@ Comment=Video Player
|
||||||
Exec=/usr/bin/cast
|
Exec=/usr/bin/cast
|
||||||
Icon=video-single-display-symbolic
|
Icon=video-single-display-symbolic
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Security;Utility;
|
Categories=Video;
|
||||||
Keywords=SVT;
|
Keywords=SVT;
|
||||||
|
|
194
scripts/cast
Normal file → Executable file
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
import pychromecast
|
import pychromecast
|
||||||
|
@ -11,7 +13,8 @@ from vlc import Instance
|
||||||
|
|
||||||
from Channel import SVT
|
from Channel import SVT
|
||||||
from ChannelProvider import ChannelProvider
|
from ChannelProvider import ChannelProvider
|
||||||
from Utils import MYPATH, make_bitmap_from_file, make_sized_button
|
from Utils import (get_all_svt_categories, make_bitmap_from_file,
|
||||||
|
make_sized_button, BASEPATH)
|
||||||
|
|
||||||
WIDTH = int(720 / 2)
|
WIDTH = int(720 / 2)
|
||||||
HEIGHT = int(1440 / 2)
|
HEIGHT = int(1440 / 2)
|
||||||
|
@ -27,12 +30,23 @@ FLAGS.Center()
|
||||||
|
|
||||||
|
|
||||||
class Cast(wx.Frame):
|
class Cast(wx.Frame):
|
||||||
|
|
||||||
def __init__(self, *args, **kw):
|
def __init__(self, *args, **kw):
|
||||||
"""__init__.
|
"""__init__.
|
||||||
:param args:
|
:param args:
|
||||||
:param kw:
|
:param kw:
|
||||||
"""
|
"""
|
||||||
|
self.m_mode = 'normal'
|
||||||
|
self.asset_path: str = '/usr/share/cast'
|
||||||
|
url = None
|
||||||
|
if "url" in kw:
|
||||||
|
self.m_mode = 'single'
|
||||||
|
url = kw['url']
|
||||||
|
del kw['url']
|
||||||
super().__init__(*args, **kw)
|
super().__init__(*args, **kw)
|
||||||
|
if not os.path.isdir(BASEPATH):
|
||||||
|
os.mkdir(BASEPATH)
|
||||||
|
|
||||||
self.m_selected_chromecast = None
|
self.m_selected_chromecast = None
|
||||||
self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH)
|
self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH)
|
||||||
self.m_style = self.GetWindowStyle()
|
self.m_style = self.GetWindowStyle()
|
||||||
|
@ -40,7 +54,6 @@ class Cast(wx.Frame):
|
||||||
args=(),
|
args=(),
|
||||||
kwargs={})
|
kwargs={})
|
||||||
self.m_vlc = Instance()
|
self.m_vlc = Instance()
|
||||||
self.m_vlc_medialist = self.m_vlc.media_list_new()
|
|
||||||
self.m_vlc_listplayer = self.m_vlc.media_list_player_new()
|
self.m_vlc_listplayer = self.m_vlc.media_list_player_new()
|
||||||
self.m_chromecast_thr.start()
|
self.m_chromecast_thr.start()
|
||||||
self.m_sizer: wx.Sizer = wx.BoxSizer(wx.VERTICAL)
|
self.m_sizer: wx.Sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
@ -49,7 +62,12 @@ class Cast(wx.Frame):
|
||||||
self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True)
|
self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True)
|
||||||
self.m_panel.SetSizer(self.m_sizer)
|
self.m_panel.SetSizer(self.m_sizer)
|
||||||
self.m_providers: list[ChannelProvider] = self.get_providers()
|
self.m_providers: list[ChannelProvider] = self.get_providers()
|
||||||
self.show_channel_list(None, 0)
|
if url:
|
||||||
|
self.show_player(None, url, 0)
|
||||||
|
elif len(self.m_providers) > 1:
|
||||||
|
self.show_provider_list(None)
|
||||||
|
else:
|
||||||
|
self.show_channel_list(None, 0)
|
||||||
|
|
||||||
def add_back_button(self, callback: Callable) -> None:
|
def add_back_button(self, callback: Callable) -> None:
|
||||||
backbtn = wx.Button(self.m_panel,
|
backbtn = wx.Button(self.m_panel,
|
||||||
|
@ -68,6 +86,7 @@ class Cast(wx.Frame):
|
||||||
self.m_chromecasts, self.m_browser = pychromecast.get_chromecasts()
|
self.m_chromecasts, self.m_browser = pychromecast.get_chromecasts()
|
||||||
|
|
||||||
def get_player_controls(self, channel_index: int, uri: str) -> wx.BoxSizer:
|
def get_player_controls(self, channel_index: int, uri: str) -> wx.BoxSizer:
|
||||||
|
outer_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
play_button = wx.Button(self.m_panel,
|
play_button = wx.Button(self.m_panel,
|
||||||
-1,
|
-1,
|
||||||
|
@ -79,28 +98,34 @@ class Cast(wx.Frame):
|
||||||
"Pause",
|
"Pause",
|
||||||
size=(WIDTH / 4, BTN_HEIGHT))
|
size=(WIDTH / 4, BTN_HEIGHT))
|
||||||
|
|
||||||
back_button = wx.Button(self.m_panel,
|
|
||||||
-1,
|
|
||||||
"Back",
|
|
||||||
size=(WIDTH / 4, BTN_HEIGHT))
|
|
||||||
back_button.Bind(
|
|
||||||
wx.EVT_BUTTON,
|
|
||||||
lambda event, cindex=channel_index: self.show_video_list(
|
|
||||||
event, cindex),
|
|
||||||
)
|
|
||||||
|
|
||||||
stop_button = wx.Button(self.m_panel,
|
stop_button = wx.Button(self.m_panel,
|
||||||
-1,
|
-1,
|
||||||
"Stop",
|
"Stop",
|
||||||
size=(WIDTH / 4, BTN_HEIGHT))
|
size=(WIDTH / 4, BTN_HEIGHT))
|
||||||
|
if self.m_mode == 'normal':
|
||||||
|
back_button = wx.Button(self.m_panel,
|
||||||
|
-1,
|
||||||
|
"Go Back",
|
||||||
|
size=(WIDTH, BTN_HEIGHT))
|
||||||
|
back_button.Bind(
|
||||||
|
wx.EVT_BUTTON,
|
||||||
|
lambda event, cindex=channel_index: self.show_video_list(
|
||||||
|
event, cindex),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
back_button = wx.Button(self.m_panel,
|
||||||
|
-1,
|
||||||
|
label="Close",
|
||||||
|
size=(WIDTH, BTN_HEIGHT))
|
||||||
|
back_button.Bind(wx.EVT_BUTTON, lambda event: self.Destroy())
|
||||||
|
outer_sizer.Add(back_button, FLAGS)
|
||||||
inner_sizer.Add(play_button, FLAGS)
|
inner_sizer.Add(play_button, FLAGS)
|
||||||
inner_sizer.Add(pause_button, FLAGS)
|
inner_sizer.Add(pause_button, FLAGS)
|
||||||
inner_sizer.Add(back_button, FLAGS)
|
|
||||||
inner_sizer.Add(stop_button, FLAGS)
|
inner_sizer.Add(stop_button, FLAGS)
|
||||||
|
|
||||||
if not self.m_chromecast_thr.is_alive(
|
if self.has_usable_chromecasts():
|
||||||
) and not self.m_selected_chromecast:
|
btm = make_bitmap_from_file(
|
||||||
btm = make_bitmap_from_file('{}/assets/Cast.png'.format(MYPATH), wx.Size(24,24))
|
'{}/assets/Cast.png'.format(self.asset_path), wx.Size(24, 24))
|
||||||
cast_button = wx.BitmapButton(self.m_panel,
|
cast_button = wx.BitmapButton(self.m_panel,
|
||||||
-1,
|
-1,
|
||||||
bitmap=btm,
|
bitmap=btm,
|
||||||
|
@ -112,7 +137,6 @@ class Cast(wx.Frame):
|
||||||
)
|
)
|
||||||
inner_sizer.Add(cast_button, FLAGS)
|
inner_sizer.Add(cast_button, FLAGS)
|
||||||
|
|
||||||
|
|
||||||
if self.m_selected_chromecast:
|
if self.m_selected_chromecast:
|
||||||
self.cast(wx.media.EVT_MEDIA_LOADED, uri),
|
self.cast(wx.media.EVT_MEDIA_LOADED, uri),
|
||||||
play_button.Bind(wx.EVT_BUTTON, self.play_cast)
|
play_button.Bind(wx.EVT_BUTTON, self.play_cast)
|
||||||
|
@ -128,42 +152,75 @@ class Cast(wx.Frame):
|
||||||
play_button.Bind(wx.EVT_BUTTON, self.play)
|
play_button.Bind(wx.EVT_BUTTON, self.play)
|
||||||
pause_button.Bind(wx.EVT_BUTTON, self.pause)
|
pause_button.Bind(wx.EVT_BUTTON, self.pause)
|
||||||
stop_button.Bind(wx.EVT_BUTTON, self.stop)
|
stop_button.Bind(wx.EVT_BUTTON, self.stop)
|
||||||
|
outer_sizer.Add(inner_sizer, FLAGS)
|
||||||
inner_sizer.Fit(self)
|
inner_sizer.Fit(self)
|
||||||
inner_sizer.Layout()
|
outer_sizer.Fit(self)
|
||||||
|
outer_sizer.Layout()
|
||||||
|
|
||||||
return inner_sizer
|
return outer_sizer
|
||||||
|
|
||||||
def get_providers(self) -> list[ChannelProvider]:
|
def get_providers(self) -> list[ChannelProvider]:
|
||||||
|
|
||||||
providers = list()
|
providers = list()
|
||||||
svt = ChannelProvider(
|
chandict = {
|
||||||
"SVT",
|
"kanaler": {
|
||||||
channels=[
|
"channels": [
|
||||||
SVT.SVT("feed"),
|
"ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen",
|
||||||
SVT.SVT("ch-svt1"),
|
"ch-kunskapskanalen"
|
||||||
SVT.SVT("ch-svt2"),
|
],
|
||||||
SVT.SVT("ch-svt24"),
|
"displayname":
|
||||||
SVT.SVT("ch-barnkanalen"),
|
"SVT Channel Streams"
|
||||||
SVT.SVT("ch-kunskapskanalen"),
|
}
|
||||||
],
|
}
|
||||||
)
|
chandict["program"] = {
|
||||||
|
"channels": ["feed", "allprograms"],
|
||||||
|
"displayname": "SVT Shows - Latest and A-Ö"
|
||||||
|
}
|
||||||
|
|
||||||
providers.append(svt)
|
categories = list()
|
||||||
|
for category in get_all_svt_categories():
|
||||||
|
categories.append(category['id'])
|
||||||
|
chandict["kategorier"] = {
|
||||||
|
"channels": categories,
|
||||||
|
"displayname": "SVT All Categories"
|
||||||
|
}
|
||||||
|
for provider in ["kanaler", "program", "kategorier"]:
|
||||||
|
channels = list()
|
||||||
|
for id in chandict[provider]["channels"]:
|
||||||
|
channels.append(SVT.SVT(id))
|
||||||
|
svt = ChannelProvider(chandict[provider]["displayname"],
|
||||||
|
channels=channels)
|
||||||
|
providers.append(svt)
|
||||||
|
|
||||||
return providers
|
return providers
|
||||||
|
|
||||||
|
def has_usable_chromecasts(self) -> bool:
|
||||||
|
if self.m_chromecast_thr.is_alive():
|
||||||
|
return False
|
||||||
|
if self.m_selected_chromecast:
|
||||||
|
return False
|
||||||
|
result = False
|
||||||
|
for cast in self.m_chromecasts:
|
||||||
|
if cast.cast_type != 'audio':
|
||||||
|
result = True
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
def show_channel_list(self, _, provider_index) -> None:
|
def show_channel_list(self, _, provider_index) -> None:
|
||||||
self.m_selected_provider_index = provider_index
|
self.m_selected_provider_index = provider_index
|
||||||
self.m_selected_provider = self.m_providers[provider_index]
|
self.m_selected_provider = self.m_providers[provider_index]
|
||||||
self.m_sizer.Clear(delete_windows=True)
|
self.m_sizer.Clear(delete_windows=True)
|
||||||
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
|
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
closebtn = wx.Button(self.m_panel,
|
if len(self.m_providers) > 1:
|
||||||
-1,
|
back_callback = lambda event: self.show_provider_list(event)
|
||||||
label="Close",
|
self.add_back_button(back_callback)
|
||||||
size=(WIDTH, BTN_HEIGHT))
|
else:
|
||||||
closebtn.Bind(wx.EVT_BUTTON, lambda event: self.Destroy())
|
closebtn = wx.Button(self.m_panel,
|
||||||
self.m_sizer.Add(closebtn, 0, wx.ALL, 1)
|
-1,
|
||||||
|
label="Close",
|
||||||
|
size=(WIDTH, BTN_HEIGHT))
|
||||||
|
closebtn.Bind(wx.EVT_BUTTON, lambda event: self.Destroy())
|
||||||
|
self.m_sizer.Add(closebtn, 0, wx.ALL, 1)
|
||||||
|
|
||||||
channel_index = 0
|
channel_index = 0
|
||||||
|
|
||||||
|
@ -182,6 +239,30 @@ class Cast(wx.Frame):
|
||||||
self.m_sizer.Fit(self)
|
self.m_sizer.Fit(self)
|
||||||
self.m_sizer.Layout()
|
self.m_sizer.Layout()
|
||||||
|
|
||||||
|
def show_provider_list(self, _) -> None:
|
||||||
|
self.m_sizer.Clear(delete_windows=True)
|
||||||
|
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
closebtn = wx.Button(self.m_panel,
|
||||||
|
-1,
|
||||||
|
label="Close",
|
||||||
|
size=(WIDTH, BTN_HEIGHT))
|
||||||
|
closebtn.Bind(wx.EVT_BUTTON, lambda event: self.Destroy())
|
||||||
|
self.m_sizer.Add(closebtn, 0, wx.ALL, 1)
|
||||||
|
provider_index = 0
|
||||||
|
for provider in self.m_providers:
|
||||||
|
bitmap = provider.get_logo_as_bitmap()
|
||||||
|
callback = lambda event, pindex=provider_index: self.show_channel_list(
|
||||||
|
event, pindex)
|
||||||
|
btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel, bitmap,
|
||||||
|
provider.get_name(),
|
||||||
|
callback)
|
||||||
|
self.m_sizer.Add(btn_sizer, 0, wx.ALL, 1)
|
||||||
|
provider_index += 1
|
||||||
|
self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True)
|
||||||
|
self.m_panel.SetSizer(self.m_sizer)
|
||||||
|
self.m_sizer.Fit(self)
|
||||||
|
self.m_sizer.Layout()
|
||||||
|
|
||||||
def show_video_list(self, _, channel_index) -> None:
|
def show_video_list(self, _, channel_index) -> None:
|
||||||
self.SetWindowStyle(self.m_style)
|
self.SetWindowStyle(self.m_style)
|
||||||
self.m_selected_channel = self.m_selected_provider.get_channel_by_index(
|
self.m_selected_channel = self.m_selected_provider.get_channel_by_index(
|
||||||
|
@ -191,7 +272,11 @@ class Cast(wx.Frame):
|
||||||
back_callback = lambda event: self.show_channel_list(
|
back_callback = lambda event: self.show_channel_list(
|
||||||
event, self.m_selected_provider_index)
|
event, self.m_selected_provider_index)
|
||||||
self.add_back_button(back_callback)
|
self.add_back_button(back_callback)
|
||||||
if self.m_selected_provider.get_name() == 'SVT' and self.m_selected_channel.get_id() == 'feed':
|
if self.m_selected_provider.get_name().startswith(
|
||||||
|
'SVT') and self.m_selected_channel.get_id() not in [
|
||||||
|
"ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen",
|
||||||
|
"ch-kunskapskanalen"
|
||||||
|
]:
|
||||||
|
|
||||||
def refresh_callback(event):
|
def refresh_callback(event):
|
||||||
self.m_selected_channel.refresh()
|
self.m_selected_channel.refresh()
|
||||||
|
@ -206,9 +291,13 @@ class Cast(wx.Frame):
|
||||||
|
|
||||||
if self.m_selected_channel.wait():
|
if self.m_selected_channel.wait():
|
||||||
with wx.BusyInfo("Please wait, working..."):
|
with wx.BusyInfo("Please wait, working..."):
|
||||||
|
number_of_waits = 0
|
||||||
while self.m_selected_channel.wait():
|
while self.m_selected_channel.wait():
|
||||||
|
number_of_waits += 1
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
wx.GetApp().Yield()
|
wx.GetApp().Yield()
|
||||||
|
if number_of_waits > 10:
|
||||||
|
break
|
||||||
|
|
||||||
btnindex = 0
|
btnindex = 0
|
||||||
for item in self.m_selected_channel.get_items(): # type: ignore
|
for item in self.m_selected_channel.get_items(): # type: ignore
|
||||||
|
@ -271,8 +360,10 @@ class Cast(wx.Frame):
|
||||||
:param uri str: the link to the video stream
|
:param uri str: the link to the video stream
|
||||||
"""
|
"""
|
||||||
media = self.m_vlc.media_new(uri)
|
media = self.m_vlc.media_new(uri)
|
||||||
self.m_vlc_medialist.add_media(media)
|
medialist = self.m_vlc.media_list_new()
|
||||||
self.m_vlc_listplayer.set_media_list(self.m_vlc_medialist)
|
|
||||||
|
medialist.add_media(media)
|
||||||
|
self.m_vlc_listplayer.set_media_list(medialist)
|
||||||
|
|
||||||
self.m_sizer.Clear(delete_windows=True)
|
self.m_sizer.Clear(delete_windows=True)
|
||||||
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
|
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
@ -298,7 +389,13 @@ class Cast(wx.Frame):
|
||||||
self.m_sizer.Add(cancel_btn)
|
self.m_sizer.Add(cancel_btn)
|
||||||
|
|
||||||
for cast in self.m_chromecasts:
|
for cast in self.m_chromecasts:
|
||||||
friendly_name = cast.device.friendly_name
|
if cast.cast_type == 'audio':
|
||||||
|
continue
|
||||||
|
friendly_name = "Unknown Chromecast"
|
||||||
|
try:
|
||||||
|
friendly_name = cast.device.friendly_name
|
||||||
|
except AttributeError:
|
||||||
|
friendly_name = cast.cast_info.friendly_name
|
||||||
btn = wx.Button(self.m_panel,
|
btn = wx.Button(self.m_panel,
|
||||||
id=-1,
|
id=-1,
|
||||||
label=friendly_name,
|
label=friendly_name,
|
||||||
|
@ -388,7 +485,12 @@ class Cast(wx.Frame):
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# When this module is run (not imported) then create the app, the
|
# When this module is run (not imported) then create the app, the
|
||||||
# frame, show it, and start the event loop.
|
# frame, show it, and start the event loop.
|
||||||
|
url = None
|
||||||
app: wx.App = wx.App()
|
app: wx.App = wx.App()
|
||||||
frm: Cast = Cast(None, title="Cast")
|
if len(sys.argv) > 1:
|
||||||
|
url = sys.argv[1]
|
||||||
|
frm: Cast = Cast(None, title="Cast", url=url)
|
||||||
|
else:
|
||||||
|
frm: Cast = Cast(None, title="Cast")
|
||||||
frm.Show()
|
frm.Show()
|
||||||
app.MainLoop()
|
app.MainLoop()
|
||||||
|
|
12
setup.py
|
@ -5,10 +5,17 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="cast",
|
name="cast",
|
||||||
version="0.0.1",
|
version="0.1.1",
|
||||||
author="Micke Nordin",
|
author="Micke Nordin",
|
||||||
author_email="hej@mic.ke",
|
author_email="hej@mic.ke",
|
||||||
data_files = [('share/applications', ['data/org.smolnet.cast.desktop']),],
|
data_files=[('share/applications', ['data/org.smolnet.cast.desktop']),
|
||||||
|
('share/cast/assets', [
|
||||||
|
'data/assets/Barnkanalen.png', 'data/assets/Cast.png',
|
||||||
|
'data/assets/Default.png',
|
||||||
|
'data/assets/Kunskapskanalen.png', 'data/assets/SVT1.png',
|
||||||
|
'data/assets/SVT24.png', 'data/assets/SVT2.png',
|
||||||
|
'data/assets/SVT.png', 'data/assets/YouTube.png'
|
||||||
|
])],
|
||||||
description="A Public Service video player.",
|
description="A Public Service video player.",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
@ -26,4 +33,3 @@ setuptools.setup(
|
||||||
python_requires=">=3.9",
|
python_requires=">=3.9",
|
||||||
scripts=["scripts/cast"],
|
scripts=["scripts/cast"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -6,17 +6,19 @@ from datetime import datetime
|
||||||
import feedparser
|
import feedparser
|
||||||
import requests
|
import requests
|
||||||
import wx
|
import wx
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from Channel import Channel
|
from Channel import Channel
|
||||||
from Items import Item
|
from Items import Item
|
||||||
from Utils import (add_video, hash_string, make_bitmap_from_url,
|
from Utils import (add_video, get_all_svt_categories, get_all_svt_programs,
|
||||||
|
get_svt_category, get_svt_id, get_svt_thumb_from_id_changed,
|
||||||
|
get_svt_thumbnail, hash_string, make_bitmap_from_url,
|
||||||
resolve_svt_channel, video_exists)
|
resolve_svt_channel, video_exists)
|
||||||
|
|
||||||
default_rss_url = 'http://www.svtplay.se/rss.xml'
|
default_rss_url = 'http://www.svtplay.se/rss.xml'
|
||||||
|
|
||||||
|
|
||||||
class SVT(Channel):
|
class SVT(Channel):
|
||||||
|
|
||||||
def __init__(self, channel_id: str) -> None:
|
def __init__(self, channel_id: str) -> None:
|
||||||
chan_dict = resolve_svt_channel(channel_id)
|
chan_dict = resolve_svt_channel(channel_id)
|
||||||
logo = chan_dict['thumbnail']
|
logo = chan_dict['thumbnail']
|
||||||
|
@ -24,7 +26,6 @@ class SVT(Channel):
|
||||||
super().__init__(channel_id, 'SVT', default_rss_url, logo, name)
|
super().__init__(channel_id, 'SVT', default_rss_url, logo, name)
|
||||||
|
|
||||||
def parse_feed(self) -> None:
|
def parse_feed(self) -> None:
|
||||||
#self.m_items: list[Item] = list()
|
|
||||||
resolved_link = str()
|
resolved_link = str()
|
||||||
description = str()
|
description = str()
|
||||||
title = str()
|
title = str()
|
||||||
|
@ -32,8 +33,11 @@ class SVT(Channel):
|
||||||
thumbnail: wx.Bitmap = wx.Bitmap()
|
thumbnail: wx.Bitmap = wx.Bitmap()
|
||||||
published_parsed: datetime = datetime.now()
|
published_parsed: datetime = datetime.now()
|
||||||
video_id = str()
|
video_id = str()
|
||||||
|
categories: dict = dict()
|
||||||
|
for category in get_all_svt_categories():
|
||||||
|
categories[category['id']] = category
|
||||||
|
|
||||||
if self.m_id == 'feed' :
|
if self.m_id == 'feed':
|
||||||
feed = feedparser.parse(self.get_feed())
|
feed = feedparser.parse(self.get_feed())
|
||||||
entries = feed['entries']
|
entries = feed['entries']
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
|
@ -46,16 +50,7 @@ class SVT(Channel):
|
||||||
thumbnail_link = str(link['href'])
|
thumbnail_link = str(link['href'])
|
||||||
|
|
||||||
break
|
break
|
||||||
page = requests.get(str(entry['link']))
|
svt_id = get_svt_id(str(entry['link']))
|
||||||
soup = BeautifulSoup(page.text, 'html.parser')
|
|
||||||
|
|
||||||
for element in soup.find_all('a'):
|
|
||||||
href = element.get('href')
|
|
||||||
datart = element.get('data-rt')
|
|
||||||
|
|
||||||
if datart == 'top-area-play-button':
|
|
||||||
svt_id = href.split('=')[1].split('&')[0]
|
|
||||||
|
|
||||||
resolved_link = self.resolve_link(svt_id)
|
resolved_link = self.resolve_link(svt_id)
|
||||||
description = str(entry['description'])
|
description = str(entry['description'])
|
||||||
published_parsed = entry['published_parsed']
|
published_parsed = entry['published_parsed']
|
||||||
|
@ -63,12 +58,81 @@ class SVT(Channel):
|
||||||
if not resolved_link:
|
if not resolved_link:
|
||||||
continue
|
continue
|
||||||
thumbnail = make_bitmap_from_url(
|
thumbnail = make_bitmap_from_url(
|
||||||
thumbnail_link, wx.Size(int(self.m_screen_width), 150))
|
thumbnail_link, wx.Size(int(self.m_screen_width), 150), video_id)
|
||||||
item = Item(description, resolved_link, self.m_provider_name,
|
item = Item(description, resolved_link, self.m_provider_name,
|
||||||
published_parsed, thumbnail, title)
|
published_parsed, thumbnail, title)
|
||||||
self.m_items.append(item)
|
self.m_items.append(item)
|
||||||
add_video(video_id, self.m_id, self.m_provider_name, description,
|
add_video(video_id, self.m_id, self.m_provider_name,
|
||||||
resolved_link, published_parsed, thumbnail, title, 0)
|
description, resolved_link, published_parsed,
|
||||||
|
thumbnail, title, 0)
|
||||||
|
|
||||||
|
elif self.m_id == 'allprograms':
|
||||||
|
entries = get_all_svt_programs()
|
||||||
|
for entry in entries:
|
||||||
|
url = entry['item']['urls']['svtplay']
|
||||||
|
video_id = hash_string(url.split('/')[1])
|
||||||
|
svt_id = str()
|
||||||
|
link = "https://svtplay.se{}".format(url)
|
||||||
|
if video_exists(video_id, 'allprograms'):
|
||||||
|
continue
|
||||||
|
thumbnail_link = get_svt_thumbnail(link)
|
||||||
|
|
||||||
|
svt_id = get_svt_id(link)
|
||||||
|
resolved_link = self.resolve_link(svt_id)
|
||||||
|
title = str(entry['heading'])
|
||||||
|
type = str(entry['item']['__typename'])
|
||||||
|
published_parsed = datetime.now()
|
||||||
|
swe_only = "no"
|
||||||
|
if entry['item']['restrictions']['onlyAvailableInSweden']:
|
||||||
|
swe_only = 'yes'
|
||||||
|
description = "{}\nOnly available in Sweden:{}\nType of program:{}".format(
|
||||||
|
title, swe_only, type)
|
||||||
|
if not resolved_link:
|
||||||
|
continue
|
||||||
|
thumbnail = make_bitmap_from_url(
|
||||||
|
thumbnail_link, wx.Size(int(self.m_screen_width), 150), video_id)
|
||||||
|
item = Item(description,
|
||||||
|
resolved_link,
|
||||||
|
self.m_provider_name,
|
||||||
|
published_parsed,
|
||||||
|
thumbnail,
|
||||||
|
title,
|
||||||
|
sort_key=entry['selection_name'])
|
||||||
|
self.m_items.append(item)
|
||||||
|
add_video(video_id, self.m_id, self.m_provider_name,
|
||||||
|
description, resolved_link, published_parsed,
|
||||||
|
thumbnail, title, 0)
|
||||||
|
elif self.m_id in categories.keys():
|
||||||
|
entries = get_svt_category(self.m_id)
|
||||||
|
for entry in entries:
|
||||||
|
elem = entry["item"]
|
||||||
|
imgformat = "wide"
|
||||||
|
if not "wide" in elem["images"].keys():
|
||||||
|
if "cleanWide" in elem["images"].keys():
|
||||||
|
imgformat = "cleanWide"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
url = elem["urls"]["svtplay"]
|
||||||
|
video_id = hash_string(url)
|
||||||
|
svt_id = elem["videoSvtId"]
|
||||||
|
resolved_link = self.resolve_link(svt_id)
|
||||||
|
if not resolved_link:
|
||||||
|
continue
|
||||||
|
title = str(entry['heading'])
|
||||||
|
description = str(entry["description"])
|
||||||
|
published_parsed = datetime.now()
|
||||||
|
thumbnail_link = get_svt_thumb_from_id_changed(
|
||||||
|
elem['images'][imgformat]["id"],
|
||||||
|
elem['images'][imgformat]["changed"],
|
||||||
|
size=self.m_screen_width)
|
||||||
|
thumbnail = make_bitmap_from_url(
|
||||||
|
thumbnail_link, wx.Size(int(self.m_screen_width), 150), video_id)
|
||||||
|
item = Item(description, resolved_link, self.m_provider_name,
|
||||||
|
published_parsed, thumbnail, title)
|
||||||
|
self.m_items.append(item)
|
||||||
|
add_video(video_id, self.m_id, self.m_provider_name,
|
||||||
|
description, resolved_link, published_parsed,
|
||||||
|
thumbnail, title, 0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
chan_dict = resolve_svt_channel(self.m_id)
|
chan_dict = resolve_svt_channel(self.m_id)
|
||||||
|
@ -83,12 +147,10 @@ class SVT(Channel):
|
||||||
published_parsed, thumbnail, title)
|
published_parsed, thumbnail, title)
|
||||||
self.m_items.append(item)
|
self.m_items.append(item)
|
||||||
|
|
||||||
|
def resolve_link(self, svt_id) -> str:
|
||||||
def resolve_link(self,svt_id) -> str:
|
|
||||||
url = 'https://api.svt.se/video/{}'.format(svt_id)
|
url = 'https://api.svt.se/video/{}'.format(svt_id)
|
||||||
print(url)
|
print(url)
|
||||||
api = json.loads(
|
api = json.loads(requests.get(url).text)
|
||||||
requests.get(url).text)
|
|
||||||
resolved_link = ''
|
resolved_link = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -98,4 +160,3 @@ class SVT(Channel):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return resolved_link
|
return resolved_link
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#/usr/bin/env python3
|
#/usr/bin/env python3
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
import wx
|
import wx
|
||||||
|
@ -16,7 +15,7 @@ class ChannelProvider:
|
||||||
self.m_id = providerid
|
self.m_id = providerid
|
||||||
self.m_logo: wx.Bitmap = get_default_logo(providerid)
|
self.m_logo: wx.Bitmap = get_default_logo(providerid)
|
||||||
self.m_channels: list[Channel] = channels
|
self.m_channels: list[Channel] = channels
|
||||||
if len(self.m_channels) > 0 and not providerid == 'SVT':
|
if len(self.m_channels) > 0 and not providerid.startswith('SVT'):
|
||||||
self.m_thr = threading.Thread(target=self.make_latest,
|
self.m_thr = threading.Thread(target=self.make_latest,
|
||||||
args=(),
|
args=(),
|
||||||
kwargs={})
|
kwargs={})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import wx
|
import wx
|
||||||
|
|
||||||
|
@ -12,7 +13,9 @@ class Item(dict):
|
||||||
published: datetime,
|
published: datetime,
|
||||||
thumbnail: wx.Bitmap,
|
thumbnail: wx.Bitmap,
|
||||||
title: str,
|
title: str,
|
||||||
watchtime: int = 0):
|
watchtime: int = 0,
|
||||||
|
sort_key: Union[str, None] = None,
|
||||||
|
):
|
||||||
self.__dict__['description'] = description
|
self.__dict__['description'] = description
|
||||||
self.__dict__['link'] = link
|
self.__dict__['link'] = link
|
||||||
self.__dict__['provider_id'] = provider_id
|
self.__dict__['provider_id'] = provider_id
|
||||||
|
@ -20,6 +23,7 @@ class Item(dict):
|
||||||
self.__dict__['thumbnail'] = thumbnail
|
self.__dict__['thumbnail'] = thumbnail
|
||||||
self.__dict__['title'] = title
|
self.__dict__['title'] = title
|
||||||
self.__dict__['watchtime'] = watchtime
|
self.__dict__['watchtime'] = watchtime
|
||||||
|
self.__dict__['sort_key'] = sort_key
|
||||||
|
|
||||||
def __setitem__(self, key, item):
|
def __setitem__(self, key, item):
|
||||||
self.__dict__[key] = item
|
self.__dict__[key] = item
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -9,18 +10,20 @@ from typing import Callable, Union
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import wx
|
import wx
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from Items import Item
|
from Items import Item
|
||||||
|
|
||||||
HEIGHT = int(1440 / 2)
|
HEIGHT = int(1440 / 2)
|
||||||
BTN_HEIGHT = 40
|
BTN_HEIGHT = 40
|
||||||
SIZE = wx.Size(68,100)
|
SIZE = wx.Size(68, 100)
|
||||||
MYPATH = path.dirname(path.abspath(__file__))
|
|
||||||
SCREEN_WIDTH = int(720 / 2)
|
SCREEN_WIDTH = int(720 / 2)
|
||||||
BASEPATH = path.join(str(environ.get("HOME")), '.config/cast')
|
BASEPATH = path.join(str(environ.get("HOME")), '.config/cast')
|
||||||
DB_FILE_NAME = 'cast.db'
|
DB_FILE_NAME = 'cast.db'
|
||||||
SUB_TABLE = 'subscriptions'
|
SUB_TABLE = 'subscriptions'
|
||||||
VIDEO_TABLE = 'videos'
|
VIDEO_TABLE = 'videos'
|
||||||
|
CAT_CACHE = None
|
||||||
|
CHAN_CACHE = None
|
||||||
|
|
||||||
|
|
||||||
def add_video(video_id: str,
|
def add_video(video_id: str,
|
||||||
|
@ -68,11 +71,99 @@ def add_video(video_id: str,
|
||||||
con.commit()
|
con.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_default_logo(providerid: str = 'default') -> wx.Bitmap:
|
def get_svt_thumb_from_id_changed(id: str,
|
||||||
if providerid == 'SVT':
|
changed: str,
|
||||||
return wx.Bitmap('{}/assets/SVT.png'.format(MYPATH))
|
size: str = "480") -> str:
|
||||||
|
return "https://www.svtstatic.se/image/custom/{}/{}/{}".format(
|
||||||
|
size, id, changed)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_svt_categories() -> list:
|
||||||
|
global CAT_CACHE
|
||||||
|
if CAT_CACHE:
|
||||||
|
categories = CAT_CACHE
|
||||||
else:
|
else:
|
||||||
return wx.Bitmap('{}/assets/Default.png'.format(MYPATH))
|
categories: list = list()
|
||||||
|
url = "https://www.svtplay.se/kategori"
|
||||||
|
data = get_svt_data(url)
|
||||||
|
for entry in data:
|
||||||
|
if 'genres' in entry.keys():
|
||||||
|
categories = entry['genres']
|
||||||
|
break
|
||||||
|
CAT_CACHE = categories
|
||||||
|
return categories
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_svt_channels() -> dict:
|
||||||
|
url = "https://www.svtplay.se/kanaler"
|
||||||
|
result: dict = dict()
|
||||||
|
data = get_svt_data(url)
|
||||||
|
for entry in data:
|
||||||
|
if "channels" in entry:
|
||||||
|
for channel in entry["channels"]:
|
||||||
|
if type(entry["channels"][channel]) == type(list()):
|
||||||
|
for item in entry["channels"][channel]:
|
||||||
|
if item["__typename"] == "Channel" and "running" in item:
|
||||||
|
result[item["id"]] = {
|
||||||
|
"thumbnail":
|
||||||
|
make_bitmap_from_url(
|
||||||
|
get_svt_thumb_from_id_changed(
|
||||||
|
item["running"]["image"]['id'],
|
||||||
|
item["running"]["image"]['changed'],
|
||||||
|
SCREEN_WIDTH),
|
||||||
|
wx.Size(width=SCREEN_WIDTH, height=480)),
|
||||||
|
"name":
|
||||||
|
item["name"]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_svt_category(category: str) -> list:
|
||||||
|
url = 'https://www.svtplay.se/kategori/{}?tab=all'.format(category)
|
||||||
|
data = get_svt_data(url)
|
||||||
|
programs = list()
|
||||||
|
for entry in data:
|
||||||
|
if "categoryPage" in entry.keys():
|
||||||
|
for tab in entry["categoryPage"]["lazyLoadedTabs"]:
|
||||||
|
if "selections" in tab.keys():
|
||||||
|
for selection in tab["selections"]:
|
||||||
|
programs += selection['items']
|
||||||
|
break
|
||||||
|
return programs
|
||||||
|
|
||||||
|
|
||||||
|
def get_svt_data(url: str) -> list:
|
||||||
|
result: list = list()
|
||||||
|
res = requests.get(url)
|
||||||
|
soup = BeautifulSoup(res.text, features="lxml")
|
||||||
|
data = json.loads(soup.find(
|
||||||
|
id="__NEXT_DATA__").string)["props"]["urqlState"] # type: ignore
|
||||||
|
for key in data.keys():
|
||||||
|
result.append(json.loads(data[key]["data"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_svt_programs() -> list:
|
||||||
|
url = 'https://www.svtplay.se/program'
|
||||||
|
data = get_svt_data(url)
|
||||||
|
programs = list()
|
||||||
|
for entry in data:
|
||||||
|
if "programAtillO" in entry.keys():
|
||||||
|
for selection in entry["programAtillO"]["selections"]:
|
||||||
|
for item in selection["items"]:
|
||||||
|
item['selection_name'] = selection['name']
|
||||||
|
programs.append(item)
|
||||||
|
break
|
||||||
|
|
||||||
|
return programs
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_logo(providerid: str = 'default',
|
||||||
|
path: str = '/usr/share/cast') -> wx.Bitmap:
|
||||||
|
if providerid.startswith('SVT'):
|
||||||
|
return wx.Bitmap('{}/assets/SVT.png'.format(path))
|
||||||
|
else:
|
||||||
|
return wx.Bitmap('{}/assets/Default.png'.format(path))
|
||||||
|
|
||||||
|
|
||||||
def get_latest(provider_id: str,
|
def get_latest(provider_id: str,
|
||||||
|
@ -141,6 +232,29 @@ def get_subscriptions(basepath: str = BASEPATH,
|
||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
||||||
|
|
||||||
|
def get_svt_id(link: str) -> str:
|
||||||
|
svt_id = str()
|
||||||
|
page = requests.get(link)
|
||||||
|
soup = BeautifulSoup(page.text, 'html.parser')
|
||||||
|
|
||||||
|
for element in soup.find_all('a'):
|
||||||
|
href = element.get('href')
|
||||||
|
datart = element.get('data-rt')
|
||||||
|
|
||||||
|
if datart == 'top-area-play-button':
|
||||||
|
svt_id = href.split('=')[1].split('&')[0]
|
||||||
|
return svt_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_svt_thumbnail(link: str) -> str:
|
||||||
|
page = requests.get(link)
|
||||||
|
soup = BeautifulSoup(page.text, 'html.parser')
|
||||||
|
meta = soup.find(property="og:image")
|
||||||
|
image_link = meta["content"] #type: ignore
|
||||||
|
|
||||||
|
return image_link # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def get_videos(channel_id: str,
|
def get_videos(channel_id: str,
|
||||||
basepath: str = BASEPATH,
|
basepath: str = BASEPATH,
|
||||||
filename: str = DB_FILE_NAME) -> list[Item]:
|
filename: str = DB_FILE_NAME) -> list[Item]:
|
||||||
|
@ -182,7 +296,7 @@ def hash_string(string: str) -> str:
|
||||||
def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
|
def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
|
||||||
str],
|
str],
|
||||||
text: str, callback: Callable) -> wx.BoxSizer:
|
text: str, callback: Callable) -> wx.BoxSizer:
|
||||||
btn_sizer = wx.StaticBoxSizer(wx.HORIZONTAL,parent_pnl)
|
btn_sizer = wx.StaticBoxSizer(wx.HORIZONTAL, parent_pnl)
|
||||||
if type(bitmap_or_str) == type(str):
|
if type(bitmap_or_str) == type(str):
|
||||||
if bitmap_or_str.startswith('http'): # type: ignore
|
if bitmap_or_str.startswith('http'): # type: ignore
|
||||||
bitmap = make_bitmap_from_url(bitmap_or_str) # type: ignore
|
bitmap = make_bitmap_from_url(bitmap_or_str) # type: ignore
|
||||||
|
@ -195,7 +309,7 @@ def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
|
||||||
wx.ID_ANY,
|
wx.ID_ANY,
|
||||||
bitmap,
|
bitmap,
|
||||||
style=btn_style,
|
style=btn_style,
|
||||||
size=wx.Size(100,68))
|
size=wx.Size(100, 68))
|
||||||
btn_logo.SetToolTip(text)
|
btn_logo.SetToolTip(text)
|
||||||
btn_sizer.Add(btn_logo, 0, wx.EXPAND, 1)
|
btn_sizer.Add(btn_logo, 0, wx.EXPAND, 1)
|
||||||
|
|
||||||
|
@ -203,8 +317,7 @@ def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
|
||||||
wx.ID_ANY,
|
wx.ID_ANY,
|
||||||
text,
|
text,
|
||||||
style=wx.BORDER_NONE | wx.BU_AUTODRAW,
|
style=wx.BORDER_NONE | wx.BU_AUTODRAW,
|
||||||
size=wx.Size(SCREEN_WIDTH - 100,
|
size=wx.Size(SCREEN_WIDTH - 100, SIZE.GetHeight()))
|
||||||
SIZE.GetHeight()))
|
|
||||||
btn_text.SetToolTip(text)
|
btn_text.SetToolTip(text)
|
||||||
btn_sizer.Add(btn_text, 0, wx.EXPAND, 1)
|
btn_sizer.Add(btn_text, 0, wx.EXPAND, 1)
|
||||||
parent_pnl.Bind(wx.EVT_BUTTON, callback, btn_logo)
|
parent_pnl.Bind(wx.EVT_BUTTON, callback, btn_logo)
|
||||||
|
@ -213,7 +326,16 @@ def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
|
||||||
return btn_sizer
|
return btn_sizer
|
||||||
|
|
||||||
|
|
||||||
def make_bitmap_from_url(logo_url: str, size: wx.Size = SIZE) -> wx.Bitmap:
|
def make_bitmap_from_url(logo_url: str,
|
||||||
|
size: wx.Size = SIZE,
|
||||||
|
video_id: str = "") -> wx.Bitmap:
|
||||||
|
if not video_id:
|
||||||
|
video_id = hash_string(logo_url)
|
||||||
|
thumbpath = path.join(BASEPATH, 'thumbnails')
|
||||||
|
thumbnail = path.join(thumbpath, video_id)
|
||||||
|
if path.isfile(thumbnail):
|
||||||
|
return make_bitmap_from_file(thumbnail, size)
|
||||||
|
|
||||||
res = requests.get(logo_url)
|
res = requests.get(logo_url)
|
||||||
content = res.content
|
content = res.content
|
||||||
content_bytes = io.BytesIO(content)
|
content_bytes = io.BytesIO(content)
|
||||||
|
@ -223,7 +345,12 @@ def make_bitmap_from_url(logo_url: str, size: wx.Size = SIZE) -> wx.Bitmap:
|
||||||
height = image.GetHeight()
|
height = image.GetHeight()
|
||||||
size.SetHeight(int(height / scale_factor))
|
size.SetHeight(int(height / scale_factor))
|
||||||
image.Rescale(size.GetWidth(), size.GetHeight())
|
image.Rescale(size.GetWidth(), size.GetHeight())
|
||||||
return wx.Bitmap(image)
|
bitmap = wx.Bitmap(image)
|
||||||
|
if not path.isdir(thumbpath):
|
||||||
|
makedirs(thumbpath)
|
||||||
|
if not path.isfile(thumbnail):
|
||||||
|
bitmap.SaveFile(thumbnail, wx.BITMAP_TYPE_PNG)
|
||||||
|
return bitmap
|
||||||
|
|
||||||
|
|
||||||
def make_bitmap_from_file(path, size: wx.Size = SIZE) -> wx.Bitmap:
|
def make_bitmap_from_file(path, size: wx.Size = SIZE) -> wx.Bitmap:
|
||||||
|
@ -236,44 +363,34 @@ def make_bitmap_from_file(path, size: wx.Size = SIZE) -> wx.Bitmap:
|
||||||
return wx.Bitmap(image)
|
return wx.Bitmap(image)
|
||||||
|
|
||||||
|
|
||||||
def resolve_svt_channel(svt_id: str) -> dict:
|
def resolve_svt_channel(svt_id: str, path: str = '/usr/share/cast') -> dict:
|
||||||
|
global CHAN_CACHE
|
||||||
|
|
||||||
channels = {
|
if CHAN_CACHE:
|
||||||
"ch-barnkanalen": {
|
channels = CHAN_CACHE
|
||||||
"name":
|
else:
|
||||||
"Barnkanalen",
|
channels = get_all_svt_channels()
|
||||||
"thumbnail":
|
channels["feed"] = {
|
||||||
make_bitmap_from_file('{}/assets/Barnkanalen.png'.format(MYPATH))
|
|
||||||
},
|
|
||||||
"ch-svt1": {
|
|
||||||
"name": "SVT 1",
|
|
||||||
"thumbnail":
|
|
||||||
make_bitmap_from_file('{}/assets/SVT1.png'.format(MYPATH))
|
|
||||||
},
|
|
||||||
"ch-svt2": {
|
|
||||||
"name": "SVT 2",
|
|
||||||
"thumbnail":
|
|
||||||
make_bitmap_from_file('{}/assets/SVT2.png'.format(MYPATH))
|
|
||||||
},
|
|
||||||
"ch-svt24": {
|
|
||||||
"name":
|
|
||||||
"SVT 24",
|
|
||||||
"thumbnail":
|
|
||||||
make_bitmap_from_file('{}/assets/SVT24.png'.format(MYPATH))
|
|
||||||
},
|
|
||||||
"ch-kunskapskanalen": {
|
|
||||||
"name":
|
|
||||||
"Kunskapskanalen",
|
|
||||||
"thumbnail":
|
|
||||||
make_bitmap_from_file(
|
|
||||||
'{}/assets/Kunskapskanalen.png'.format(MYPATH))
|
|
||||||
},
|
|
||||||
"feed": {
|
|
||||||
"name": "Senaste program",
|
"name": "Senaste program",
|
||||||
"thumbnail":
|
"thumbnail":
|
||||||
make_bitmap_from_file('{}/assets/SVT.png'.format(MYPATH))
|
make_bitmap_from_file('{}/assets/SVT.png'.format(path))
|
||||||
},
|
}
|
||||||
}
|
channels["allprograms"] = {
|
||||||
|
"name": "Alla program",
|
||||||
|
"thumbnail":
|
||||||
|
make_bitmap_from_file('{}/assets/SVT.png'.format(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
for category in get_all_svt_categories():
|
||||||
|
channels[category['id']] = {
|
||||||
|
"name":
|
||||||
|
category["name"],
|
||||||
|
"thumbnail":
|
||||||
|
make_bitmap_from_url(
|
||||||
|
get_svt_thumb_from_id_changed(
|
||||||
|
category['image']['id'], category['image']['changed']))
|
||||||
|
}
|
||||||
|
CHAN_CACHE = channels
|
||||||
|
|
||||||
return channels[svt_id]
|
return channels[svt_id]
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
Depends3: python3-bs4, python3-wxgtk4.0, python3-feedparser, python3-vlc, python3-pychromecast
|
Depends3: python3-bs4, python3-wxgtk4.0, python3-wxgtk-media4.0, python3-feedparser, python3-vlc, python3-pychromecast
|
||||||
Debian-Version: 1
|
Debian-Version: 1
|
||||||
|
Package3: cast
|
||||||
|
|