Compare commits

...

20 commits
0.0.1 ... main

Author SHA1 Message Date
Micke Nordin
de02f43529 Update install instructions 2022-07-11 09:21:39 +00:00
b7113b2e44 Change to correct category 2022-07-10 18:33:44 +02:00
0a418e5836 Bump version to 0.1.1 2022-07-10 18:32:08 +02:00
03edf98b86 Better controlls 2022-07-10 18:31:17 +02:00
5a161c7e82 Better caching 2022-07-10 18:09:57 +02:00
60ab02b4ba Various fixes for chromecasts 2022-07-10 15:46:10 +02:00
fc0a26c74e Overwrite playlist when selecting a new video 2022-07-10 15:14:32 +02:00
9857816402 Create cache dir 2022-07-10 15:08:42 +02:00
Micke Nordin
e794b336ee Update 'README.md' 2022-07-08 13:52:48 +00:00
Micke Nordin
ff9ac9fb6c
Rename package python3-cast -> cast 2022-07-08 15:45:07 +02:00
Micke Nordin
733f8acee5
Bump version to 0.1.0 2022-07-08 15:44:06 +02:00
Micke Nordin
a5a2a5abb8
Cache alot more so now it is usable
Fixes: #4
2022-07-08 15:42:40 +02:00
Micke Nordin
165c98175a
Add back provider splash 2022-07-08 14:45:01 +02:00
Micke Nordin
0528d20f1d
Code format 2022-07-08 13:21:30 +02:00
Micke Nordin
4f719ac781
Working categories. still not really usable 2022-07-08 13:16:49 +02:00
Micke Nordin
30544ac026
Initial support for all programs A-Ö
This feature is not yet really usable since it takes a really long
time to load all programs sequentially and synchronously.
2022-07-08 11:04:01 +02:00
Micke Nordin
564f64106a
Fix paths for assets 2022-06-30 16:52:18 +02:00
Micke Nordin
ee5aff33c1
Fix paths for assets 2022-06-30 16:38:35 +02:00
6af864c5d6
Add support for running in single video mode 2022-05-27 18:03:07 +02:00
Micke Nordin
d90de29d4f Update 'README.md' 2022-05-27 15:20:51 +00:00
18 changed files with 427 additions and 124 deletions

View file

