From bd20d305ba9dfa53680bf8d17e9014927ee44f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Wed, 17 Mar 2021 10:38:39 +0100 Subject: [PATCH] YT: store reusable alive info in temp folder --- src/fileOps.js | 32 ++++++++++----- src/youtube.js | 104 +++++++++++++++++++++++++++++++------------------ 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/src/fileOps.js b/src/fileOps.js index f6ff5768..d452d76b 100644 --- a/src/fileOps.js +++ b/src/fileOps.js @@ -7,23 +7,37 @@ const { debug } = Debug; Gio._promisify(Gio._LocalFilePrototype, 'make_directory_async', 'make_directory_finish'); function createCacheDirPromise() +{ + const dir = Gio.File.new_for_path( + GLib.get_user_cache_dir() + '/' + Misc.appId + ); + + return createDirPromise(dir); +} + +function createTempDirPromise() +{ + const dir = Gio.File.new_for_path( + GLib.get_tmp_dir() + '/.' + Misc.appId + ); + + return createDirPromise(dir); +} + +function createDirPromise(dir) { return new Promise(async (resolve, reject) => { - const cacheDir = Gio.File.new_for_path( - GLib.get_user_cache_dir() + '/' + Misc.appId - ); + if(dir.query_exists(null)) + return resolve(dir); - if(cacheDir.query_exists(null)) - return resolve(cacheDir); - - const dirCreated = await cacheDir.make_directory_async( + const dirCreated = await dir.make_directory_async( GLib.PRIORITY_DEFAULT, null, ).catch(debug); if(!dirCreated) - return reject(new Error(`could not create dir: ${cacheDir.get_path()}`)); + return reject(new Error(`could not create dir: ${dir.get_path()}`)); - resolve(cacheDir); + resolve(dir); }); } diff --git a/src/youtube.js b/src/youtube.js index 5307b02c..525efa57 100644 --- a/src/youtube.js +++ b/src/youtube.js @@ -83,7 +83,34 @@ var YouTubeClient = GObject.registerClass({ debug(`obtaining YouTube video info: ${videoId}`); this.downloadingVideoId = videoId; - let result = await this._getInfoPromise(videoId).catch(debug); + let result; + let isFoundInTemp = false; + + const tempInfo = await this._getFileContentsPromise('tmp', 'yt-info', videoId).catch(debug); + if(tempInfo) { + debug('checking temp info for requested video'); + let parsedTempInfo; + + try { parsedTempInfo = JSON.parse(tempInfo); } + catch(err) { debug(err); } + + if(parsedTempInfo) { + const nowSeconds = Math.floor(Date.now() / 1000); + const { expireDate } = parsedTempInfo.streamingData; + + if(expireDate && expireDate > nowSeconds) { + debug(`found usable info, remaining live: ${expireDate - nowSeconds}`); + + isFoundInTemp = true; + result = { data: parsedTempInfo }; + } + else + debug('temp info expired'); + } + } + + if(!result) + result = await this._getInfoPromise(videoId).catch(debug); if(!result || !result.data) { if(result && result.isAborted) @@ -153,7 +180,7 @@ var YouTubeClient = GObject.registerClass({ debug(`found player URI: ${ytUri}`); const ytId = ytPath.split('/').find(el => Misc.isHex(el)); - actions = await this._getCacheFileActionsPromise(ytId).catch(debug); + actions = await this._getFileContentsPromise('user_cache', 'yt-sig', ytId).catch(debug); if(!actions) { result = await this._downloadDataPromise(ytUri).catch(debug); @@ -167,8 +194,8 @@ var YouTubeClient = GObject.registerClass({ actions = YTDL.sig.extractActions(result.data); if(actions) { - debug('deciphered'); - this._createCacheFileAsync(ytId, actions); + debug('deciphered, saving cipher actions to cache file'); + this._createSubdirFileAsync('user_cache', 'yt-sig', ytId, actions); } } if(!actions || !actions.length) { @@ -191,6 +218,19 @@ var YouTubeClient = GObject.registerClass({ } } + if(!isFoundInTemp) { + const exp = info.streamingData.expiresInSeconds || 0; + const len = info.videoDetails.lengthSeconds || 3; + + /* Estimated safe time for rewatching video */ + info.streamingData.expireDate = Math.floor(Date.now() / 1000) + + Number(exp) - (3 * len); + + this._createSubdirFileAsync( + 'tmp', 'yt-info', videoId, JSON.stringify(info) + ); + } + this.lastInfo = info; this.emit('info-resolved', true); this.downloadingVideoId = null; @@ -330,7 +370,7 @@ var YouTubeClient = GObject.registerClass({ let info = null; try { info = JSON.parse(playerResponse); } - catch(err) { debug(err.message) } + catch(err) { debug(err.message); } if(!info) return reject(new Error('could not parse video info JSON')); @@ -460,65 +500,53 @@ var YouTubeClient = GObject.registerClass({ return `${url}&${sig}=${key}`; } - async _createCacheFileAsync(ytId, actions) + async _createSubdirFileAsync(place, folderName, fileName, data) { - debug('saving cipher actions to cache file'); - - const ytCacheDir = Gio.File.new_for_path([ - GLib.get_user_cache_dir(), + const destDir = Gio.File.new_for_path([ + GLib[`get_${place}_dir`](), Misc.appId, - 'yt-sig' + folderName ].join('/')); - for(let dir of [ytCacheDir.get_parent(), ytCacheDir]) { - if(dir.query_exists(null)) - continue; - - const dirCreated = await dir.make_directory_async( - GLib.PRIORITY_DEFAULT, - null, - ).catch(debug); - - if(!dirCreated) { - debug(new Error(`could not create dir: ${dir.get_path()}`)); - return; - } + for(let dir of [destDir.get_parent(), destDir]) { + const createdDir = await FileOps.createDirPromise(dir).catch(debug); + if(!createdDir) return; } - const cacheFile = ytCacheDir.get_child(ytId); - cacheFile.replace_contents_bytes_async( - GLib.Bytes.new_take(actions), + const destFile = destDir.get_child(fileName); + destFile.replace_contents_bytes_async( + GLib.Bytes.new_take(data), null, false, Gio.FileCreateFlags.NONE, null ) - .then(() => debug('saved cache file')) + .then(() => debug(`saved file: ${destFile.get_path()}`)) .catch(debug); } - _getCacheFileActionsPromise(ytId) + _getFileContentsPromise(place, folderName, fileName) { return new Promise((resolve, reject) => { - debug('checking decipher actions from cache file'); + debug(`reading data from ${place} file`); - const ytActionsFile = Gio.File.new_for_path([ - GLib.get_user_cache_dir(), + const file = Gio.File.new_for_path([ + GLib[`get_${place}_dir`](), Misc.appId, - 'yt-sig', - ytId + folderName, + fileName ].join('/')); - if(!ytActionsFile.query_exists(null)) { - debug(`no such cache file: ${ytId}`); + if(!file.query_exists(null)) { + debug(`no such file: ${file.get_path()}`); return resolve(null); } - ytActionsFile.load_bytes_async(null) + file.load_bytes_async(null) .then(result => { const data = result[0].get_data(); if(!data || !data.length) - return reject(new Error('actions cache file is empty')); + return reject(new Error('source file is empty')); if(data instanceof Uint8Array) resolve(ByteArray.toString(data));