Compare commits

...

9 Commits
v0.1.0 ... main

@ -4,11 +4,15 @@ This program is intended for use on linux phones, and allow watching video strea
Currently Swedish Public Service TV is supported. Currently Swedish Public Service TV is supported.
Install like this: 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 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 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 python3-cast 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
```

@ -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;

@ -2,6 +2,7 @@
import sys import sys
import threading import threading
import time import time
import os
from typing import Callable from typing import Callable
import pychromecast import pychromecast
@ -13,7 +14,7 @@ from vlc import Instance
from Channel import SVT from Channel import SVT
from ChannelProvider import ChannelProvider from ChannelProvider import ChannelProvider
from Utils import (get_all_svt_categories, make_bitmap_from_file, from Utils import (get_all_svt_categories, make_bitmap_from_file,
make_sized_button) make_sized_button, BASEPATH)
WIDTH = int(720 / 2) WIDTH = int(720 / 2)
HEIGHT = int(1440 / 2) HEIGHT = int(1440 / 2)
@ -43,6 +44,9 @@ class Cast(wx.Frame):
url = kw['url'] url = kw['url']
del 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()
@ -50,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)
@ -83,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,
@ -101,8 +105,8 @@ class Cast(wx.Frame):
if self.m_mode == 'normal': if self.m_mode == 'normal':
back_button = wx.Button(self.m_panel, back_button = wx.Button(self.m_panel,
-1, -1,
"Back", "Go Back",
size=(WIDTH / 4, BTN_HEIGHT)) size=(WIDTH, BTN_HEIGHT))
back_button.Bind( back_button.Bind(
wx.EVT_BUTTON, wx.EVT_BUTTON,
lambda event, cindex=channel_index: self.show_video_list( lambda event, cindex=channel_index: self.show_video_list(
@ -112,15 +116,14 @@ class Cast(wx.Frame):
back_button = wx.Button(self.m_panel, back_button = wx.Button(self.m_panel,
-1, -1,
label="Close", label="Close",
size=(WIDTH / 4, BTN_HEIGHT)) size=(WIDTH, BTN_HEIGHT))
back_button.Bind(wx.EVT_BUTTON, lambda event: self.Destroy()) 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(stop_button, FLAGS) inner_sizer.Add(stop_button, FLAGS)
inner_sizer.Add(back_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(self.asset_path), 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,
@ -149,10 +152,12 @@ 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]:
@ -183,19 +188,31 @@ class Cast(wx.Frame):
channels = list() channels = list()
for id in chandict[provider]["channels"]: for id in chandict[provider]["channels"]:
channels.append(SVT.SVT(id)) channels.append(SVT.SVT(id))
svt = ChannelProvider(chandict[provider]["displayname"], channels=channels) svt = ChannelProvider(chandict[provider]["displayname"],
channels=channels)
providers.append(svt) 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)
if len(self.m_providers) > 1: if len(self.m_providers) > 1:
back_callback = lambda event: self.show_provider_list( back_callback = lambda event: self.show_provider_list(event)
event)
self.add_back_button(back_callback) self.add_back_button(back_callback)
else: else:
closebtn = wx.Button(self.m_panel, closebtn = wx.Button(self.m_panel,
@ -343,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)
@ -370,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,

@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup( setuptools.setup(
name="cast", name="cast",
version="0.1.0", 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']),

@ -106,8 +106,14 @@ class SVT(Channel):
entries = get_svt_category(self.m_id) entries = get_svt_category(self.m_id)
for entry in entries: for entry in entries:
elem = entry["item"] 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"] url = elem["urls"]["svtplay"]
video_id = hash_string(url.split('/')[1]) video_id = hash_string(url)
svt_id = elem["videoSvtId"] svt_id = elem["videoSvtId"]
resolved_link = self.resolve_link(svt_id) resolved_link = self.resolve_link(svt_id)
if not resolved_link: if not resolved_link:
@ -116,8 +122,8 @@ class SVT(Channel):
description = str(entry["description"]) description = str(entry["description"])
published_parsed = datetime.now() published_parsed = datetime.now()
thumbnail_link = get_svt_thumb_from_id_changed( thumbnail_link = get_svt_thumb_from_id_changed(
elem['images']["wide"]["id"], elem['images'][imgformat]["id"],
elem['images']["wide"]["changed"], elem['images'][imgformat]["changed"],
size=self.m_screen_width) size=self.m_screen_width)
thumbnail = make_bitmap_from_url( thumbnail = make_bitmap_from_url(
thumbnail_link, wx.Size(int(self.m_screen_width), 150), video_id) thumbnail_link, wx.Size(int(self.m_screen_width), 150), video_id)

@ -22,7 +22,8 @@ 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'
USE_CACHED_CATEGORIES = False CAT_CACHE = None
CHAN_CACHE = None
def add_video(video_id: str, def add_video(video_id: str,
@ -77,27 +78,44 @@ def get_svt_thumb_from_id_changed(id: str,
size, id, changed) size, id, changed)
def get_all_svt_categories(basepath=BASEPATH) -> list: def get_all_svt_categories() -> list:
global USE_CACHED_CATEGORIES global CAT_CACHE
categoryfile = path.join(basepath,"categories.json") if CAT_CACHE:
if USE_CACHED_CATEGORIES: categories = CAT_CACHE
if path.isfile(categoryfile): else:
with open(categoryfile, 'r') as jfile: categories: list = list()
return(json.loads(jfile.read())) 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
categories: list = list() def get_all_svt_channels() -> dict:
url = "https://www.svtplay.se/kategori" url = "https://www.svtplay.se/kanaler"
result: dict = dict()
data = get_svt_data(url) data = get_svt_data(url)
for entry in data: for entry in data:
if 'genres' in entry.keys(): if "channels" in entry:
categories = entry['genres'] for channel in entry["channels"]:
break if type(entry["channels"][channel]) == type(list()):
with open(categoryfile, 'w') as jfile: for item in entry["channels"][channel]:
jfile.write(json.dumps(categories)) if item["__typename"] == "Channel" and "running" in item:
USE_CACHED_CATEGORIES = True result[item["id"]] = {
return categories "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: def get_svt_category(category: str) -> list:
@ -118,8 +136,8 @@ def get_svt_data(url: str) -> list:
result: list = list() result: list = list()
res = requests.get(url) res = requests.get(url)
soup = BeautifulSoup(res.text, features="lxml") soup = BeautifulSoup(res.text, features="lxml")
data = json.loads( data = json.loads(soup.find(
soup.find(id="__NEXT_DATA__").string)["props"]["urqlState"] id="__NEXT_DATA__").string)["props"]["urqlState"] # type: ignore
for key in data.keys(): for key in data.keys():
result.append(json.loads(data[key]["data"])) result.append(json.loads(data[key]["data"]))
return result return result
@ -232,9 +250,9 @@ def get_svt_thumbnail(link: str) -> str:
page = requests.get(link) page = requests.get(link)
soup = BeautifulSoup(page.text, 'html.parser') soup = BeautifulSoup(page.text, 'html.parser')
meta = soup.find(property="og:image") meta = soup.find(property="og:image")
image_link = meta["content"] image_link = meta["content"] #type: ignore
return image_link return image_link # type: ignore
def get_videos(channel_id: str, def get_videos(channel_id: str,
@ -308,7 +326,9 @@ 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, video_id: str = "") -> wx.Bitmap: def make_bitmap_from_url(logo_url: str,
size: wx.Size = SIZE,
video_id: str = "") -> wx.Bitmap:
if not video_id: if not video_id:
video_id = hash_string(logo_url) video_id = hash_string(logo_url)
thumbpath = path.join(BASEPATH, 'thumbnails') thumbpath = path.join(BASEPATH, 'thumbnails')
@ -344,57 +364,34 @@ def make_bitmap_from_file(path, size: wx.Size = SIZE) -> wx.Bitmap:
def resolve_svt_channel(svt_id: str, path: str = '/usr/share/cast') -> 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(path))
},
"ch-svt1": {
"name": "SVT 1",
"thumbnail":
make_bitmap_from_file('{}/assets/SVT1.png'.format(path))
},
"ch-svt2": {
"name": "SVT 2",
"thumbnail":
make_bitmap_from_file('{}/assets/SVT2.png'.format(path))
},
"ch-svt24": {
"name": "SVT 24",
"thumbnail":
make_bitmap_from_file('{}/assets/SVT24.png'.format(path))
},
"ch-kunskapskanalen": {
"name":
"Kunskapskanalen",
"thumbnail":
make_bitmap_from_file('{}/assets/Kunskapskanalen.png'.format(path))
},
"feed": {
"name": "Senaste program", "name": "Senaste program",
"thumbnail": "thumbnail":
make_bitmap_from_file('{}/assets/SVT.png'.format(path)) make_bitmap_from_file('{}/assets/SVT.png'.format(path))
}, }
"allprograms": { channels["allprograms"] = {
"name": "Alla program", "name": "Alla program",
"thumbnail": "thumbnail":
make_bitmap_from_file('{}/assets/SVT.png'.format(path)) 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']))
} }
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]

Loading…
Cancel
Save