diff --git a/dpkg.lst b/dpkg.lst index 7b12271..cd264d1 100644 --- a/dpkg.lst +++ b/dpkg.lst @@ -22,7 +22,6 @@ libgstreamer-plugins-bad1.0-0:amd64 libgstreamer-plugins-base1.0-0:amd64 libgstreamer-plugins-good1.0-0:amd64 libgstreamer1.0-0:amd64 -libmpv1 python3-wxgtk-media4.0 python3-wxgtk4.0 python3-feedparser diff --git a/requirements.txt b/requirements.txt index 850b39e..668ee46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ pychromecast -python-mpv +youtube-search-python diff --git a/src/Channel/YouTube/__init__.py b/src/Channel/YouTube/__init__.py index 1c77b03..d5fe146 100644 --- a/src/Channel/YouTube/__init__.py +++ b/src/Channel/YouTube/__init__.py @@ -1,14 +1,11 @@ -import threading - import feedparser import wx -import time -import youtube_dl from Channel import Channel from Items import Item from Utils import (add_video, get_default_logo, get_latest_video_timestamp, hash_string, make_bitmap_from_url, video_exists) +#from youtubesearchpython.search import VideosSearch class YouTube(Channel): @@ -22,11 +19,6 @@ class YouTube(Channel): def parse_feed(self) -> None: feed = feedparser.parse(self.get_feed()) - ydl_opts = { - 'format': - 'worstvideo[ext=mp4]+worstaudio[ext=m4a]/worstvideo+worstaudio', - 'container': 'webm_dash', - } entries = feed['entries'] for entry in entries: @@ -36,22 +28,11 @@ class YouTube(Channel): title = str(entry['title']) thumbnail_link = str(entry['media_thumbnail'][0]['url']) description = str(entry['description']) - link = '' - with youtube_dl.YoutubeDL(ydl_opts) as ydl: - try: - video = ydl.extract_info(entry['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 - - resolved_link = link + #video_search = VideosSearch(entry['id'], limit = 1).result()['result'][0] + resolved_link = entry['link'] + print(resolved_link) - published_parsed = entry['published_parsed'] + published_parsed = entry['published_parsed'] if not resolved_link: continue diff --git a/src/Utils/__init__.py b/src/Utils/__init__.py index 1ea227e..ff8a21b 100644 --- a/src/Utils/__init__.py +++ b/src/Utils/__init__.py @@ -11,9 +11,12 @@ from urllib.parse import urlparse import requests import wx +import youtube_dl from Items import Item +HEIGHT = int(1440 / 2) +BTN_HEIGHT = 40 SIZE = wx.Size(100, 68) MYPATH = path.dirname(path.abspath(__file__)) SCREEN_WIDTH = int(720 / 2) @@ -129,9 +132,10 @@ def get_latest(provider_id: str, pass return videos + def get_latest_video_timestamp(channel_id: str, - basepath: str = BASEPATH, - filename: str = DB_FILE_NAME) -> datetime: + basepath: str = BASEPATH, + filename: str = DB_FILE_NAME) -> datetime: fullpath = path.join(basepath, filename) try: con = sqlite3.connect(fullpath) @@ -145,6 +149,7 @@ def get_latest_video_timestamp(channel_id: str, pass return datetime.fromtimestamp(timestamp) + def get_subscriptions(basepath: str = BASEPATH, filename: str = DB_FILE_NAME) -> list[tuple[str, str]]: subscriptions = list() @@ -223,17 +228,20 @@ def make_sized_button(parent_pnl: wx.Panel, bitmap_or_str: Union[wx.Bitmap, else: bitmap = bitmap_or_str btn_style = wx.BORDER_NONE | wx.BU_AUTODRAW | wx.BU_EXACTFIT | wx.BU_NOTEXT - btn_logo = wx.BitmapButton(parent_pnl, wx.ID_ANY, bitmap, style=btn_style) - btn_logo.SetMinSize(SIZE) + btn_logo = wx.BitmapButton(parent_pnl, + wx.ID_ANY, + bitmap, + style=btn_style, + size=bitmap.GetSize()) btn_logo.SetToolTip(text) btn_sizer.Add(btn_logo, 0, wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.TOP, 1) btn_text = wx.Button(parent_pnl, wx.ID_ANY, text, - style=wx.BORDER_NONE | wx.BU_AUTODRAW) - btn_text.SetMinSize( - wx.Size(SCREEN_WIDTH - SIZE.GetWidth(), SIZE.GetHeight())) + style=wx.BORDER_NONE | wx.BU_AUTODRAW, + size=wx.Size(SCREEN_WIDTH - SIZE.GetWidth(), + SIZE.GetHeight())) btn_text.SetToolTip(text) btn_sizer.Add(btn_text, 0, wx.BOTTOM | wx.RIGHT | wx.TOP | wx.EXPAND, 1) parent_pnl.Bind(wx.EVT_BUTTON, callback, btn_logo) @@ -307,6 +315,27 @@ 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, diff --git a/src/main.py b/src/main.py index 0a496ab..846427c 100644 --- a/src/main.py +++ b/src/main.py @@ -1,20 +1,19 @@ #!/usr/bin/env python3 -import mpv -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 youtubesearchpython import ChannelsSearch from Channel import SVT, YouTube from ChannelProvider import ChannelProvider -from Utils import get_subscriptions, import_from_newpipe, make_sized_button +from Utils import (get_subscriptions, import_from_newpipe, make_sized_button, + resolve_youtube_link) WIDTH = int(720 / 2) HEIGHT = int(1440 / 2) @@ -25,6 +24,9 @@ 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 +FLAGS = wx.SizerFlags() +FLAGS.Center() + class Cast(wx.Frame): def __init__(self, *args, **kw): @@ -35,6 +37,7 @@ class Cast(wx.Frame): super().__init__(*args, **kw) self.m_selected_chromecast = None self.SetSizeHints(WIDTH, HEIGHT, maxW=WIDTH) + self.m_style = self.GetWindowStyle() self.m_chromecast_thr = threading.Thread(target=self.get_chromecasts, args=(), kwargs={}) @@ -46,8 +49,6 @@ class Cast(wx.Frame): 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.m_selected_channel = None -# self.m_selected_provider_index = None self.show_provider_list(None) def add_back_button(self, callback: Callable) -> None: @@ -58,6 +59,31 @@ 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] + print(channels_search) + + 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] @@ -66,6 +92,71 @@ 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: + inner_sizer = wx.BoxSizer(wx.HORIZONTAL) + play_button = wx.Button(self.m_panel, + -1, + "Play", + size=(WIDTH / 4, BTN_HEIGHT)) + + pause_button = wx.Button(self.m_panel, + -1, + "Pause", + size=(WIDTH / 4, BTN_HEIGHT)) + + back_button = wx.Button(self.m_panel, + -1, + "Back", + size=(WIDTH / 4, BTN_HEIGHT)) + back_button.Bind( + wx.EVT_BUTTON, + lambda event, cindex=channel_index: self.show_video_list( + event, cindex), + ) + + inner_sizer.Add(play_button, FLAGS) + inner_sizer.Add(pause_button, FLAGS) + inner_sizer.Add(back_button, FLAGS) + + if not self.m_chromecast_thr.is_alive( + ) and not self.m_selected_chromecast: + chromecast_button = wx.Button(self.m_panel, + -1, + "Cast", + size=(WIDTH / 4, BTN_HEIGHT)) + chromecast_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: + + + 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) + 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) + inner_sizer.Fit(self) + inner_sizer.Layout() + + return inner_sizer + def get_providers(self) -> list[ChannelProvider]: providers = list() @@ -88,8 +179,9 @@ class Cast(wx.Frame): for channel in subscriptions: channels.append(YouTube.YouTube(channel[0], channel[1])) else: - channels.append(YouTube.YouTube("UCs6A_0Jm21SIvpdKyg9Gmxw", "Pine 64")) - + channels.append( + YouTube.YouTube("UCs6A_0Jm21SIvpdKyg9Gmxw", "Pine 64")) + youtube = ChannelProvider("YouTube", channels=channels) providers.append(youtube) @@ -97,11 +189,14 @@ class Cast(wx.Frame): 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: + 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 + return # the user changed their mind # Proceed loading the file chosen by the user subfile = file_dialog.GetPath() @@ -113,14 +208,14 @@ class Cast(wx.Frame): yt_chan = YouTube.YouTube(channel[0], channel[1]) 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) + 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) - # self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) closebtn = wx.Button(self.m_panel, -1, label="Close", @@ -154,9 +249,7 @@ class Cast(wx.Frame): self.add_back_button(bck_callback) if self.m_selected_provider.get_name() == "YouTube": - 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) + self.add_youtube_buttons() channel_index = 0 @@ -175,37 +268,42 @@ class Cast(wx.Frame): self.m_sizer.Fit(self) self.m_sizer.Layout() - def show_video_list(self, _,channel_index) -> None: - self.Show() - self.m_selected_channel = self.m_selected_provider.get_channel_by_index(channel_index) + def show_video_list(self, _, channel_index) -> None: + self.SetWindowStyle(self.m_style) + self.m_selected_channel = self.m_selected_provider.get_channel_by_index( + channel_index) self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) - # self.m_sizer.AddSpacer(SPACER_HEIGHT * 4) 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() == 'YouTube' or ( + 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: + 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(): + 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.show_video_list(event,channel_index) + self.show_video_list(event, channel_index) refreshbtn = wx.Button(self.m_panel, - -1, - label="Refresh", - size=(WIDTH, BTN_HEIGHT)) + -1, + label="Refresh", + size=(WIDTH, BTN_HEIGHT)) refreshbtn.Bind(wx.EVT_BUTTON, refresh_callback) self.m_sizer.Add(refreshbtn) @@ -215,7 +313,6 @@ class Cast(wx.Frame): time.sleep(1) wx.GetApp().Yield() - btnindex = 0 for item in self.m_selected_channel.get_items(): # type: ignore inner_sizer = wx.BoxSizer(wx.VERTICAL) @@ -228,7 +325,8 @@ class Cast(wx.Frame): btn = wx.BitmapButton(self.m_panel, id=btnindex, bitmap=bitmap, - style=BM_BTN_STYLE) + style=BM_BTN_STYLE, + size=bitmap.GetSize()) btn.Bind( wx.EVT_BUTTON, lambda event, link=item["link"], cindex=channel_index: self. @@ -236,7 +334,10 @@ class Cast(wx.Frame): ) inner_sizer.Add(title) inner_sizer.Add(btn) - collapsable_pane = wx.CollapsiblePane(self.m_panel, wx.ID_ANY, "Details:") + collapsable_pane = wx.CollapsiblePane(self.m_panel, + wx.ID_ANY, + "Details:", + size=(WIDTH, 20)) inner_sizer.Add(collapsable_pane, 0, wx.GROW | wx.ALL, 5) pane_win = collapsable_pane.GetPane() description = wx.StaticText(pane_win, -1, item["description"]) @@ -245,6 +346,7 @@ class Cast(wx.Frame): pane_sizer.Add(description, wx.GROW | wx.ALL, 2) pane_win.SetSizer(pane_sizer) pane_sizer.SetSizeHints(pane_win) + def fit_and_layout(_): pane_sizer.Layout() pane_sizer.Fit(pane_win) @@ -253,7 +355,8 @@ class Cast(wx.Frame): self.m_sizer.Fit(self) self.m_sizer.Layout() - collapsable_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, lambda event: fit_and_layout(event) ) + collapsable_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, + lambda event: fit_and_layout(event)) #inner_sizer.Add(description) self.m_sizer.Add(inner_sizer) self.m_sizer.AddSpacer(SPACER_HEIGHT) @@ -263,84 +366,48 @@ class Cast(wx.Frame): self.m_sizer.Fit(self) self.m_sizer.Layout() - def show_player(self, _, uri, channel_index: int): + def show_player(self, _, uri: str, channel_index: int): """ Show the video player :param _ event: unused :param uri str: the link to the video stream """ + if 'youtube' in uri: + uri = resolve_youtube_link(uri) self.m_sizer.Clear(delete_windows=True) self.m_sizer = wx.BoxSizer(wx.VERTICAL) - inner_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.m_control = wx.media.MediaCtrl( - self.m_panel, - size=(0,0), - szBackend=wx.media.MEDIABACKEND_GSTREAMER, - ) - play_button = wx.Button(self.m_panel, -1, "Play") - - pause_button = wx.Button(self.m_panel, -1, "Pause") - - back_button = wx.Button(self.m_panel, -1, "Back") - back_button.Bind( - wx.EVT_BUTTON, - lambda event, cindex=channel_index: self.show_video_list(event, cindex), - ) - - self.m_control.Show() - self.m_sizer.Add(self.m_control) - inner_sizer.Add(play_button) - inner_sizer.Add(pause_button) - inner_sizer.Add(back_button) - - 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, cindex=channel_index: self.select_chromecast(event, muri, cindex), + if not self.m_selected_chromecast: + self.m_control = wx.media.MediaCtrl( + self.m_panel, + szBackend=wx.media.MEDIABACKEND_GSTREAMER, ) - inner_sizer.Add(chromecast_button) - self.m_sizer.Add(inner_sizer) - - if self.m_selected_chromecast: - self.Bind( - wx.media.EVT_MEDIA_LOADED, - lambda event, muri=uri: self.cast(event, muri), - ) - play_button.Bind(wx.EVT_BUTTON, self.play_cast) - pause_button.Bind(wx.EVT_BUTTON, self.pause_cast) - else: - self.Bind(wx.media.EVT_MEDIA_LOADED, self.play) - play_button.Bind(wx.EVT_BUTTON, self.play) - pause_button.Bind(wx.EVT_BUTTON, self.pause) - - 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.SetSizeHints(minW=WIDTH, minH=-1, maxH=HEIGHT) - self.load_uri(uri) + 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) self.m_sizer.Fit(self) self.m_sizer.Layout() - self.Hide() def select_chromecast(self, _, uri, channel_index): self.m_sizer.Clear(delete_windows=True) 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) + size=(WIDTH, BTN_HEIGHT)) cancel_btn.Bind( wx.EVT_BUTTON, lambda event, cindex=channel_index: self.show_video_list( event, cindex), ) - self.m_sizer.Add(cancel_btn) #, wx.ALIGN_CENTER_VERTICAL) + self.m_sizer.Add(cancel_btn) for cast in self.m_chromecasts: friendly_name = cast.cast_info.friendly_name @@ -353,7 +420,7 @@ class Cast(wx.Frame): lambda event, chromecast=cast, muri=uri, cindex=channel_index: self.set_chromecast(event, chromecast, muri, cindex), ) - self.m_sizer.Add(btn) #, wx.ALIGN_CENTER_VERTICAL) + self.m_sizer.Add(btn) self.m_panel.SetupScrolling(rate_y=SCROLL_RATE, scrollToTop=True) self.m_panel.SetSizer(self.m_sizer) self.m_sizer.Fit(self) @@ -375,7 +442,6 @@ class Cast(wx.Frame): while True: if player_state != cast.media_controller.status.player_state: player_state = cast.media_controller.status.player_state - # print("Player state:", player_state) if player_state == "PLAYING": has_played = True @@ -392,12 +458,32 @@ class Cast(wx.Frame): def pause_cast(self, event): cast = self.m_selected_chromecast + cast.wait() cast.media_controller.pause() def play_cast(self, event): cast = self.m_selected_chromecast + cast.wait() cast.media_controller.play() + def stop_cast(self, event): + cast = self.m_selected_chromecast + cast.wait() + cast.media_controller.pause() + time.sleep(2) + cast.media_controller.stop() + self.m_selected_chromecast = None + + def stop_callback(self, event, uri: str, channel_index: int): + self.stop_cast(event) + self.m_sizer.Clear(delete_windows=True) + self.m_sizer = wx.BoxSizer(wx.VERTICAL) + self.m_sizer.Add(self.get_player_controls(channel_index, uri)) + 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 load_uri(self, uri): self.m_control.LoadURI(uri)