Kill YouTube Support and switch to VLC for playback

With this commit only SVT Play is currently supported. I feel that
TubeFeeder is the best choice for YouTube on mobile linux and
maintenance burden will be less if I don't compete with an allready
nice app :)

I might add support for other providers down the line if they are not
supported in TubeFeeder. See:

* https://github.com/Tubefeeder/TubeFeeder

for more information about TubeFeeder

I am also switching out gstreamer in favour of VLC with this commit.
main
Micke Nordin 2 years ago
parent c2502162a8
commit 3604862b84
Signed by: micke
GPG Key ID: 014B273D614BE877

@ -2,4 +2,4 @@
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.
Currently Swedish Public Service TV and YouTube is supported.
Currently Swedish Public Service TV is supported.

@ -1,28 +1,5 @@
python3-bs4
gir1.2-gstreamer-1.0:amd64
gstreamer1.0-alsa:amd64
gstreamer1.0-clutter-3.0:amd64
gstreamer1.0-gl:amd64
gstreamer1.0-gtk3:amd64
gstreamer1.0-libav:amd64
gstreamer1.0-nice:amd64
gstreamer1.0-packagekit
gstreamer1.0-pipewire:amd64
gstreamer1.0-plugins-bad:amd64
gstreamer1.0-plugins-base:amd64
gstreamer1.0-plugins-base-apps
gstreamer1.0-plugins-good:amd64
gstreamer1.0-plugins-rtp
gstreamer1.0-plugins-ugly:amd64
gstreamer1.0-pulseaudio:amd64
gstreamer1.0-tools
gstreamer1.0-x:amd64
libgstreamer-gl1.0-0:amd64
libgstreamer-plugins-bad1.0-0:amd64
libgstreamer-plugins-base1.0-0:amd64
libgstreamer-plugins-good1.0-0:amd64
libgstreamer1.0-0:amd64
python3-wxgtk-media4.0
python3-wxgtk4.0
python3-feedparser
youtube-dl
python3-vlc
python3-pychromecast

@ -11,35 +11,7 @@ elif [[ "${1}" == "-h" ]]; then
exit 0
fi
sudo apt install python3-pip \
python3-bs4 \
gir1.2-gstreamer-1.0 \
gstreamer1.0-alsa \
gstreamer1.0-clutter-3.0 \
gstreamer1.0-gl \
gstreamer1.0-gtk3 \
gstreamer1.0-libav \
gstreamer1.0-nice \
gstreamer1.0-packagekit \
gstreamer1.0-pipewire \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-base-apps \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-rtp \
gstreamer1.0-plugins-ugly \
gstreamer1.0-pulseaudio \
gstreamer1.0-tools \
gstreamer1.0-x \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-bad1.0-0 \
libgstreamer-plugins-base1.0-0 \
libgstreamer1.0-0 \
python3-wxgtk-media4.0 \
python3-wxgtk4.0 \
python3-feedparser \
youtube-dl
sudo pip3 install pychromecast
sudo apt install $(cat dpkg.lst | tr '\n' ' ')
sudo cp src/main.py /usr/local/bin/cast
sudo chmod +x /usr/local/bin/cast
sudo cp -a src/{Channel,ChannelProvider,Items,Utils} /usr/lib/python3/dist-packages/

@ -1,2 +1 @@
pychromecast
youtube-search-python

@ -1,7 +1,6 @@
#!/usr/bin/env python3
import json
import threading
from datetime import datetime
import feedparser
@ -64,7 +63,7 @@ class SVT(Channel):
if not resolved_link:
continue
thumbnail = make_bitmap_from_url(
thumbnail_link, wx.Size(self.m_screen_width, 150))
thumbnail_link, wx.Size(int(self.m_screen_width), 150))
item = Item(description, resolved_link, self.m_provider_name,
published_parsed, thumbnail, title)
self.m_items.append(item)
@ -86,8 +85,10 @@ class SVT(Channel):
def resolve_link(self,svt_id) -> str:
url = 'https://api.svt.se/video/{}'.format(svt_id)
print(url)
api = json.loads(
requests.get('https://api.svt.se/video/{}'.format(svt_id)).text)
requests.get(url).text)
resolved_link = ''
try:

