Commit 7bc79401 authored by Todd Weaver's avatar Todd Weaver
Browse files

Adding playlist support

parent 136098e9
......@@ -27,12 +27,23 @@ class Menu(Gtk.PopoverMenu):
__gtype_name__ = 'Menu'
autoplay_toggle = Gtk.Template.Child()
volume = Gtk.Template.Child()
def __init__(self, app_window, **kwargs):
super().__init__(**kwargs)
self.app_window = app_window
@Gtk.Template.Callback()
def volume_change(self, event, data):
self.volume_slider(event.get_value())
def volume_slider(self, volume_value):
list = self.app_window.get_scroller_list()
children = list.get_children()
for child in children:
child.get_child().player.set_property("volume", volume_value)
@Gtk.Template.Callback()
def show_about(self, data):
about = About()
......
......@@ -25,19 +25,23 @@ from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gst, Gtk
Gst.init(None)
Gst.init_check(None)
from .search import Search
@Gtk.Template(resource_path='/sm/puri/Stream/ui/results.ui')
class ResultsBox(Gtk.Box):
__gtype_name__ = 'ResultsBox'
event_box = Gtk.Template.Child()
player_box = Gtk.Template.Child()
poster_image = Gtk.Template.Child()
controls_box = Gtk.Template.Child()
event_box = Gtk.Template.Child()
play = Gtk.Template.Child()
pause = Gtk.Template.Child()
slider = Gtk.Template.Child()
time_viewed = Gtk.Template.Child()
time_remaining = Gtk.Template.Child()
playlist_overlay = Gtk.Template.Child()
playlist_label = Gtk.Template.Child()
audio_dl = Gtk.Template.Child()
audio_dl_image = Gtk.Template.Child()
......@@ -52,7 +56,8 @@ class ResultsBox(Gtk.Box):
channel = Gtk.Template.Child()
duration = Gtk.Template.Child()
window_to_player_box_padding = 28
window_to_results_margin = 8
window_to_player_box_margin = 28
def __init__(self, app_window, **kwargs):
super().__init__(**kwargs)
......@@ -63,7 +68,7 @@ class ResultsBox(Gtk.Box):
self.event_box.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
# do ratio calculation from width (16:9 or 1.77)
self.video_box_width = int(self.app_window.app_orig_width - self.window_to_player_box_padding)
self.video_box_width = int(self.app_window.app_orig_width - self.window_to_player_box_margin)
self.video_box_height = int(self.video_box_width / 1.77)
self.player_box.set_size_request(self.video_box_width, self.video_box_height)
......@@ -110,26 +115,47 @@ class ResultsBox(Gtk.Box):
stream = poster_file.read_async(0, None,
self.on_file_read, None)
def setup_stream(self, video_meta):
self.video_id = video_meta['videoId']
self.video_title = video_meta['title']
self.video_channel = video_meta['author']
self.poster_uri = video_meta['poster_uri']
self.video_duration = self.get_readable_seconds(video_meta['lengthSeconds'])
def setup_stream(self, meta):
if 'type' in meta:
self.type = meta['type']
if meta['type'] == 'video':
self.setup_meta(meta)
self.setup_video(meta)
elif meta['type'] == 'playlist':
self.setup_meta(meta)
self.setup_playlist(meta)
def setup_meta(self, meta):
self.meta_title = meta['title']
meta_channel = meta['author']
poster_uri = meta['poster_uri']
# initialize video duration
self.video_duration = 0
self.title.set_label(self.meta_title)
self.channel.set_label(meta_channel)
poster_file = Gio.File.new_for_uri(poster_uri)
self.stream_at_scale_async(poster_file)
self.title.set_label(self.video_title)
self.channel.set_label(self.video_channel)
self.duration.set_label(self.video_duration)
def setup_playlist(self, playlist_meta):
self.app_window.playlist_id = playlist_meta['playlistId']
self.playlist_overlay.set_visible(True)
self.controls_box.set_visible(False)
# switch to playlist display
self.duration.set_visible(False)
self.playlist_label.set_visible(True)
def setup_video(self, video_meta):
self.time_viewed.set_label('0:00')
self.video_duration = self.get_readable_seconds(video_meta['lengthSeconds'])
self.duration.set_label(self.video_duration)
self.time_remaining.set_label(f"-{self.video_duration}")
self.player.set_property("uri", video_meta['video_uri'])
self.player.set_property("video-sink", self.sink)
poster_file = Gio.File.new_for_uri(video_meta['poster_uri'])
self.stream_at_scale_async(poster_file)
if 'audio_dl_uri' in video_meta:
if video_meta['audio_dl_uri']:
# enable download button
......@@ -193,7 +219,7 @@ class ResultsBox(Gtk.Box):
self.show_error_icon('audio')
return False
dest_title = self.strictify_name(self.video_title)
dest_title = self.strictify_name(self.meta_title)
dest_path = f"{dest_dir}/{dest_title}.{dest_ext}"
dest = Gio.File.new_for_path(dest_path)
dl_stream.copy_async(dest, Gio.FileCopyFlags.OVERWRITE,
......@@ -210,7 +236,7 @@ class ResultsBox(Gtk.Box):
self.show_error_icon('video')
return False
dest_title = self.strictify_name(self.video_title)
dest_title = self.strictify_name(self.meta_title)
dest_path = f"{dest_dir}/{dest_title}.{dest_ext}"
dest = Gio.File.new_for_path(dest_path)
flags = Gio.FileCopyFlags
......@@ -278,10 +304,11 @@ class ResultsBox(Gtk.Box):
self.download_video_uri(self.video_dl_uri)
def box_grab_focus(self):
if self.app_window.results_list.get_focus_child():
list = self.app_window.get_scroller_list()
if list.get_focus_child():
# grab the parent flowboxchild and focus it
# to snap it into frame
self.app_window.results_list.get_focus_child().grab_focus()
list.get_focus_child().grab_focus()
# grab the result box to enable future focus snapping
self.grab_focus()
......@@ -306,7 +333,7 @@ class ResultsBox(Gtk.Box):
# allow seeking
self.slider.set_sensitive(True)
app_volume = self.app_window.volume.get_value()
app_volume = self.app_window.menu.volume.get_value()
self.player.set_property("volume", app_volume)
# update slider to track video time in slider
......@@ -347,11 +374,11 @@ class ResultsBox(Gtk.Box):
self.unfullscreen.set_visible(True)
self.app_window.fullscreen()
self.app_window.is_fullscreen = True
self.app_window.search_bar_toggle.set_active(False)
self.app_window.search_bar.set_visible(False)
self.app_window.header_bar.set_visible(False)
self.details.set_visible(False)
self.app_window.search_bar.set_visible(False)
set_width = int(Gdk.Screen.get_default().get_width())
set_height = int(Gdk.Screen.get_default().get_height())
self.resize_player(set_width, set_height)
......@@ -373,16 +400,18 @@ class ResultsBox(Gtk.Box):
self.unfullscreen.set_visible(False)
self.app_window.unfullscreen()
self.app_window.is_fullscreen = False
self.app_window.search_bar_toggle.set_active(True)
self.app_window.search_bar.set_visible(True)
# if on scroller show search toggle active result
self.app_window.header_bar.set_visible(True)
self.details.set_visible(True)
if not self.app_window.playlist_scroller.get_visible():
self.app_window.search_bar.set_visible(self.app_window.search_bar_toggle.get_active())
results_context = self.get_style_context()
results_context.remove_class("fullscreen")
results_context.add_class("results")
set_width = int(self.app_window.app_orig_width - self.window_to_player_box_padding)
set_width = int(self.app_window.app_orig_width - self.window_to_player_box_margin)
set_height = int(set_width / 1.77)
self.resize_player(set_width, set_height)
......@@ -430,24 +459,44 @@ class ResultsBox(Gtk.Box):
def poll_mouse(self):
now_is = int(GLib.get_current_time())
if (int(now_is) - (self.last_move)) >= 1:
if (int(now_is) - (self.last_move)) >= 2:
if self.controls_box.get_visible():
self.controls_box.set_visible(False)
@Gtk.Template.Callback()
def event_box_mouse_click(self, event, data):
if self.app_window.is_playing:
self.pause_button(None)
def event_mouse_click(self, event, data):
if self.playlist_overlay.get_visible():
self.app_window.pause_all(self)
self.app_window.clear_playlist(0, 0, None)
self.app_window.search_bar_toggle.set_visible(False)
self.app_window.search_bar.set_visible(False)
self.app_window.header_bar.set_property('title', "Playlist")
self.app_window.back_button.set_visible(True)
self.app_window.scroller.set_visible(False)
self.app_window.playlist_scroller.set_visible(True)
self.app_window.playlist_search = Search(app_window = self.app_window,
toggle_status_spinner = self.app_window.toggle_status_spinner,
scroller = self.app_window.playlist_scroller,
add_result_meta = self.app_window.add_playlist_result_meta)
self.app_window.playlist_search.do_playlist(playlist_id = self.app_window.playlist_id, page = self.app_window.page_playlist)
else:
self.play_button(None)
self.event_box_mouse_action(event, data)
if self.app_window.is_playing:
self.pause_button(None)
else:
self.play_button(None)
self.event_mouse_action(event, data)
@Gtk.Template.Callback()
def event_box_mouse_action(self, event, data):
self.last_move = int(GLib.get_current_time())
GLib.timeout_add_seconds(2, self.poll_mouse)
if not self.controls_box.get_visible():
self.controls_box.set_visible(True)
def event_mouse_action(self, event, data):
if not self.playlist_overlay.get_visible():
self.last_move = int(GLib.get_current_time())
GLib.timeout_add_seconds(2, self.poll_mouse)
if not self.controls_box.get_visible():
self.controls_box.set_visible(True)
@Gtk.Template.Callback()
def swallow_slider_scroll_event(self, event, data):
......
......@@ -33,6 +33,7 @@ class Search:
self.this_instance = self.app_window.strong_instances[self.si_index]
self.search_video_ids = []
self.search_playlist_ids = []
# limited access
self.add_result_meta = kwargs.get('add_result_meta', None)
......@@ -41,7 +42,7 @@ class Search:
self.toggle_status_spinner(True)
self.query = query
esc_query = GLib.uri_escape_string(self.query, None, None)
uri = f"{self.this_instance}/api/v1/search?q={esc_query};page={page};fields=title,videoId,author,lengthSeconds,videoThumbnails"
uri = f"{self.this_instance}/api/v1/search?q={esc_query};page={page};type=all;fields=type,title,videoId,playlistId,author,lengthSeconds,videoThumbnails,videoCount,videos"
#print(uri)
self.session = Soup.Session.new()
......@@ -64,18 +65,61 @@ class Search:
"The streaming server response failed to parse results.")
return False
for video_meta in self.search_json:
if video_meta['videoId'] not in self.search_video_ids:
self.get_poster_url(video_meta)
self.get_video_details(video_meta)
for meta in self.search_json:
if meta['type'] == 'video':
if meta['videoId'] not in self.search_video_ids:
video_meta = meta
self.get_poster_url(video_meta, meta)
self.get_video_details(meta)
elif meta['type'] == 'playlist':
if meta['playlistId'] not in self.search_playlist_ids:
if 'videos' in meta:
first_video_meta = meta['videos'][0]
self.get_poster_url(first_video_meta, meta)
self.append_playlist(meta)
elif meta['type'] == 'channel':
print('channel')
def do_playlist(self, playlist_id, page):
self.toggle_status_spinner(True)
#/api/v1/playlists/:plid
uri = f"{self.this_instance}/api/v1/playlists/{playlist_id}?page={page};fields=videos"
#print(uri)
self.session = Soup.Session.new()
self.session.set_property("timeout", 5)
message = Soup.Message.new("GET", uri)
self.session.queue_message(message, self.show_playlist_results, page)
def show_playlist_results(self, session, results, page):
if results.status_code != 200:
if page == 1:
self.app_window.show_error_box("Service Failure",
"There is no response from the streaming servers.")
return False
def get_poster_url(self, video_meta):
for poster in video_meta['videoThumbnails']:
try:
self.search_json = json.loads(results.response_body.data)
except:
if page == 1:
self.app_window.show_error_box("Service Failure",
"The streaming server response failed to parse results.")
return False
if 'videos' in self.search_json:
for meta in self.search_json['videos']:
meta['type'] = 'video'
video_meta = meta
self.get_poster_url(video_meta, meta)
self.get_video_details(meta)
def get_poster_url(self, meta, append_meta):
for poster in meta['videoThumbnails']:
if poster['quality'] == 'medium':
if poster['url'].startswith('/'):
video_meta['poster_uri'] = f"{self.this_instance}{poster['url']}"
append_meta['poster_uri'] = f"{self.this_instance}{poster['url']}"
else:
video_meta['poster_uri'] = poster['url']
append_meta['poster_uri'] = poster['url']
def get_video_details(self, video_meta):
video_id = video_meta['videoId']
......@@ -144,6 +188,17 @@ class Search:
self.toggle_status_spinner(False)
self.scroller.set_visible(True)
def append_playlist(self, playlist_meta):
# add the playlist to the list
# which will trigger the playlist to display to the user
self.add_result_meta(playlist_meta)
# appending known playable playlists to filter duplicates
self.search_playlist_ids.append(playlist_meta['playlistId'])
self.toggle_status_spinner(False)
self.scroller.set_visible(True)
def get_download_uris(self, video_meta):
# get download link urls based on (future) user-config
# video quality: ["480p", "720p", "1080p"] # default 720p
......
......@@ -2,6 +2,11 @@
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="volume_adjustment">
<property name="upper">1</property>
<property name="step-increment">.05</property>
<property name="page-increment">.1</property>
</object>
<template class="Menu" parent="GtkPopoverMenu">
<property name="can-focus">False</property>
<child>
......@@ -14,6 +19,53 @@
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-left">6</property>
<property name="margin-start">6</property>
<property name="label" translatable="yes">Volume</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkVolumeButton" id="volume">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="halign">end</property>
<property name="adjustment">volume_adjustment</property>
<property name="orientation">horizontal</property>
<signal name="value-changed" handler="volume_change" swapped="no"/>
<property name="value">0.90</property>
<property name="icons">audio-volume-muted-symbolic
audio-volume-high-symbolic
audio-volume-low-symbolic
audio-volume-medium-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
......@@ -47,7 +99,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
<child>
......@@ -58,7 +110,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
<child>
......@@ -73,7 +125,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
<child>
......@@ -88,7 +140,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
<child>
......@@ -103,7 +155,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
</object>
......
......@@ -14,7 +14,6 @@
<property name="valign">start</property>
<property name="orientation">vertical</property>
<property name="baseline-position">top</property>
<signal name="motion-notify-event" handler="mouse_move" swapped="no"/>
<child>
<object class="GtkOverlay" id="video_overlay">
<property name="visible">True</property>
......@@ -60,8 +59,8 @@
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="above-child">True</property>
<signal name="button-press-event" handler="event_box_mouse_click" swapped="no"/>
<signal name="motion-notify-event" handler="event_box_mouse_action" swapped="no"/>
<signal name="button-press-event" handler="event_mouse_click" swapped="no"/>
<signal name="motion-notify-event" handler="event_mouse_action" swapped="no"/>
<child>
<placeholder/>
</child>
......@@ -70,6 +69,69 @@
<property name="pass-through">True</property>
</packing>
</child>
<child type="overlay">
<object class="GtkBox" id="playlist_overlay">
<property name="sensitive">False</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="margin-start">48</property>
<property name="icon-name">content-loading-symbolic</property>
<property name="icon_size">3</property>
<style>
<class name="playlist-icon"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-end">48</property>
<property name="icon-name">go-next-symbolic</property>
<property name="icon_size">3</property>
<style>
<class name="playlist-icon"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="playlist-overlay"/>
</style>
</object>
<packing>
<property name="pass-through">True</property>
<property name="index">1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkBox" id="controls_box">
<property name="visible">True</property>
......@@ -425,7 +487,7 @@
</child>
</object>
<packing>
<property name="index">1</property>
<property name="index">2</property>
</packing>
</child>
</object>
......@@ -441,20 +503,33 @@
<property name="can-focus">False</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="title">
<object class="GtkBox">
<property name="width-request">280</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">False</property>
<property name="label">...</property>
<property name="ellipsize">end</property>
<property name="max-width-chars">32</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1"/>
</attributes>
<child>
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label">...</property>
<property name="ellipsize">end</property>
<property name="single-line-mode">True</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
......@@ -483,16 +558,38 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="duration">
<object class="GtkStack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="label">...</property>