import lbryio from "./lbryio.js"; import lighthouse from "./lighthouse.js"; import jsonrpc from "./jsonrpc.js"; import lbryuri from "./lbryuri.js"; import { getLocal, getSession, setSession, setLocal } from "./utils.js"; const { remote, ipcRenderer } = require("electron"); const menu = remote.require("./menu/main-menu"); let lbry = { isConnected: false, daemonConnectionString: "http://localhost:5279/lbryapi", pendingPublishTimeout: 20 * 60 * 1000, defaultClientSettings: { showNsfw: false, showUnavailable: true, debug: false, useCustomLighthouseServers: false, customLighthouseServers: [], showDeveloperMenu: false, language: "en", }, }; function apiCall(method, params, resolve, reject) { return jsonrpc.call( lbry.daemonConnectionString, method, params, resolve, reject, reject ); } /** * Records a publish attempt in local storage. Returns a dictionary with all the data needed to * needed to make a dummy claim or file info object. */ let pendingId = 0; function savePendingPublish({ name, channel_name }) { let uri; if (channel_name) { uri = lbryuri.build({ name: channel_name, path: name }, false); } else { uri = lbryuri.build({ name: name }, false); } ++pendingId; const pendingPublishes = getLocal("pendingPublishes") || []; const newPendingPublish = { name, channel_name, claim_id: "pending-" + pendingId, txid: "pending-" + pendingId, nout: 0, outpoint: "pending-" + pendingId + ":0", time: Date.now(), }; setLocal("pendingPublishes", [...pendingPublishes, newPendingPublish]); return newPendingPublish; } /** * If there is a pending publish with the given name or outpoint, remove it. * A channel name may also be provided along with name. */ function removePendingPublishIfNeeded({ name, channel_name, outpoint }) { function pubMatches(pub) { return ( pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name)) ); } setLocal( "pendingPublishes", lbry.getPendingPublishes().filter(pub => !pubMatches(pub)) ); } /** * Gets the current list of pending publish attempts. Filters out any that have timed out and * removes them from the list. */ lbry.getPendingPublishes = function() { const pendingPublishes = getLocal("pendingPublishes") || []; const newPendingPublishes = pendingPublishes.filter( pub => Date.now() - pub.time <= lbry.pendingPublishTimeout ); setLocal("pendingPublishes", newPendingPublishes); return newPendingPublishes; }; /** * Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be * provided along withe the name. If no pending publish is found, returns null. */ function getPendingPublish({ name, channel_name, outpoint }) { const pendingPublishes = lbry.getPendingPublishes(); return ( pendingPublishes.find( pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name)) ) || null ); } function pendingPublishToDummyClaim({ channel_name, name, outpoint, claim_id, txid, nout, }) { return { name, outpoint, claim_id, txid, nout, channel_name }; } function pendingPublishToDummyFileInfo({ name, outpoint, claim_id }) { return { name, outpoint, claim_id, metadata: null }; } //core lbry._connectPromise = null; lbry.connect = function() { if (lbry._connectPromise === null) { lbry._connectPromise = new Promise((resolve, reject) => { let tryNum = 0; function checkDaemonStartedFailed() { if (tryNum <= 200) { // Move # of tries into constant or config option setTimeout(() => { tryNum++; checkDaemonStarted(); }, tryNum < 50 ? 400 : 1000); } else { reject(new Error("Unable to connect to LBRY")); } } // Check every half second to see if the daemon is accepting connections function checkDaemonStarted() { lbry.status().then(resolve).catch(checkDaemonStartedFailed); } checkDaemonStarted(); }); } return lbry._connectPromise; }; /** * Takes a LBRY URI; will first try and calculate a total cost using * Lighthouse. If Lighthouse can't be reached, it just retrives the * key fee. * * Returns an object with members: * - cost: Number; the calculated cost of the name * - includes_data: Boolean; indicates whether or not the data fee info * from Lighthouse is included. */ lbry.costPromiseCache = {}; lbry.getCostInfo = function(uri) { if (lbry.costPromiseCache[uri] === undefined) { lbry.costPromiseCache[uri] = new Promise((resolve, reject) => { const COST_INFO_CACHE_KEY = "cost_info_cache"; let costInfoCache = getSession(COST_INFO_CACHE_KEY, {}); function cacheAndResolve(cost, includesData) { costInfoCache[uri] = { cost, includesData }; setSession(COST_INFO_CACHE_KEY, costInfoCache); resolve({ cost, includesData }); } if (!uri) { return reject(new Error(`URI required.`)); } if (costInfoCache[uri] && costInfoCache[uri].cost) { return resolve(costInfoCache[uri]); } function getCost(uri, size) { lbry .stream_cost_estimate({ uri, ...(size !== null ? { size } : {}) }) .then(cost => { cacheAndResolve(cost, size !== null); }, reject); } const uriObj = lbryuri.parse(uri); const name = uriObj.path || uriObj.name; lighthouse.get_size_for_name(name).then(size => { if (size) { getCost(name, size); } else { getCost(name, null); } }); }); } return lbry.costPromiseCache[uri]; }; /** * Publishes a file. The optional fileListedCallback is called when the file becomes available in * lbry.file_list() during the publish process. * * This currently includes a work-around to cache the file in local storage so that the pending * publish can appear in the UI immediately. */ lbry.publishDeprecated = function( params, fileListedCallback, publishedCallback, errorCallback ) { lbry.publish(params).then( result => { if (returnPendingTimeout) clearTimeout(returnPendingTimeout); publishedCallback(result); }, err => { if (returnPendingTimeout) clearTimeout(returnPendingTimeout); errorCallback(err); } ); // Give a short grace period in case publish() returns right away or (more likely) gives an error const returnPendingTimeout = setTimeout( () => { if (publishedCallback) { savePendingPublish({ name: params.name, channel_name: params.channel_name, }); publishedCallback(true); } if (fileListedCallback) { const { name, channel_name } = params; savePendingPublish({ name: params.name, channel_name: params.channel_name, }); fileListedCallback(true); } }, 2000, { once: true } ); }; lbry.getClientSettings = function() { var outSettings = {}; for (let setting of Object.keys(lbry.defaultClientSettings)) { var localStorageVal = localStorage.getItem("setting_" + setting); outSettings[setting] = localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal); } return outSettings; }; lbry.getClientSetting = function(setting) { var localStorageVal = localStorage.getItem("setting_" + setting); if (setting == "showDeveloperMenu") { return true; } return localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal); }; lbry.setClientSettings = function(settings) { for (let setting of Object.keys(settings)) { lbry.setClientSetting(setting, settings[setting]); } }; lbry.setClientSetting = function(setting, value) { return localStorage.setItem("setting_" + setting, JSON.stringify(value)); }; lbry.formatName = function(name) { // Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes) name = name.replace("/s+/g", "-"); name = name.toLowerCase().replace(lbryuri.REGEXP_INVALID_URI, ""); return name; }; lbry.imagePath = function(file) { return "img/" + file; }; lbry.getMediaType = function(contentType, fileName) { if (contentType) { return /^[^/]+/.exec(contentType)[0]; } else if (fileName) { var dotIndex = fileName.lastIndexOf("."); if (dotIndex == -1) { return "unknown"; } var ext = fileName.substr(dotIndex + 1); if (/^mp4|m4v|webm|flv|f4v|ogv$/i.test(ext)) { return "video"; } else if (/^mp3|m4a|aac|wav|flac|ogg|opus$/i.test(ext)) { return "audio"; } else if ( /^html|htm|xml|pdf|odf|doc|docx|md|markdown|txt|epub|org$/i.test(ext) ) { return "document"; } else { return "unknown"; } } else { return "unknown"; } }; lbry._subscribeIdCount = 0; lbry._balanceSubscribeCallbacks = {}; lbry._balanceSubscribeInterval = 5000; lbry._balanceUpdateInterval = null; lbry._updateBalanceSubscribers = function() { lbry.wallet_balance().then(function(balance) { for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) { callback(balance); } }); if ( !lbry._balanceUpdateInterval && Object.keys(lbry._balanceSubscribeCallbacks).length ) { lbry._balanceUpdateInterval = setInterval(() => { lbry._updateBalanceSubscribers(); }, lbry._balanceSubscribeInterval); } }; lbry.balanceSubscribe = function(callback) { const subscribeId = ++lbry._subscribeIdCount; lbry._balanceSubscribeCallbacks[subscribeId] = callback; lbry._updateBalanceSubscribers(); return subscribeId; }; lbry.balanceUnsubscribe = function(subscribeId) { delete lbry._balanceSubscribeCallbacks[subscribeId]; if ( lbry._balanceUpdateInterval && !Object.keys(lbry._balanceSubscribeCallbacks).length ) { clearInterval(lbry._balanceUpdateInterval); } }; lbry.showMenuIfNeeded = function() { const showingMenu = sessionStorage.getItem("menuShown") || null; const chosenMenu = lbry.getClientSetting("showDeveloperMenu") ? "developer" : "normal"; if (chosenMenu != showingMenu) { menu.showMenubar(chosenMenu == "developer"); } sessionStorage.setItem("menuShown", chosenMenu); }; lbry.getAppVersionInfo = function() { return new Promise((resolve, reject) => { ipcRenderer.once("version-info-received", (event, versionInfo) => { resolve(versionInfo); }); ipcRenderer.send("version-info-requested"); }); }; /** * Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs, * these are designed to be transparent wrappers around the corresponding API methods. */ /** * Returns results from the file_list API method, plus dummy entries for pending publishes. * (If a real publish with the same name is found, the pending publish will be ignored and removed.) */ lbry.file_list = function(params = {}) { return new Promise((resolve, reject) => { const { name, channel_name, outpoint } = params; /** * If we're searching by outpoint, check first to see if there's a matching pending publish. * Pending publishes use their own faux outpoints that are always unique, so we don't need * to check if there's a real file. */ if (outpoint) { const pendingPublish = getPendingPublish({ outpoint }); if (pendingPublish) { resolve([pendingPublishToDummyFileInfo(pendingPublish)]); return; } } apiCall( "file_list", params, fileInfos => { removePendingPublishIfNeeded({ name, channel_name, outpoint }); const dummyFileInfos = lbry .getPendingPublishes() .map(pendingPublishToDummyFileInfo); resolve([...fileInfos, ...dummyFileInfos]); }, reject ); }); }; lbry.claim_list_mine = function(params = {}) { return new Promise((resolve, reject) => { apiCall( "claim_list_mine", params, claims => { for (let { name, channel_name, txid, nout } of claims) { removePendingPublishIfNeeded({ name, channel_name, outpoint: txid + ":" + nout, }); } const dummyClaims = lbry .getPendingPublishes() .map(pendingPublishToDummyClaim); resolve([...claims, ...dummyClaims]); }, reject ); }); }; lbry.claim_abandon = function(params = {}) { return new Promise((resolve, reject) => { apiCall("claim_abandon", params, resolve, reject); }); }; lbry._resolveXhrs = {}; lbry.resolve = function(params = {}) { return new Promise((resolve, reject) => { if (!params.uri) { throw __("Resolve has hacked cache on top of it that requires a URI"); } lbry._resolveXhrs[params.uri] = apiCall( "resolve", params, function(data) { resolve(data && data[params.uri] ? data[params.uri] : {}); }, reject ); }); }; lbry.cancelResolve = function(params = {}) { const xhr = lbry._resolveXhrs[params.uri]; if (xhr && xhr.readyState > 0 && xhr.readyState < 4) { xhr.abort(); } }; lbry = new Proxy(lbry, { get: function(target, name) { if (name in target) { return target[name]; } return function(params = {}) { return new Promise((resolve, reject) => { apiCall(name, params, resolve, reject); }); }; }, }); export default lbry;