diff --git a/src/Channel/YouTube/__init__.py b/src/Channel/YouTube/__init__.py index ab043d7..6db8fd1 100644 --- a/src/Channel/YouTube/__init__.py +++ b/src/Channel/YouTube/__init__.py @@ -1,42 +1,46 @@ import hashlib -import io import os import pickle import threading +import time from typing import Union import feedparser -import requests import wx -from bs4 import BeautifulSoup -from youtube_dl import YoutubeDL as yt - from Channel import Channel from Items import Item from Utils import get_default_logo, make_bitmap_from_url +from youtube_dl import YoutubeDL as yt +from youtube_dl.utils import DownloadError, ExtractorError class YouTube(Channel): m_cache: dict = dict() - m_cachefile = '/tmp/yt_cache' - def __init__(self, channel_id) -> None: + def __init__(self, channel_id: str, name: str) -> None: self.m_channel_id = channel_id - self.m_info = get_info(channel_id) + self.m_name = name rss_url = 'https://www.youtube.com/feeds/videos.xml?channel_id={}'.format( channel_id) + self.m_cachefile = '/tmp/yt_cache_{}'.format(channel_id) self.m_logo = get_default_logo('YouTube') - super().__init__(channel_id, rss_url, self.m_logo, self.m_info['title']) + super().__init__(channel_id, rss_url, self.m_logo, + self.m_name) self.m_items: Union[list[Item], None] = None if os.path.exists(self.m_cachefile): with open(self.m_cachefile, 'rb') as cachehandle: - self.m_cache = pickle.load(cachehandle) + try: + self.m_cache = pickle.load(cachehandle) + except EOFError or pickle.UnpicklingError: + pass + self.m_thr = threading.Thread(target=self.parse_feed, args=(), kwargs={}) self.m_thr.start() + self.pickle() def wait(self) -> bool: return self.m_thr.is_alive() @@ -68,13 +72,16 @@ class YouTube(Channel): description = str(entry['description']) link = '' with yt(ydl_opts) as ydl: - video = ydl.extract_info(entry['link'], download=False) + try: + video = ydl.extract_info(entry['link'], download=False) - for form in video['formats']: - if form['height']: - if form['height'] < 480 and form[ - 'acodec'] != 'none': - link = form['url'] + for form in video['formats']: + if form['height']: + if form['height'] < 480 and form[ + 'acodec'] != 'none': + link = form['url'] + except ExtractorError or DownloadError: + pass resolved_link = link @@ -87,19 +94,16 @@ class YouTube(Channel): self.m_cache[key]['description'] = description self.m_cache[key]['published_parsed'] = published_parsed self.m_cache[key]['title'] = title - thumbnail = make_bitmap_from_url(thumbnail_link, wx.Size(self.m_screen_width,150)) + 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) + + def pickle(self) -> None: + while self.wait(): + time.sleep(1) + # write to cache file with open(self.m_cachefile, 'wb') as cachehandle: pickle.dump(self.m_cache, cachehandle) - - -def get_info(channel_id: str) -> dict: - info: dict = {'title': 'unknown'} - link = 'https://www.youtube.com/channel/{}'.format(channel_id) - ydl_opts = {'extract_flat': True, 'skip_download': True} - with yt(ydl_opts) as ydl: - info = ydl.extract_info(link, download=False) - return info diff --git a/src/Utils/__init__.py b/src/Utils/__init__.py index f034a86..7d105e3 100644 --- a/src/Utils/__init__.py +++ b/src/Utils/__init__.py @@ -89,7 +89,7 @@ def resolve_svt_channel(svt_id: str) -> dict: "SVT 24", "thumbnail": make_bitmap_from_file('{}/assets/SVT24.png'.format(MYPATH)) }, - "kunskapskanalen": { + "ch-kunskapskanalen": { "name": "Kunskapskanalen", "thumbnail": make_bitmap_from_file('{}/assets/Kunskapskanalen.png'.format(MYPATH)) diff --git a/src/main.py b/src/main.py index 5b6711d..33cf638 100644 --- a/src/main.py +++ b/src/main.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 +import json +import os import threading import time +from typing import Callable +from urllib.parse import urlparse import pychromecast import wx import wx.lib.scrolledpanel as scrolled import wx.media -from typing import Callable - from Channel import SVT, YouTube from ChannelProvider import ChannelProvider from Utils import make_sized_button @@ -22,6 +24,7 @@ SCROLL_RATE = 5 BM_BTN_STYLE = wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.TOP BTN_STYLE = wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT + class Cast(wx.Frame): def __init__(self, *args, **kw): """__init__. @@ -32,47 +35,27 @@ class Cast(wx.Frame): self.m_selected_chromecast = None self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH) self.m_index = 0 - self.m_chromecast_thr = threading.Thread( - target=self.get_chromecasts, args=(), kwargs={} - ) + self.m_chromecast_thr = threading.Thread(target=self.get_chromecasts, + args=(), + kwargs={}) 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_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.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) - self.m_providers: list[ChannelProvider] = [ - ChannelProvider( - "SVT", - channels=[ - SVT.SVT("feed"), - SVT.SVT("ch-svt1"), - SVT.SVT("ch-svt2"), - SVT.SVT("ch-svt24"), - ], - ), - ChannelProvider( - "YouTube", - channels=[ - YouTube.YouTube("UCtESv1e7ntJaLJYKIO1FoYw"), - YouTube.YouTube("UC9-y-6csu5WGm29I7JiwpnA"), - YouTube.YouTube("UCoxcjq-8xIDTYp3uz647V5A"), - YouTube.YouTube("UCu6mSoMNzHQiBIOCkHUa2Aw"), - ], - ), - ] + self.m_providers: list[ChannelProvider] = self.get_providers() self.m_selected_channel = None self.m_selected_provider_index = None - self, self.show_provider_list(None) def add_back_button(self, callback: Callable) -> None: - backbtn = wx.Button(self.m_panel, -1, label="Go back", size=(WIDTH, BTN_HEIGHT)) - backbtn.Bind( - wx.EVT_BUTTON, callback - ) + backbtn = wx.Button(self.m_panel, + -1, + label="Go back", + size=(WIDTH, BTN_HEIGHT)) + backbtn.Bind(wx.EVT_BUTTON, callback) self.m_sizer.Add(backbtn) def get_chromecasts(self) -> None: @@ -83,22 +66,68 @@ class Cast(wx.Frame): """ self.m_chromecasts, self.m_browser = pychromecast.get_chromecasts() + 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"), + ], + ) + + providers.append(svt) + youtube = ChannelProvider( + "YouTube", + channels=[ + YouTube.YouTube("UCs6A_0Jm21SIvpdKyg9Gmxw", "Pine 64"), + ], + ) + subfile = 'yt_subs.json' + + if os.path.isfile(subfile): + with open(subfile, 'r') as subs: + janson = json.loads(subs.read()) + + for channel in janson['subscriptions']: + if channel['service_id'] == 0: + channel_id = urlparse( + channel['url']).path.split('/').pop() + youtube.append_channel( + YouTube.YouTube(channel_id, channel['name'])) + + providers.append(youtube) + + return providers + def show_provider_list(self, _) -> None: self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) - closebtn = wx.Button(self.m_panel, -1, label="Close", size=(WIDTH, BTN_HEIGHT)) + 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) provider_index = 0 + for provider in self.m_providers: bitmap = provider.get_logo_as_bitmap() - callback = lambda event, index=provider_index: self.show_channel_list(event, index) - btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel,bitmap,provider.get_name(),callback) + callback = lambda event, index=provider_index: self.show_channel_list( + event, index) + btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel, bitmap, + provider.get_name(), + callback) self.m_sizer.Add(btn_sizer) provider_index += 1 - self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) + 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() @@ -112,14 +141,18 @@ class Cast(wx.Frame): bck_callback = lambda event: self.show_provider_list(event) self.add_back_button(bck_callback) channel_index = 0 + for channel in self.m_selected_provider.get_channels(): bitmap = channel.get_logo_as_bitmap() - callback = lambda event, index=channel_index: self.show_video_list(event, index) - btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel,bitmap,channel.get_name(),callback) + callback = lambda event, index=channel_index: self.show_video_list( + event, index) + btn_sizer: wx.BoxSizer = make_sized_button(self.m_panel, bitmap, + channel.get_name(), + callback) self.m_sizer.Add(btn_sizer) channel_index += 1 - self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) + 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() @@ -129,30 +162,36 @@ class Cast(wx.Frame): self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) channel = self.m_selected_provider.get_channel_by_index(index) + if channel.wait(): with wx.BusyInfo("Please wait, working..."): index = 0 + while channel.wait(): time.sleep(1) wx.GetApp().Yield() - callback = lambda event: self.show_channel_list(event, self.m_selected_provider_index) + callback = lambda event: self.show_channel_list( + event, self.m_selected_provider_index) self.add_back_button(callback) - for item in channel.get_items(): # type: ignore + for item in channel.get_items(): # type: ignore inner_sizer = wx.BoxSizer(wx.VERTICAL) title = wx.StaticText(self.m_panel, -1) - title.SetLabelMarkup("{}".format(item["title"])) + title.SetLabelMarkup("{}".format( + item["title"])) description = wx.StaticText(self.m_panel, -1, item["description"]) - description.Wrap(WIDTH -2) + description.Wrap(WIDTH - 2) bitmap = item["thumbnail"] - btn = wx.BitmapButton(self.m_panel, id=self.m_index, bitmap=bitmap, style= BM_BTN_STYLE) + btn = wx.BitmapButton(self.m_panel, + id=self.m_index, + bitmap=bitmap, + style=BM_BTN_STYLE) btn.Bind( wx.EVT_BUTTON, - lambda event, link=item["link"], provider_index=index: self.show_player( - event, link, provider_index - ), + lambda event, link=item["link"], provider_index=index: self. + show_player(event, link, provider_index), ) inner_sizer.Add(title) inner_sizer.Add(btn) @@ -160,7 +199,7 @@ class Cast(wx.Frame): self.m_sizer.Add(inner_sizer) self.m_sizer.AddSpacer(SPACER_HEIGHT) self.m_index = self.m_index + 1 - self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) + 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() @@ -178,8 +217,8 @@ class Cast(wx.Frame): inner_sizer = wx.GridBagSizer() self.m_control = wx.media.MediaCtrl( self.m_panel, - # size=(WIDTH, HEIGHT/2), - # style=wx.SIMPLE_BORDER, + size=(WIDTH, HEIGHT / 2), + style=wx.SIMPLE_BORDER, szBackend=wx.media.MEDIABACKEND_GSTREAMER, ) play_button = wx.Button(self.m_panel, -1, "Play") @@ -189,7 +228,8 @@ class Cast(wx.Frame): back_button = wx.Button(self.m_panel, -1, "Back") back_button.Bind( wx.EVT_BUTTON, - lambda event, index=provider_index: self.show_video_list(event, index), + lambda event, index=provider_index: self.show_video_list( + event, index), ) inner_sizer.Add(self.m_control, (0, 0)) @@ -198,13 +238,13 @@ class Cast(wx.Frame): inner_sizer.Add(pause_button, (1, 2)) inner_sizer.Add(back_button, (1, 3)) - if not self.m_chromecast_thr.is_alive() and not self.m_selected_chromecast: + if not self.m_chromecast_thr.is_alive( + ) and not self.m_selected_chromecast: chromecast_button = wx.Button(self.m_panel, -1, "Cast") chromecast_button.Bind( wx.EVT_BUTTON, - lambda event, muri=uri, index=provider_index: self.select_chromecast( - event, muri, index - ), + lambda event, muri=uri, index=provider_index: self. + select_chromecast(event, muri, index), ) inner_sizer.Add(chromecast_button, (2, 2)) self.m_sizer.Add(inner_sizer) @@ -223,7 +263,7 @@ class Cast(wx.Frame): self.Bind(wx.media.EVT_MEDIA_FINISHED, self.show_video_list) self.load_uri(uri) - self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) + 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() @@ -234,23 +274,31 @@ class Cast(wx.Frame): self.m_sizer = wx.BoxSizer(wx.VERTICAL) self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) - cancel_btn = wx.Button(self.m_panel, -1, "Cancel", size=(WIDTH, BTN_HEIGHT), style=wx.BU_EXACTFIT) + cancel_btn = wx.Button(self.m_panel, + -1, + "Cancel", + size=(WIDTH, BTN_HEIGHT), + style=wx.BU_EXACTFIT) cancel_btn.Bind( wx.EVT_BUTTON, - lambda event, index=provider_index: self.show_video_list(event, index), + lambda event, index=provider_index: self.show_video_list( + event, index), ) - self.m_sizer.Add(cancel_btn) #, wx.ALIGN_CENTER_VERTICAL) + self.m_sizer.Add(cancel_btn) #, wx.ALIGN_CENTER_VERTICAL) + for cast in self.m_chromecasts: friendly_name = cast.cast_info.friendly_name - btn = wx.Button(self.m_panel, id=-1, label=friendly_name, size=(WIDTH, BTN_HEIGHT)) + btn = wx.Button(self.m_panel, + id=-1, + label=friendly_name, + size=(WIDTH, BTN_HEIGHT)) btn.Bind( wx.EVT_BUTTON, - lambda event, chromecast=cast, muri=uri, index=provider_index: self.set_chromecast( - event, chromecast, muri, index - ), + lambda event, chromecast=cast, muri=uri, index=provider_index: + self.set_chromecast(event, chromecast, muri, index), ) - self.m_sizer.Add(btn)#, wx.ALIGN_CENTER_VERTICAL) - self.m_panel.SetupScrolling(rate_y=SCROLL_RATE,scrollToTop=True) + self.m_sizer.Add(btn) #, wx.ALIGN_CENTER_VERTICAL) + 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() @@ -278,11 +326,8 @@ class Cast(wx.Frame): break - if ( - cast.socket_client.is_connected - and has_played - and player_state != "PLAYING" - ): + if (cast.socket_client.is_connected and has_played + and player_state != "PLAYING"): has_played = False cast.media_controller.play_media(uri, mimetype)