Compare commits

...

9 commits
v0.1.0 ... 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
6 changed files with 120 additions and 88 deletions

View file

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

View file

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

View file

@ -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:
if cast.cast_type == 'audio':
continue
friendly_name = "Unknown Chromecast"
try:
friendly_name = cast.device.friendly_name 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,

View file

@ -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']),

View file

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

View file

@ -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,16 +78,11 @@ 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:
return(json.loads(jfile.read()))
categories: list = list() categories: list = list()
url = "https://www.svtplay.se/kategori" url = "https://www.svtplay.se/kategori"
data = get_svt_data(url) data = get_svt_data(url)
@ -94,12 +90,34 @@ def get_all_svt_categories(basepath=BASEPATH) -> list:
if 'genres' in entry.keys(): if 'genres' in entry.keys():
categories = entry['genres'] categories = entry['genres']
break break
with open(categoryfile, 'w') as jfile: CAT_CACHE = categories
jfile.write(json.dumps(categories))
USE_CACHED_CATEGORIES = True
return 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: def get_svt_category(category: str) -> list:
url = 'https://www.svtplay.se/kategori/{}?tab=all'.format(category) url = 'https://www.svtplay.se/kategori/{}?tab=all'.format(category)
data = get_svt_data(url) data = get_svt_data(url)
@ -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,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: 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(): 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"], category["name"],
"thumbnail": "thumbnail":
make_bitmap_from_url( make_bitmap_from_url(
get_svt_thumb_from_id_changed(category['image']['id'], get_svt_thumb_from_id_changed(
category['image']['changed'])) category['image']['id'], category['image']['changed']))
} }
CHAN_CACHE = channels
return channels[svt_id] return channels[svt_id]