@ -1,47 +0,0 @@
import feedparser
import wx
from Channel import Channel
from Items import Item
from Utils import add_video, hash_string, make_bitmap_from_url, video_exists
#from youtubesearchpython.search import VideosSearch
class YouTube(Channel):
def __init__(self, channel_id: str, name: str, logo: wx.Bitmap) -> None:
self.m_name = name
rss_url = 'https://www.youtube.com/feeds/videos.xml?channel_id={}'.format(
channel_id)
self.m_logo = logo
self.m_items: list[Item] = list()
super().__init__(channel_id, 'YouTube', rss_url, self.m_logo, name)
def parse_feed(self) -> None:
feed = feedparser.parse(self.get_feed())
entries = feed['entries']
for entry in entries:
video_id = hash_string(entry['id'])
if video_exists(video_id, self.m_id):
continue
title = str(entry['title'])
thumbnail_link = str(entry['media_thumbnail'][0]['url'])
description = str(entry['description'])
#video_search = VideosSearch(entry['id'], limit = 1).result()['result'][0]
resolved_link = entry['link']
print(resolved_link)
published_parsed = entry['published_parsed']
if not resolved_link:
continue
thumbnail = make_bitmap_from_url(thumbnail_link,
wx.Size(self.m_screen_width, 150))
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)

@ -1,18 +1,14 @@
#!/usr/bin/env python3
import hashlib
import io
import json
import sqlite3
import time
from datetime import datetime
from os import environ, makedirs, path
from typing import Callable, Union
from urllib.parse import urlparse
from youtubesearchpython import ChannelsSearch
import requests
import wx
import youtube_dl
from Items import Item
@ -27,34 +23,6 @@ SUB_TABLE = 'subscriptions'
VIDEO_TABLE = 'videos'
def add_subscription(channel_id: str,
name: str,
basepath: str = BASEPATH,
filename: str = DB_FILE_NAME) -> None:
fullpath = path.join(basepath, filename)
thumbpath = path.join(basepath, 'thumbnails')
thumbnail = path.join(thumbpath, channel_id)
fullpath = path.join(basepath, filename)
if not path.isdir(thumbpath):
makedirs(thumbpath)
if not path.isfile(thumbnail):
channels_search = ChannelsSearch(name, limit=1).result()['result'][0] #type: ignore
bitmap = make_bitmap_from_url('https:' + channels_search['thumbnails'][0]['url'])
bitmap.SaveFile(thumbnail, wx.BITMAP_TYPE_PNG)
con = sqlite3.connect(fullpath)
cur = con.cursor()
create_query: str = '''CREATE TABLE IF NOT EXISTS {}
(channel_id TEXT PRIMARY KEY, channel_name TEXT, thumb_path TEXT)'''.format(
SUB_TABLE)
cur.execute(create_query)
con.commit()
upsert_query: str = '''INSERT INTO {} (channel_id, channel_name, thumb_path)
VALUES(?,?,?) ON CONFLICT(channel_id) DO NOTHING'''.format(
SUB_TABLE )
cur.execute(upsert_query, [channel_id, name, thumbnail])
con.commit()
def add_video(video_id: str,
channel_id: str,
provider_id: str,
@ -103,8 +71,6 @@ def add_video(video_id: str,
def get_default_logo(providerid: str = 'default') -> wx.Bitmap:
if providerid == 'SVT':
return wx.Bitmap('{}/assets/SVT.png'.format(MYPATH))
if providerid == 'YouTube':
return wx.Bitmap('{}/assets/YouTube.png'.format(MYPATH))
else:
return wx.Bitmap('{}/assets/Default.png'.format(MYPATH))
@ -213,18 +179,6 @@ def hash_string(string: str) -> str:
return hash_object.hexdigest()
def import_from_newpipe(filename) -> None:
if path.isfile(filename):
with open(filename, 'r') as subs:
sub_data = json.loads(subs.read())
for channel in sub_data['subscriptions']:
if channel['service_id'] == 0:
channel_id = urlparse(channel['url']).path.split('/').pop()
add_subscription(channel_id, channel['name'])
def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap,
str],
text: str, callback: Callable) -> wx.BoxSizer:
@ -265,9 +219,9 @@ def make_bitmap_from_url(logo_url: str, size: wx.Size = SIZE) -> wx.Bitmap:
content_bytes = io.BytesIO(content)
image = wx.Image(content_bytes, type=wx.BITMAP_TYPE_ANY, index=-1)
scale_factor = image.GetWidth() / size.GetWidth()
size.SetWidth(image.GetWidth() / scale_factor)
size.SetWidth(int(image.GetWidth() / scale_factor))
height = image.GetHeight()
size.SetHeight(height / scale_factor)
size.SetHeight(int(height / scale_factor))
image.Rescale(size.GetWidth(), size.GetHeight())
return wx.Bitmap(image)
@ -275,9 +229,9 @@ def make_bitmap_from_url(logo_url: str, size: wx.Size = SIZE) -> wx.Bitmap:
def make_bitmap_from_file(path, size: wx.Size = SIZE) -> wx.Bitmap:
image = wx.Image(path, type=wx.BITMAP_TYPE_ANY, index=-1)
scale_factor = image.GetWidth() / size.GetWidth()
size.SetWidth(image.GetWidth() / scale_factor)
size.SetWidth(int(image.GetWidth() / scale_factor))
height = image.GetHeight()
size.SetHeight(height / scale_factor)
size.SetHeight(int(height / scale_factor))
image.Rescale(size.GetWidth(), size.GetHeight())
return wx.Bitmap(image)
@ -324,27 +278,6 @@ def resolve_svt_channel(svt_id: str) -> dict:
return channels[svt_id]
def resolve_youtube_link(link):
ydl_opts = {
'format':
'worstvideo[ext=mp4]+worstaudio[ext=m4a]/worstvideo+worstaudio',
'container': 'webm dash'
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
try:
video = ydl.extract_info(link, download=False)
for form in video['formats']: # type: ignore
if form['height']:
if form['height'] < 480 and form['acodec'] != 'none':
link = form['url']
except youtube_dl.utils.ExtractorError and youtube_dl.utils.DownloadError:
pass
return link
def video_exists(video_id: str,
channel_id: str,
basepath: str = BASEPATH,

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

@ -8,4 +8,4 @@ Exec=/usr/local/bin/cast
Icon=video-single-display-symbolic
Terminal=false
Categories=Security;Utility;
Keywords=YouTube;SVT;
Keywords=SVT;

@ -1,5 +1,4 @@
#!/usr/bin/env python3
import os
import threading
import time
from typing import Callable
@ -8,13 +7,11 @@ import pychromecast
import wx
import wx.lib.scrolledpanel as scrolled
import wx.media
from youtubesearchpython import ChannelsSearch
from vlc import Instance
from Channel import SVT, YouTube
from Channel import SVT
from ChannelProvider import ChannelProvider
from Utils import (get_default_logo, get_subscriptions, import_from_newpipe,
make_bitmap_from_file, make_bitmap_from_url,
make_sized_button, resolve_youtube_link)
from Utils import MYPATH, make_bitmap_from_file, make_sized_button
WIDTH = int(720 / 2)
HEIGHT = int(1440 / 2)
@ -42,15 +39,17 @@ class Cast(wx.Frame):
self.m_chromecast_thr = threading.Thread(target=self.get_chromecasts,
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)
self.m_panel: wx.lib.scrolledpanel.ScrolledPanel = scrolled.ScrolledPanel( # type: ignore
self, -1, style=wx.VSCROLL)
self.m_control = None
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()
self.show_provider_list(None)
self.show_channel_list(None, 0)
def add_back_button(self, callback: Callable) -> None:
backbtn = wx.Button(self.m_panel,
@ -60,38 +59,6 @@ class Cast(wx.Frame):
backbtn.Bind(wx.EVT_BUTTON, callback)
self.m_sizer.Add(backbtn)
def add_youtube_buttons(self) -> None:
def search_callback(event, text: wx.TextCtrl) -> None:
reply = text.GetLineText(0)
text.Clear()
channels_search = ChannelsSearch(
reply, limit=1).result()['result'][0] #type: ignore
if 'id' in channels_search:
found_channel = YouTube.YouTube(
channels_search['id'],
channels_search['title'],
logo=make_bitmap_from_url(
'https:' + channels_search['thumbnails'][0]['url'],
size=wx.Size(68, 100))) #type: ignore
self.m_selected_provider.append_channel(found_channel)
text: wx.TextCtrl = wx.TextCtrl(self.m_panel, size=(WIDTH, BTN_HEIGHT))
self.m_sizer.Add(text)
searchbtn = wx.Button(self.m_panel,
-1,
label="Search",
size=(WIDTH, BTN_HEIGHT))
self.m_sizer.Add(searchbtn)
searchbtn.Bind(
wx.EVT_BUTTON,
lambda event, textctl=text: search_callback(event, textctl))
importbtn = wx.Button(self.m_panel,
-1,
label="Import from NewPipe",
size=(WIDTH, BTN_HEIGHT))
importbtn.Bind(wx.EVT_BUTTON, lambda event: self.show_importer(event))
self.m_sizer.Add(importbtn)
def get_chromecasts(self) -> None:
"""
[TODO:description]
@ -122,44 +89,45 @@ class Cast(wx.Frame):
event, cindex),
)
stop_button = wx.Button(self.m_panel,
-1,
"Stop",
size=(WIDTH / 4, BTN_HEIGHT))
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:
chromecast_button = wx.Button(self.m_panel,
btm = make_bitmap_from_file('{}/assets/Cast.png'.format(MYPATH), wx.Size(24,24))
cast_button = wx.BitmapButton(self.m_panel,
-1,
"Cast",
bitmap=btm,
size=(WIDTH / 4, BTN_HEIGHT))
chromecast_button.Bind(
cast_button.Bind(
wx.EVT_BUTTON,
lambda event, muri=uri, cindex=channel_index: self.
select_chromecast(event, muri, cindex),
)
inner_sizer.Add(chromecast_button, FLAGS)
elif self.m_selected_chromecast:
inner_sizer.Add(cast_button, FLAGS)
chromecast_button = wx.Button(self.m_panel,
-1,
"Stop Cast",
size=(WIDTH / 4, BTN_HEIGHT))
chromecast_button.Bind(
wx.EVT_BUTTON,
lambda event, muri=uri, cindex=channel_index: self.
stop_callback(event, muri, cindex),
)
inner_sizer.Add(chromecast_button, FLAGS)
if self.m_selected_chromecast:
self.cast(wx.media.EVT_MEDIA_LOADED, uri),
play_button.Bind(wx.EVT_BUTTON, self.play_cast)
pause_button.Bind(wx.EVT_BUTTON, self.pause_cast)
stop_button.Bind(
wx.EVT_BUTTON,
lambda event, muri=uri, cindex=channel_index: self.
stop_callback(event, muri, cindex),
)
else:
self.Bind(wx.media.EVT_MEDIA_LOADED,
lambda event: wx.Frame.SetFocus(self))
play_button.Bind(wx.EVT_BUTTON, self.play)
pause_button.Bind(wx.EVT_BUTTON, self.pause)
stop_button.Bind(wx.EVT_BUTTON, self.stop)
inner_sizer.Fit(self)
inner_sizer.Layout()
@ -168,7 +136,6 @@ class Cast(wx.Frame):
def get_providers(self) -> list[ChannelProvider]:
providers = list()
channels = list()
svt = ChannelProvider(
"SVT",
channels=[
@ -182,87 +149,21 @@ class Cast(wx.Frame):
)
providers.append(svt)
subscriptions = get_subscriptions()
if subscriptions:
for channel in subscriptions:
logo = make_bitmap_from_file(channel[2])
channels.append(YouTube.YouTube(channel[0], channel[1], logo))
else:
logo = make_bitmap_from_url(
'https://yt3.ggpht.com/ytc/AKedOLQ5L9xUSDxB2j6V3VC8L_HEwiKeHM21CgbSUyqe=s88-c-k-c0x00ffffff-no-rj'
)
channels.append(
YouTube.YouTube("UCs6A_0Jm21SIvpdKyg9Gmxw", "Pine 64", logo))
youtube = ChannelProvider("YouTube", channels=channels)
providers.append(youtube)
return providers
def show_importer(self, _) -> None:
with wx.FileDialog(self,
"Open Newpipe json file",
wildcard="Json files (*.json)|*.json",
style=wx.FD_OPEN
| wx.FD_FILE_MUST_EXIST) as file_dialog:
if file_dialog.ShowModal() == wx.ID_CANCEL:
return # the user changed their mind
# Proceed loading the file chosen by the user
subfile = file_dialog.GetPath()
channels = list()
logo = get_default_logo('YouTube')
if os.path.isfile(subfile):
import_from_newpipe(subfile)
subscriptions = get_subscriptions()
for channel in subscriptions:
yt_chan = YouTube.YouTube(channel[0], channel[1], logo)
yt_chan.refresh()
channels.append(yt_chan)
# Index 1 is YouTube
self.m_providers[1].set_channels(channels)
self.m_providers[1].make_latest()
self.show_channel_list(None, self.m_selected_provider_index)
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_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)
#self.m_sizer.AddSpacer(SPACER_HEIGHT * 4)
bck_callback = lambda event: self.show_provider_list(event)
self.add_back_button(bck_callback)
if self.m_selected_provider.get_name() == "YouTube":
self.add_youtube_buttons()
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)
channel_index = 0
@ -290,27 +191,10 @@ 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() == 'YouTube' or (
self.m_selected_provider.get_name() == 'SVT'
and self.m_selected_channel.get_id() == 'feed'):
if self.m_selected_provider.get_name() == 'SVT' and self.m_selected_channel.get_id() == 'feed':
def refresh_callback(event):
if self.m_selected_provider.get_name(
) == 'YouTube' and channel_index == 0:
with wx.BusyInfo("Please wait, working..."):
for chan in self.m_selected_provider.get_channels():
chan.refresh()
wait = 1
while wait > 0:
wait = 0
for chan in self.m_selected_provider.get_channels(
):
if chan.wait():
wait += 1
time.sleep(1)
wx.GetApp().Yield()
else:
self.m_selected_channel.refresh()
self.m_selected_channel.refresh()
self.show_video_list(event, channel_index)
refreshbtn = wx.Button(self.m_panel,
@ -386,22 +270,12 @@ class Cast(wx.Frame):
:param _ event: unused
:param uri str: the link to the video stream
"""
if 'youtube' in uri:
uri = resolve_youtube_link(uri)
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)
self.m_sizer.Clear(delete_windows=True)
self.m_sizer = wx.BoxSizer(wx.VERTICAL)
if not self.m_selected_chromecast:
self.m_control = wx.media.MediaCtrl(
self.m_panel,
size=(WIDTH,HEIGHT/2),
szBackend=wx.media.MEDIABACKEND_GSTREAMER,
)
self.m_sizer.Add(self.m_control, FLAGS)
self.Bind(wx.media.EVT_MEDIA_FINISHED,
lambda event: self.show_video_list(event, 0))
self.Bind(wx.EVT_POWER_SUSPENDING,
lambda event: wx.EVT_POWER_SUSPENDING.Veto(event))
self.load_uri(uri)
self.m_sizer.Add(self.get_player_controls(channel_index, uri), FLAGS)
self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True)
self.m_panel.SetSizer(self.m_sizer)
@ -424,7 +298,7 @@ class Cast(wx.Frame):
self.m_sizer.Add(cancel_btn)
for cast in self.m_chromecasts:
friendly_name = cast.cast_info.friendly_name
friendly_name = cast.device.friendly_name
btn = wx.Button(self.m_panel,
id=-1,
label=friendly_name,
@ -498,14 +372,14 @@ class Cast(wx.Frame):
self.m_sizer.Fit(self)
self.m_sizer.Layout()
def load_uri(self, uri):
self.m_control.LoadURI(uri)
def play(self, _):
self.m_control.Play()
self.m_vlc_listplayer.play()
def pause(self, _):
self.m_control.Pause()
self.m_vlc_listplayer.pause()
def stop(self, _):
self.m_vlc_listplayer.stop()
def quit(self, _):
self.Destroy()

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save