Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
|
de02f43529 | ||
b7113b2e44 | |||
0a418e5836 | |||
03edf98b86 | |||
5a161c7e82 | |||
60ab02b4ba | |||
fc0a26c74e | |||
9857816402 | |||
|
e794b336ee |
6 changed files with 120 additions and 88 deletions
|
@ -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.
|
||||
|
||||
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
|
||||
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
|
||||
Icon=video-single-display-symbolic
|
||||
Terminal=false
|
||||
Categories=Security;Utility;
|
||||
Categories=Video;
|
||||
Keywords=SVT;
|
||||
|
|
55
scripts/cast
55
scripts/cast
|
@ -2,6 +2,7 @@
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
import pychromecast
|
||||
|
@ -13,7 +14,7 @@ from vlc import Instance
|
|||
from Channel import SVT
|
||||
from ChannelProvider import ChannelProvider
|
||||
from Utils import (get_all_svt_categories, make_bitmap_from_file,
|
||||
make_sized_button)
|
||||
make_sized_button, BASEPATH)
|
||||
|
||||
WIDTH = int(720 / 2)
|
||||
HEIGHT = int(1440 / 2)
|
||||
|
@ -43,6 +44,9 @@ class Cast(wx.Frame):
|
|||
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()
|
||||
|
@ -50,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)
|
||||
|
@ -83,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,
|
||||
|
@ -101,8 +105,8 @@ class Cast(wx.Frame):
|
|||
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(
|
||||
|
@ -112,15 +116,14 @@ class Cast(wx.Frame):
|
|||
back_button = wx.Button(self.m_panel,
|
||||
-1,
|
||||
label="Close",
|
||||
size=(WIDTH / 4, BTN_HEIGHT))
|
||||
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(stop_button, FLAGS)
|
||||
inner_sizer.Add(back_button, FLAGS)
|
||||
|
||||
if not self.m_chromecast_thr.is_alive(
|
||||
) and not self.m_selected_chromecast:
|
||||
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,
|
||||
|
@ -149,10 +152,12 @@ 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]:
|
||||
|
||||
|
@ -183,19 +188,31 @@ class Cast(wx.Frame):
|
|||
channels = list()
|
||||
for id in chandict[provider]["channels"]:
|
||||
channels.append(SVT.SVT(id))
|
||||
svt = ChannelProvider(chandict[provider]["displayname"], channels=channels)
|
||||
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)
|
||||
back_callback = lambda event: self.show_provider_list(event)
|
||||
self.add_back_button(back_callback)
|
||||
else:
|
||||
closebtn = wx.Button(self.m_panel,
|
||||
|
@ -343,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)
|
||||
|
@ -370,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,
|
||||
|
|
2
setup.py
2
setup.py
|
@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|||
|
||||
setuptools.setup(
|
||||
name="cast",
|
||||
version="0.1.0",
|
||||
version="0.1.1",
|
||||
author="Micke Nordin",
|
||||
author_email="hej@mic.ke",
|
||||
data_files=[('share/applications', ['data/org.smolnet.cast.desktop']),
|
||||
|
|
|
@ -106,8 +106,14 @@ class SVT(Channel):
|
|||
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.split('/')[1])
|
||||
video_id = hash_string(url)
|
||||
svt_id = elem["videoSvtId"]
|
||||
resolved_link = self.resolve_link(svt_id)
|
||||
if not resolved_link:
|
||||
|
@ -116,8 +122,8 @@ class SVT(Channel):
|
|||
description = str(entry["description"])
|
||||
published_parsed = datetime.now()
|
||||
thumbnail_link = get_svt_thumb_from_id_changed(
|
||||
elem['images']["wide"]["id"],
|
||||
elem['images']["wide"]["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)
|
||||
|
|
|
@ -22,7 +22,8 @@ BASEPATH = path.join(str(environ.get("HOME")), '.config/cast')
|
|||
DB_FILE_NAME = 'cast.db'
|
||||
SUB_TABLE = 'subscriptions'
|
||||
VIDEO_TABLE = 'videos'
|
||||
USE_CACHED_CATEGORIES = False
|
||||
CAT_CACHE = None
|
||||
CHAN_CACHE = None
|
||||
|
||||
|
||||
def add_video(video_id: str,
|
||||
|
@ -77,16 +78,11 @@ def get_svt_thumb_from_id_changed(id: str,
|
|||
size, id, changed)
|
||||
|
||||
|
||||
def get_all_svt_categories(basepath=BASEPATH) -> list:
|
||||
global USE_CACHED_CATEGORIES
|
||||
categoryfile = path.join(basepath,"categories.json")
|
||||
if USE_CACHED_CATEGORIES:
|
||||
if path.isfile(categoryfile):
|
||||
with open(categoryfile, 'r') as jfile:
|
||||
return(json.loads(jfile.read()))
|
||||
|
||||
|
||||
|
||||
def get_all_svt_categories() -> list:
|
||||
global CAT_CACHE
|
||||
if CAT_CACHE:
|
||||
categories = CAT_CACHE
|
||||
else:
|
||||
categories: list = list()
|
||||
url = "https://www.svtplay.se/kategori"
|
||||
data = get_svt_data(url)
|
||||
|
@ -94,12 +90,34 @@ def get_all_svt_categories(basepath=BASEPATH) -> list:
|
|||
if 'genres' in entry.keys():
|
||||
categories = entry['genres']
|
||||
break
|
||||
with open(categoryfile, 'w') as jfile:
|
||||
jfile.write(json.dumps(categories))
|
||||
USE_CACHED_CATEGORIES = True
|
||||
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)
|
||||
|
@ -118,8 +136,8 @@ 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"]
|
||||
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
|
||||
|
@ -232,9 +250,9 @@ 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"]
|
||||
image_link = meta["content"] #type: ignore
|
||||
|
||||
return image_link
|
||||
return image_link # type: ignore
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
video_id = hash_string(logo_url)
|
||||
thumbpath = path.join(BASEPATH, 'thumbnails')
|
||||
|
@ -344,45 +364,21 @@ 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:
|
||||
global CHAN_CACHE
|
||||
|
||||
channels = {
|
||||
"ch-barnkanalen": {
|
||||
"name":
|
||||
"Barnkanalen",
|
||||
"thumbnail":
|
||||
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": {
|
||||
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(path))
|
||||
},
|
||||
"allprograms": {
|
||||
}
|
||||
channels["allprograms"] = {
|
||||
"name": "Alla program",
|
||||
"thumbnail":
|
||||
make_bitmap_from_file('{}/assets/SVT.png'.format(path))
|
||||
},
|
||||
}
|
||||
|
||||
for category in get_all_svt_categories():
|
||||
|
@ -391,9 +387,10 @@ def resolve_svt_channel(svt_id: str, path: str = '/usr/share/cast') -> dict:
|
|||
category["name"],
|
||||
"thumbnail":
|
||||
make_bitmap_from_url(
|
||||
get_svt_thumb_from_id_changed(category['image']['id'],
|
||||
category['image']['changed']))
|
||||
get_svt_thumb_from_id_changed(
|
||||
category['image']['id'], category['image']['changed']))
|
||||
}
|
||||
CHAN_CACHE = channels
|
||||
|
||||
return channels[svt_id]
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue