mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 07:42:23 +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.keyPressCount = 0;
|
||||
this.ytClient = null;
|
||||
|
||||
const keyController = new Gtk.EventControllerKey();
|
||||
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
|
||||
@@ -78,8 +79,10 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
async getYouTubeUriAsync(videoId)
|
||||
{
|
||||
const client = new YouTube.YouTubeClient();
|
||||
const info = await client.getVideoInfoPromise(videoId).catch(debug);
|
||||
if(!this.ytClient)
|
||||
this.ytClient = new YouTube.YouTubeClient();
|
||||
|
||||
const info = await this.ytClient.getVideoInfoPromise(videoId).catch(debug);
|
||||
|
||||
if(!info)
|
||||
throw new Error('no YouTube video info');
|
||||
@@ -87,7 +90,7 @@ class ClapperPlayer extends PlayerBase
|
||||
const dash = Dash.generateDash(info);
|
||||
const videoUri = (dash)
|
||||
? await Dash.saveDashPromise(dash).catch(debug)
|
||||
: client.getBestCombinedUri(info);
|
||||
: this.ytClient.getBestCombinedUri(info);
|
||||
|
||||
if(!videoUri)
|
||||
throw new Error('no YouTube video URI');
|
||||
|
@@ -4,6 +4,7 @@ const Debug = imports.src.debug;
|
||||
const Dialogs = imports.src.dialogs;
|
||||
const Misc = imports.src.misc;
|
||||
const { Player } = imports.src.player;
|
||||
const YouTube = imports.src.youtube;
|
||||
const Revealers = imports.src.revealers;
|
||||
|
||||
const { debug } = Debug;
|
||||
@@ -721,9 +722,11 @@ class ClapperWidget extends Gtk.Grid
|
||||
{
|
||||
const dropTarget = new Gtk.DropTarget({
|
||||
actions: Gdk.DragAction.COPY,
|
||||
preload: true,
|
||||
});
|
||||
dropTarget.set_gtypes([GObject.TYPE_STRING]);
|
||||
dropTarget.connect('drop', this._onDataDrop.bind(this));
|
||||
dropTarget.connect('notify::value', this._onDropValueNotify.bind(this));
|
||||
|
||||
return dropTarget;
|
||||
}
|
||||
@@ -897,6 +900,31 @@ class ClapperWidget extends Gtk.Grid
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
var YouTubeClient = GObject.registerClass(
|
||||
class ClapperYouTubeClient extends Soup.Session
|
||||
var YouTubeClient = GObject.registerClass({
|
||||
Signals: {
|
||||
'info-resolved': {
|
||||
param_types: [GObject.TYPE_BOOLEAN]
|
||||
}
|
||||
}
|
||||
}, class ClapperYouTubeClient extends Soup.Session
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
timeout: 5,
|
||||
});
|
||||
|
||||
/* videoID of current active download */
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
this.downloadAborted = false;
|
||||
this.lastInfo = null;
|
||||
}
|
||||
|
||||
getVideoInfoPromise(videoId)
|
||||
{
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const url = `https://www.youtube.com/get_video_info?video_id=${videoId}&el=embedded`;
|
||||
let tries = 2;
|
||||
/* If in middle of download and same videoID,
|
||||
* resolve to current download */
|
||||
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--) {
|
||||
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(this.downloadAborted)
|
||||
return reject(new Error('download aborted'));
|
||||
|
||||
debug(`failed, remaining tries: ${tries}`);
|
||||
continue;
|
||||
}
|
||||
@@ -33,16 +60,31 @@ class ClapperYouTubeClient extends Soup.Session
|
||||
if(
|
||||
!info.playabilityStatus
|
||||
|| !info.playabilityStatus.status === 'OK'
|
||||
)
|
||||
) {
|
||||
this.emit('info-resolved', false);
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
return reject(new Error('video is not playable'));
|
||||
}
|
||||
|
||||
/* 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'));
|
||||
}
|
||||
|
||||
this.lastInfo = info;
|
||||
this.emit('info-resolved', true);
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
return resolve(info);
|
||||
}
|
||||
|
||||
this.emit('info-resolved', false);
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
reject(new Error('could not obtain YouTube video info'));
|
||||
});
|
||||
}
|
||||
@@ -65,9 +107,45 @@ class ClapperYouTubeClient extends Soup.Session
|
||||
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) => {
|
||||
const url = `https://www.youtube.com/get_video_info?video_id=${videoId}&el=embedded`;
|
||||
const message = Soup.Message.new('GET', url);
|
||||
let data = '';
|
||||
|
||||
@@ -85,8 +163,14 @@ class ClapperYouTubeClient extends Soup.Session
|
||||
|
||||
debug('got message response');
|
||||
|
||||
if(msg.status_code !== 200)
|
||||
return reject(new Error(`response code: ${msg.status_code}`));
|
||||
const statusCode = 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');
|
||||
|
||||
|
Reference in New Issue
Block a user