mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-31 00:11:59 +02:00
Prefetch YouTube video info on hover
Speed up loading of YouTube videos by downloading and parsing their info before video is dropped into player.
This commit is contained in:
@@ -26,6 +26,7 @@ class ClapperPlayer extends PlayerBase
|
|||||||
this.needsTocUpdate = true;
|
this.needsTocUpdate = true;
|
||||||
|
|
||||||
this.keyPressCount = 0;
|
this.keyPressCount = 0;
|
||||||
|
this.ytClient = null;
|
||||||
|
|
||||||
const keyController = new Gtk.EventControllerKey();
|
const keyController = new Gtk.EventControllerKey();
|
||||||
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
|
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
|
||||||
@@ -78,8 +79,10 @@ class ClapperPlayer extends PlayerBase
|
|||||||
|
|
||||||
async getYouTubeUriAsync(videoId)
|
async getYouTubeUriAsync(videoId)
|
||||||
{
|
{
|
||||||
const client = new YouTube.YouTubeClient();
|
if(!this.ytClient)
|
||||||
const info = await client.getVideoInfoPromise(videoId).catch(debug);
|
this.ytClient = new YouTube.YouTubeClient();
|
||||||
|
|
||||||
|
const info = await this.ytClient.getVideoInfoPromise(videoId).catch(debug);
|
||||||
|
|
||||||
if(!info)
|
if(!info)
|
||||||
throw new Error('no YouTube video info');
|
throw new Error('no YouTube video info');
|
||||||
@@ -87,7 +90,7 @@ class ClapperPlayer extends PlayerBase
|
|||||||
const dash = Dash.generateDash(info);
|
const dash = Dash.generateDash(info);
|
||||||
const videoUri = (dash)
|
const videoUri = (dash)
|
||||||
? await Dash.saveDashPromise(dash).catch(debug)
|
? await Dash.saveDashPromise(dash).catch(debug)
|
||||||
: client.getBestCombinedUri(info);
|
: this.ytClient.getBestCombinedUri(info);
|
||||||
|
|
||||||
if(!videoUri)
|
if(!videoUri)
|
||||||
throw new Error('no YouTube video URI');
|
throw new Error('no YouTube video URI');
|
||||||
|
@@ -4,6 +4,7 @@ const Debug = imports.src.debug;
|
|||||||
const Dialogs = imports.src.dialogs;
|
const Dialogs = imports.src.dialogs;
|
||||||
const Misc = imports.src.misc;
|
const Misc = imports.src.misc;
|
||||||
const { Player } = imports.src.player;
|
const { Player } = imports.src.player;
|
||||||
|
const YouTube = imports.src.youtube;
|
||||||
const Revealers = imports.src.revealers;
|
const Revealers = imports.src.revealers;
|
||||||
|
|
||||||
const { debug } = Debug;
|
const { debug } = Debug;
|
||||||
@@ -721,9 +722,11 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
{
|
{
|
||||||
const dropTarget = new Gtk.DropTarget({
|
const dropTarget = new Gtk.DropTarget({
|
||||||
actions: Gdk.DragAction.COPY,
|
actions: Gdk.DragAction.COPY,
|
||||||
|
preload: true,
|
||||||
});
|
});
|
||||||
dropTarget.set_gtypes([GObject.TYPE_STRING]);
|
dropTarget.set_gtypes([GObject.TYPE_STRING]);
|
||||||
dropTarget.connect('drop', this._onDataDrop.bind(this));
|
dropTarget.connect('drop', this._onDataDrop.bind(this));
|
||||||
|
dropTarget.connect('notify::value', this._onDropValueNotify.bind(this));
|
||||||
|
|
||||||
return dropTarget;
|
return dropTarget;
|
||||||
}
|
}
|
||||||
@@ -897,6 +900,31 @@ class ClapperWidget extends Gtk.Grid
|
|||||||
this.posY = posY;
|
this.posY = posY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDropValueNotify(dropTarget)
|
||||||
|
{
|
||||||
|
if(!dropTarget.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const uris = dropTarget.value.split(/\r?\n/);
|
||||||
|
const firstUri = uris[0];
|
||||||
|
|
||||||
|
if(uris.length > 1 || !Gst.uri_is_valid(firstUri))
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Check if user is dragging a YouTube link */
|
||||||
|
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(firstUri);
|
||||||
|
if(!isYouTubeUri) return;
|
||||||
|
|
||||||
|
/* Since this is a YouTube video,
|
||||||
|
* create YT client if it was not created yet */
|
||||||
|
if(!this.player.ytClient)
|
||||||
|
this.player.ytClient = new YouTube.YouTubeClient();
|
||||||
|
|
||||||
|
/* Speed up things by prefetching new video info before drop */
|
||||||
|
if(!this.player.ytClient.compareLastVideoId(videoId))
|
||||||
|
this.player.ytClient.getVideoInfoPromise(videoId).catch(debug);
|
||||||
|
}
|
||||||
|
|
||||||
_onDataDrop(dropTarget, value, x, y)
|
_onDataDrop(dropTarget, value, x, y)
|
||||||
{
|
{
|
||||||
const playlist = value.split(/\r?\n/).filter(uri => {
|
const playlist = value.split(/\r?\n/).filter(uri => {
|
||||||
|
106
src/youtube.js
106
src/youtube.js
@@ -4,27 +4,54 @@ const Debug = imports.src.debug;
|
|||||||
|
|
||||||
const { debug } = Debug;
|
const { debug } = Debug;
|
||||||
|
|
||||||
var YouTubeClient = GObject.registerClass(
|
var YouTubeClient = GObject.registerClass({
|
||||||
class ClapperYouTubeClient extends Soup.Session
|
Signals: {
|
||||||
|
'info-resolved': {
|
||||||
|
param_types: [GObject.TYPE_BOOLEAN]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, class ClapperYouTubeClient extends Soup.Session
|
||||||
{
|
{
|
||||||
_init()
|
_init()
|
||||||
{
|
{
|
||||||
super._init({
|
super._init({
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* videoID of current active download */
|
||||||
|
this.downloadingVideoId = null;
|
||||||
|
|
||||||
|
this.downloadAborted = false;
|
||||||
|
this.lastInfo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoInfoPromise(videoId)
|
getVideoInfoPromise(videoId)
|
||||||
{
|
{
|
||||||
return new Promise(async (resolve, reject) => {
|
/* If in middle of download and same videoID,
|
||||||
const url = `https://www.youtube.com/get_video_info?video_id=${videoId}&el=embedded`;
|
* resolve to current download */
|
||||||
let tries = 2;
|
if(
|
||||||
|
this.downloadingVideoId
|
||||||
|
&& this.downloadingVideoId === videoId
|
||||||
|
)
|
||||||
|
return this._getCurrentDownloadPromise();
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
/* Do not redownload info for the same video */
|
||||||
|
if(this.compareLastVideoId(videoId))
|
||||||
|
return resolve(this.lastInfo);
|
||||||
|
|
||||||
|
this.abort();
|
||||||
|
|
||||||
|
let tries = 2;
|
||||||
while(tries--) {
|
while(tries--) {
|
||||||
debug(`obtaining YouTube video info: ${videoId}`);
|
debug(`obtaining YouTube video info: ${videoId}`);
|
||||||
|
this.downloadingVideoId = videoId;
|
||||||
|
|
||||||
const info = await this._getInfoPromise(url).catch(debug);
|
const info = await this._getInfoPromise(videoId).catch(debug);
|
||||||
if(!info) {
|
if(!info) {
|
||||||
|
if(this.downloadAborted)
|
||||||
|
return reject(new Error('download aborted'));
|
||||||
|
|
||||||
debug(`failed, remaining tries: ${tries}`);
|
debug(`failed, remaining tries: ${tries}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -33,16 +60,31 @@ class ClapperYouTubeClient extends Soup.Session
|
|||||||
if(
|
if(
|
||||||
!info.playabilityStatus
|
!info.playabilityStatus
|
||||||
|| !info.playabilityStatus.status === 'OK'
|
|| !info.playabilityStatus.status === 'OK'
|
||||||
)
|
) {
|
||||||
|
this.emit('info-resolved', false);
|
||||||
|
this.downloadingVideoId = null;
|
||||||
|
|
||||||
return reject(new Error('video is not playable'));
|
return reject(new Error('video is not playable'));
|
||||||
|
}
|
||||||
|
|
||||||
/* Check if data contains streaming URIs */
|
/* Check if data contains streaming URIs */
|
||||||
if(!info.streamingData)
|
if(!info.streamingData) {
|
||||||
|
this.emit('info-resolved', false);
|
||||||
|
this.downloadingVideoId = null;
|
||||||
|
|
||||||
return reject(new Error('video response data is missing URIs'));
|
return reject(new Error('video response data is missing URIs'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastInfo = info;
|
||||||
|
this.emit('info-resolved', true);
|
||||||
|
this.downloadingVideoId = null;
|
||||||
|
|
||||||
return resolve(info);
|
return resolve(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('info-resolved', false);
|
||||||
|
this.downloadingVideoId = null;
|
||||||
|
|
||||||
reject(new Error('could not obtain YouTube video info'));
|
reject(new Error('could not obtain YouTube video info'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -65,9 +107,45 @@ class ClapperYouTubeClient extends Soup.Session
|
|||||||
return combinedStream.url;
|
return combinedStream.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getInfoPromise(url)
|
compareLastVideoId(videoId)
|
||||||
|
{
|
||||||
|
if(!this.lastInfo)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(
|
||||||
|
!this.lastInfo
|
||||||
|
|| !this.lastInfo.videoDetails
|
||||||
|
|| this.lastInfo.videoDetails.videoId !== videoId
|
||||||
|
/* TODO: check if video expired */
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCurrentDownloadPromise()
|
||||||
|
{
|
||||||
|
debug('resolving after current download finishes');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const infoResolvedSignal = this.connect('info-resolved', (self, success) => {
|
||||||
|
this.disconnect(infoResolvedSignal);
|
||||||
|
|
||||||
|
debug('current download finished, resolving');
|
||||||
|
|
||||||
|
if(!success)
|
||||||
|
return reject(new Error('info resolve was unsuccessful'));
|
||||||
|
|
||||||
|
/* At this point new video info is set */
|
||||||
|
resolve(this.lastInfo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInfoPromise(videoId)
|
||||||
{
|
{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = `https://www.youtube.com/get_video_info?video_id=${videoId}&el=embedded`;
|
||||||
const message = Soup.Message.new('GET', url);
|
const message = Soup.Message.new('GET', url);
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
@@ -85,8 +163,14 @@ class ClapperYouTubeClient extends Soup.Session
|
|||||||
|
|
||||||
debug('got message response');
|
debug('got message response');
|
||||||
|
|
||||||
if(msg.status_code !== 200)
|
const statusCode = msg.status_code;
|
||||||
return reject(new Error(`response code: ${msg.status_code}`));
|
|
||||||
|
/* Internal Soup codes mean download abort
|
||||||
|
* or some other error that cannot be handled */
|
||||||
|
this.downloadAborted = (statusCode < 10);
|
||||||
|
|
||||||
|
if(statusCode !== 200)
|
||||||
|
return reject(new Error(`response code: ${statusCode}`));
|
||||||
|
|
||||||
debug('parsing video info JSON');
|
debug('parsing video info JSON');
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user