@ -1,5 +1,18 @@
# 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.
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
```

View file

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 401 B

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -7,5 +7,5 @@ Comment=Video Player
Exec=/usr/bin/cast
Icon=video-single-display-symbolic
Terminal=false
Categories=Security;Utility;
Categories=Video;
Keywords=SVT;

158
scripts/cast Normal file → Executable file
View file

@ -1,6 +1,8 @@
#!/usr/bin/env python3
import sys
import threading
import time
import os
from typing import Callable
import pychromecast
@ -11,7 +13,8 @@ from vlc import Instance
from Channel import SVT
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)
HEIGHT = int(1440 / 2)
@ -27,12 +30,23 @@ FLAGS.Center()
class Cast(wx.Frame):
def __init__(self, *args, **kw):
"""__init__.
:param args:
: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)
if not os.path.isdir(BASEPATH):
os.mkdir(BASEPATH)
self.m_selected_chromecast = None
self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH)
self.m_style = self.GetWindowStyle()
@ -40,7 +54,6 @@ class Cast(wx.Frame):
args=(),
kwargs={})
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_chromecast_thr.start()
self.m_sizer: wx.Sizer = wx.BoxSizer(wx.VERTICAL)
@ -49,6 +62,11 @@ class Cast(wx.Frame):
self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True)
self.m_panel.SetSizer(self.m_sizer)
self.m_providers: list[ChannelProvider] = self.get_providers()
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:
@ -68,6 +86,7 @@ class Cast(wx.Frame):
self.m_chromecasts, self.m_browser = pychromecast.get_chromecasts()
def get_player_controls(self, channel_index: int, uri: str) -> wx.BoxSizer:
outer_sizer = wx.BoxSizer(wx.VERTICAL)
inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
play_button = wx.Button(self.m_panel,
-1,
@ -79,28 +98,34 @@ class Cast(wx.Frame):
"Pause",
size=(WIDTH / 4, BTN_HEIGHT))
stop_button = wx.Button(self.m_panel,
-1,
"Stop",
size=(WIDTH / 4, BTN_HEIGHT))
if self.m_mode == 'normal':
back_button = wx.Button(self.m_panel,
-1,
"Back",
size=(WIDTH / 4, BTN_HEIGHT))
"Go Back",
size=(WIDTH, 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,
else:
back_button = wx.Button(self.m_panel,
-1,
"Stop",
size=(WIDTH / 4, BTN_HEIGHT))
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(pause_button, FLAGS)
inner_sizer.Add(back_button, FLAGS)
inner_sizer.Add(stop_button, FLAGS)
if not self.m_chromecast_thr.is_alive(
) and not self.m_selected_chromecast:
btm = make_bitmap_from_file('{}/assets/Cast.png'.format(MYPATH), wx.Size(24,24))
if self.has_usable_chromecasts():
btm = make_bitmap_from_file(
'{}/assets/Cast.png'.format(self.asset_path), wx.Size(24, 24))
cast_button = wx.BitmapButton(self.m_panel,
-1,
bitmap=btm,
@ -112,7 +137,6 @@ class Cast(wx.Frame):
)
inner_sizer.Add(cast_button, FLAGS)
if self.m_selected_chromecast:
self.cast(wx.media.EVT_MEDIA_LOADED, uri),
play_button.Bind(wx.EVT_BUTTON, self.play_cast)
@ -128,36 +152,69 @@ class Cast(wx.Frame):
play_button.Bind(wx.EVT_BUTTON, self.play)
pause_button.Bind(wx.EVT_BUTTON, self.pause)
stop_button.Bind(wx.EVT_BUTTON, self.stop)
outer_sizer.Add(inner_sizer, FLAGS)
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]:
providers = list()
svt = ChannelProvider(
"SVT",
channels=[
SVT.SVT("feed"),
SVT.SVT("ch-svt1"),
SVT.SVT("ch-svt2"),
SVT.SVT("ch-svt24"),
SVT.SVT("ch-barnkanalen"),
SVT.SVT("ch-kunskapskanalen"),
chandict = {
"kanaler": {
"channels": [
"ch-svt1", "ch-svt2", "ch-svt24", "ch-barnkanalen",
"ch-kunskapskanalen"
],
)
"displayname":
"SVT Channel Streams"
}
}
chandict["program"] = {
"channels": ["feed", "allprograms"],
"displayname": "SVT Shows - Latest and A-Ö"
}
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
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:
self.m_selected_provider_index = provider_index
self.m_selected_provider = self.m_providers[provider_index]
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
if len(self.m_providers) > 1:
back_callback = lambda event: self.show_provider_list(event)
self.add_back_button(back_callback)
else:
closebtn = wx.Button(self.m_panel,
-1,
label="Close",
@ -182,6 +239,30 @@ class Cast(wx.Frame):
self.m_sizer.Fit(self)
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:
self.SetWindowStyle(self.m_style)
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(
event, self.m_selected_provider_index)
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):
self.m_selected_channel.refresh()
@ -206,9 +291,13 @@ class Cast(wx.Frame):
if self.m_selected_channel.wait():
with wx.BusyInfo("Please wait, working..."):
number_of_waits = 0
while self.m_selected_channel.wait():
number_of_waits += 1
time.sleep(1)
wx.GetApp().Yield()
if number_of_waits > 10:
break
btnindex = 0
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
"""
media = self.m_vlc.media_new(uri)
self.m_vlc_medialist.add_media(media)
self.m_vlc_listplayer.set_media_list(self.m_vlc_medialist)
medialist = self.m_vlc.media_list_new()
medialist.add_media(media)
self.m_vlc_listplayer.set_media_list(medialist)
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
@ -298,7 +389,13 @@ class Cast(wx.Frame):
self.m_sizer.Add(cancel_btn)
for cast in self.m_chromecasts:
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,
id=-1,
label=friendly_name,
@ -388,7 +485,12 @@ class Cast(wx.Frame):
if __name__ == "__main__":
# When this module is run (not imported) then create the app, the
# frame, show it, and start the event loop.
url = None
app: wx.App = wx.App()
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()
app.MainLoop()

View file

@ -5,10 +5,17 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup(
name="cast",
version="0.0.1",
version="0.1.1",
author="Micke Nordin",
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.",
long_description=long_description,
long_description_content_type="text/markdown",
@ -26,4 +33,3 @@ setuptools.setup(
python_requires=">=3.9",
scripts=["scripts/cast"],
)

View file

@ -6,17 +6,19 @@ from datetime import datetime
import feedparser
import requests
import wx
from bs4 import BeautifulSoup
from Channel import Channel
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)
default_rss_url = 'http://www.svtplay.se/rss.xml'
class SVT(Channel):
def __init__(self, channel_id: str) -> None:
chan_dict = resolve_svt_channel(channel_id)
logo = chan_dict['thumbnail']
@ -24,7 +26,6 @@ class SVT(Channel):
super().__init__(channel_id, 'SVT', default_rss_url, logo, name)
def parse_feed(self) -> None:
#self.m_items: list[Item] = list()
resolved_link = str()
description = str()
title = str()
@ -32,6 +33,9 @@ class SVT(Channel):
thumbnail: wx.Bitmap = wx.Bitmap()
published_parsed: datetime = datetime.now()
video_id = str()
categories: dict = dict()
for category in get_all_svt_categories():
categories[category['id']] = category
if self.m_id == 'feed':
feed = feedparser.parse(self.get_feed())
@ -46,16 +50,7 @@ class SVT(Channel):
thumbnail_link = str(link['href'])
break
page = requests.get(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]
svt_id = get_svt_id(str(entry['link']))
resolved_link = self.resolve_link(svt_id)
description = str(entry['description'])
published_parsed = entry['published_parsed']
@ -63,12 +58,81 @@ class SVT(Channel):
if not resolved_link:
continue
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,
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)
add_video(video_id, self.m_id, self.m_provider_name,
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:
chan_dict = resolve_svt_channel(self.m_id)
@ -83,12 +147,10 @@ class SVT(Channel):
published_parsed, thumbnail, title)
self.m_items.append(item)
def resolve_link(self, svt_id) -> str:
url = 'https://api.svt.se/video/{}'.format(svt_id)
print(url)
api = json.loads(
requests.get(url).text)
api = json.loads(requests.get(url).text)
resolved_link = ''
try:
@ -98,4 +160,3 @@ class SVT(Channel):
except KeyError:
pass
return resolved_link

View file

@ -1,6 +1,5 @@
#/usr/bin/env python3
import threading
import time
from os import path
import wx
@ -16,7 +15,7 @@ class ChannelProvider:
self.m_id = providerid
self.m_logo: wx.Bitmap = get_default_logo(providerid)
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,
args=(),
kwargs={})

View file

@ -1,5 +1,6 @@
#!/usr/bin/env python3
from datetime import datetime
from typing import Union
import wx
@ -12,7 +13,9 @@ class Item(dict):
published: datetime,
thumbnail: wx.Bitmap,
title: str,
watchtime: int = 0):
watchtime: int = 0,
sort_key: Union[str, None] = None,
):
self.__dict__['description'] = description
self.__dict__['link'] = link
self.__dict__['provider_id'] = provider_id
@ -20,6 +23,7 @@ class Item(dict):
self.__dict__['thumbnail'] = thumbnail
self.__dict__['title'] = title
self.__dict__['watchtime'] = watchtime
self.__dict__['sort_key'] = sort_key
def __setitem__(self, key, item):
self.__dict__[key] = item

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python3
import hashlib
import io
import json
import sqlite3
import time
from datetime import datetime
@ -9,18 +10,20 @@ from typing import Callable, Union
import requests
import wx
from bs4 import BeautifulSoup
from Items import Item
HEIGHT = int(1440 / 2)
BTN_HEIGHT = 40
SIZE = wx.Size(68, 100)
MYPATH = path.dirname(path.abspath(__file__))
SCREEN_WIDTH = int(720 / 2)
BASEPATH = path.join(str(environ.get("HOME")), '.config/cast')
DB_FILE_NAME = 'cast.db'
SUB_TABLE = 'subscriptions'
VIDEO_TABLE = 'videos'
CAT_CACHE = None
CHAN_CACHE = None
def add_video(video_id: str,
@ -68,11 +71,99 @@ def add_video(video_id: str,
con.commit()
def get_default_logo(providerid: str = 'default') -> wx.Bitmap:
if providerid == 'SVT':
return wx.Bitmap('{}/assets/SVT.png'.format(MYPATH))
def get_svt_thumb_from_id_changed(id: str,
changed: str,
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:
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,
@ -141,6 +232,29 @@ def get_subscriptions(basepath: str = BASEPATH,
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,
basepath: str = BASEPATH,
filename: str = DB_FILE_NAME) -> list[Item]:
@ -203,8 +317,7 @@ def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
wx.ID_ANY,
text,
style=wx.BORDER_NONE | wx.BU_AUTODRAW,
size=wx.Size(SCREEN_WIDTH - 100,
SIZE.GetHeight()))
size=wx.Size(SCREEN_WIDTH - 100, SIZE.GetHeight()))
btn_text.SetToolTip(text)
btn_sizer.Add(btn_text, 0, wx.EXPAND, 1)
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
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)
content = res.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()
size.SetHeight(int(height / scale_factor))
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:
@ -236,44 +363,34 @@ def make_bitmap_from_file(path, size: wx.Size = SIZE) -> wx.Bitmap:
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 = {
"ch-barnkanalen": {
"name":
"Barnkanalen",
"thumbnail":
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": {
if CHAN_CACHE:
channels = CHAN_CACHE
else:
channels = get_all_svt_channels()
channels["feed"] = {
"name": "Senaste program",
"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]

View file

@ -1,3 +1,4 @@
[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
Package3: cast