Merge pull request #150 from lbryio/resolve-channels

Resolve channels
This commit is contained in:
Bill Bittner 2017-08-25 09:35:40 -07:00 committed by GitHub
commit 39da99a24f
18 changed files with 588 additions and 277 deletions

View file

@ -1,7 +1,9 @@
const lbryApi = require('../helpers/lbryApi.js'); const lbryApi = require('../helpers/lbryApi.js');
const db = require('../models'); const db = require('../models');
const logger = require('winston'); const logger = require('winston');
const { getTopFreeClaim, getFullClaimIdFromShortId, resolveAgainstClaimTable } = require('../helpers/serveHelpers.js'); const { resolveAgainstClaimTable, serveFile, showFile, showFileLite, getShortClaimIdFromLongClaimId, getClaimIdByLongChannelId, getAllChannelClaims, getLongChannelId, getShortChannelIdFromLongChannelId, getLongClaimId } = require('../helpers/serveHelpers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
const { SERVE, SHOW, SHOWLITE } = require('../helpers/constants.js');
function checkForLocalAssetByClaimId (claimId, name) { function checkForLocalAssetByClaimId (claimId, name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -20,19 +22,28 @@ function checkForLocalAssetByClaimId (claimId, name) {
}); });
} }
function formatGetResultsToFileInfo ({ name, claim_id, outpoint, file_name, download_path, mime_type, metadata }) { function addGetResultsToFileRecord (fileInfo, getResult) {
fileInfo.fileName = getResult.file_name;
fileInfo.filePath = getResult.download_path;
fileInfo.fileType = getResult.mime_type;
return fileInfo;
}
function createFileRecord ({ name, claimId, outpoint, height, address, nsfw }) {
return { return {
name, name,
claimId : claim_id, claimId,
outpoint, outpoint,
fileName: file_name, height,
filePath: download_path, address,
fileType: mime_type, fileName: '',
nsfw : metadata.stream.metadata.nsfw, filePath: '',
fileType: '',
nsfw,
}; };
} }
function getAssetByClaimId (fullClaimId, name) { function getAssetByLongClaimId (fullClaimId, name) {
logger.debug('...getting asset by claim Id...'); logger.debug('...getting asset by claim Id...');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 1. check locally for claim // 1. check locally for claim
@ -57,9 +68,8 @@ function getAssetByClaimId (fullClaimId, name) {
lbryApi.getClaim(`${name}#${fullClaimId}`) lbryApi.getClaim(`${name}#${fullClaimId}`)
.then(getResult => { .then(getResult => {
logger.debug('getResult >>', getResult); logger.debug('getResult >>', getResult);
fileRecord = formatGetResultsToFileInfo(getResult); fileRecord = createFileRecord(resolveResult);
fileRecord['address'] = (resolveResult.address || 0); fileRecord = addGetResultsToFileRecord(fileRecord, getResult);
fileRecord['height'] = resolveResult.height;
// insert a record in the File table & Update Claim table // insert a record in the File table & Update Claim table
return db.File.create(fileRecord); return db.File.create(fileRecord);
}) })
@ -82,53 +92,112 @@ function getAssetByClaimId (fullClaimId, name) {
} }
module.exports = { module.exports = {
getAssetByChannel (channelName, name) { getAssetByClaim (claimName, claimId) {
logger.debug('...getting asset by channel...'); logger.debug('getting asset by claim');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// temporarily throw error // 1. get the long claim id
reject(new Error('channel names are not currently supported')); getLongClaimId(claimName, claimId) // here
// get the claim id // 2. get the claim Id
// get the asset by claim Id .then(longClaimId => {
logger.debug('long claim id = ', longClaimId);
resolve(getAssetByLongClaimId(longClaimId, claimName));
})
.catch(error => {
reject(error);
});
}); });
}, },
getAssetByShortId: function (shortId, name) { getAssetByChannel (channelName, channelId, claimName) {
logger.debug('...getting asset by short id...'); logger.debug('getting asset by channel');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// get the full claimId // 1. get the long channel id
getFullClaimIdFromShortId(shortId, name) getLongChannelId(channelName, channelId)
// get the asset by the claimId // 2. get the claim Id
.then(longChannelId => {
return getClaimIdByLongChannelId(longChannelId, claimName);
})
// 3. get the asset by this claim id and name
.then(claimId => { .then(claimId => {
logger.debug('claim id =', claimId); logger.debug('asset claim id = ', claimId);
resolve(getAssetByClaimId(claimId, name)); resolve(getAssetByLongClaimId(claimId, claimName));
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
}); });
}, },
getAssetByClaimId (fullClaimId, name) { getChannelContents (channelName, channelId) {
return getAssetByClaimId(fullClaimId, name);
},
getAssetByName (name) {
logger.debug('...getting asset by claim name...');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 1. get a list of the free public claims let longChannelId;
getTopFreeClaim(name) let shortChannelId;
// 2. check locally for the top claim // 1. get the long channel Id
.then(topFreeClaim => { getLongChannelId(channelName, channelId)
// if no claims were found, return null // 2. get all claims for that channel
if (!topFreeClaim) { .then(result => {
return resolve(null); longChannelId = result;
return getShortChannelIdFromLongChannelId(channelName, longChannelId);
})
// 3. get all Claim records for this channel
.then(result => {
shortChannelId = result;
return getAllChannelClaims(longChannelId);
})
// 4. add extra data not available from Claim table
.then(allChannelClaims => {
if (allChannelClaims) {
allChannelClaims.forEach(element => {
element['channelName'] = channelName;
element['longChannelId'] = longChannelId;
element['shortChannelId'] = shortChannelId;
element['fileExtension'] = element.contentType.substring(element.contentType.lastIndexOf('/') + 1);
});
} }
// parse the result return resolve(allChannelClaims);
const claimId = topFreeClaim.claimId;
logger.debug('top free claim id =', claimId);
// get the asset
resolve(getAssetByClaimId(claimId, name));
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
}); });
}, },
serveOrShowAsset (fileInfo, extension, method, headers, originalUrl, ip, res) {
// add file extension to the file info
if (extension === '.gifv') {
fileInfo['fileExt'] = '.gifv';
} else {
fileInfo['fileExt'] = fileInfo.fileName.substring(fileInfo.fileName.lastIndexOf('.'));
}
// serve or show
switch (method) {
case SERVE:
serveFile(fileInfo, res);
sendGoogleAnalytics(method, headers, ip, originalUrl);
postToStats('serve', originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
return fileInfo;
case SHOWLITE:
showFileLite(fileInfo, res);
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
return fileInfo;
case SHOW:
return getShortClaimIdFromLongClaimId(fileInfo.claimId, fileInfo.name)
.then(shortId => {
fileInfo['shortId'] = shortId;
return resolveAgainstClaimTable(fileInfo.name, fileInfo.claimId);
})
.then(resolveResult => {
logger.debug('resolve result', resolveResult);
fileInfo['title'] = resolveResult.title;
fileInfo['description'] = resolveResult.description;
showFile(fileInfo, res);
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
return fileInfo;
})
.catch(error => {
console.log('thowing error...');
throw error;
});
default:
logger.error('I did not recognize that method');
break;
}
},
}; };

View file

@ -156,4 +156,18 @@ module.exports = {
}); });
}); });
}, },
getRecentClaims (startDate) {
logger.debug('retrieving most recent claims');
return new Promise((resolve, reject) => {
// get the raw requests data
db.sequelize.query(`SELECT * FROM File WHERE nsfw != 1 AND trendingEligible = 1 ORDER BY createdAt DESC LIMIT 25;`, { type: db.sequelize.QueryTypes.SELECT })
.then(results => {
resolve(results);
})
.catch(error => {
logger.error('sequelize error', error);
reject(error);
});
});
},
}; };

8
helpers/constants.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
SERVE : 'SERVE',
SHOW : 'SHOW',
SHOWLITE : 'SHOWLITE',
CHANNEL : 'CHANNEL',
CLAIM : 'CLAIM',
CHANNELID_INDICATOR: ':',
};

View file

@ -1,53 +1,6 @@
const logger = require('winston'); const logger = require('winston');
const db = require('../models'); const db = require('../models');
function determineShortClaimId (claimId, height, claimList) {
logger.debug('determining short url based on claim id and claim list');
logger.debug('claimlist starting length:', claimList.length);
// remove this claim from the claim list, if it exists
claimList = claimList.filter(claim => {
return claim.claimId !== claimId;
});
logger.debug('claim list length without this claim:', claimList.length);
// If there are no other claims, return the first letter of the claim id...
if (claimList.length === 0) {
return claimId.substring(0, 1);
// ...otherwise determine the proper short id.
} else {
const claimListCopy = claimList;
let i = 0;
// find the longest shared prefix (there is a better way to do this that filters, checks next filter, then filters (i.e. combine this step and next))
while (claimList.length !== 0) {
i++;
claimList = claimList.filter(claim => {
const otherClaimIdSegmentToCompare = claim.claimId.substring(0, i);
const thisClaimIdSegmentToCompare = claimId.substring(0, i);
logger.debug('compare:', otherClaimIdSegmentToCompare, '===', thisClaimIdSegmentToCompare, '?');
return (otherClaimIdSegmentToCompare === thisClaimIdSegmentToCompare);
});
}
// use that longest shared prefix to get only those competing claims
const lastMatchIndex = i - 1;
const lastMatch = claimId.substring(0, lastMatchIndex);
logger.debug('last match index:', lastMatchIndex, 'last match:', lastMatch);
if (lastMatchIndex === 0) { // if no other claims share a prefix, return with first letter.
return claimId.substring(0, 1);
}
const allMatchingClaimsAtLastMatch = claimListCopy.filter(claim => {
return (claim.claimId.substring(0, lastMatchIndex) === lastMatch);
});
// for those that share the longest shared prefix: see which came first in time. whichever is earliest, the others take the extra character
const sortedMatchingClaims = allMatchingClaimsAtLastMatch.sort((a, b) => {
return (a.height < b.height);
});
// compare to the earliest one, if it is earlier, this claim takes the extra character
if (sortedMatchingClaims[0].height < height) {
return claimId.substring(0, lastMatchIndex + 1);
}
return claimId.substring(0, lastMatchIndex);
}
}
function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) { function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) {
return { return {
embedUrl : `https://spee.ch/embed/${claimId}/${name}`, embedUrl : `https://spee.ch/embed/${claimId}/${name}`,
@ -57,6 +10,107 @@ function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) {
}; };
} }
function getLongChannelIdFromShortChannelId (channelName, channelId) {
return new Promise((resolve, reject) => {
logger.debug(`finding long channel id for ${channelName}:${channelId}`);
// get the long channel Id
db.sequelize.query(`SELECT claimId, height FROM Certificate WHERE name = '${channelName}' AND claimId LIKE '${channelId}%' ORDER BY height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
logger.debug('result >>', result);
switch (result.length) {
case 0:
throw new Error('That is an invalid Short Channel Id');
default: // note results must be sorted
return resolve(result[0].claimId);
}
})
.catch(error => {
reject(error);
});
});
}
function getLongChannelIdFromChannelName (channelName) {
// select the top top channel id
return new Promise((resolve, reject) => {
logger.debug(`finding long channel id for ${channelName}`);
// get the long channel Id
db.sequelize.query(`SELECT claimId, amount, height FROM Certificate WHERE name = '${channelName}' ORDER BY amount DESC, height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
logger.debug('result >>', result);
switch (result.length) {
case 0:
throw new Error('That is an invalid Channel Name');
default:
return resolve(result[0].claimId);
}
})
.catch(error => {
reject(error);
});
});
}
function getLongClaimIdFromShortClaimId (name, shortId) {
return new Promise((resolve, reject) => {
logger.debug('getting claim_id from short url');
// use the daemon to check for claims list
db.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' AND claimId LIKE '${shortId}%' ORDER BY height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
return reject(new Error('That is an invalid Short Claim Id'));
default: // note results must be sorted
return resolve(result[0].claimId);
}
})
.catch(error => {
reject(error);
});
});
}
function getTopFreeClaimIdByClaimName (name) {
return new Promise((resolve, reject) => {
db.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' ORDER BY amount DESC, height ASC LIMIT 1`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
return resolve(null);
default:
return resolve(result[0].claimId);
}
})
.catch(error => {
reject(error);
});
});
};
function sortResult (result, longId) {
let claimIndex;
let shortId = longId.substring(0, 1); // default sort id is the first letter
let shortIdLength = 0;
// find the index of this certificate
claimIndex = result.findIndex(element => {
return element.claimId === longId;
});
if (claimIndex < 0) { throw new Error('claimid not found in possible sorted list') }
// get an array of all certificates with lower height
let possibleMatches = result.slice(0, claimIndex);
// remove certificates with the same prefixes until none are left.
while (possibleMatches.length > 0) {
shortIdLength += 1;
shortId = longId.substring(0, shortIdLength);
possibleMatches = possibleMatches.filter(element => {
return (element.claimId.substring(0, shortIdLength) === shortId);
});
}
// return the short Id
logger.debug('short channel id ===', shortId);
return shortId;
}
module.exports = { module.exports = {
serveFile ({ fileName, fileType, filePath }, res) { serveFile ({ fileName, fileType, filePath }, res) {
logger.info(`serving file ${fileName}`); logger.info(`serving file ${fileName}`);
@ -90,36 +144,25 @@ module.exports = {
const openGraphInfo = createOpenGraphInfo(fileInfo); const openGraphInfo = createOpenGraphInfo(fileInfo);
res.status(200).render('showLite', { layout: 'show', fileInfo, openGraphInfo }); res.status(200).render('showLite', { layout: 'show', fileInfo, openGraphInfo });
}, },
getFullClaimIdFromShortId (shortId, name) { getLongClaimId (claimName, claimId) { // read the various inputs and decide how to return the long claim id
return new Promise((resolve, reject) => { if (claimId && (claimId.length === 40)) {
logger.debug('getting claim_id from short url'); return new Promise((resolve, reject) => resolve(claimId));
// use the daemon to check for claims list } else if (claimId && claimId.length < 40) {
db.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' AND claimId LIKE '${shortId}%' ORDER BY height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT }) return getLongClaimIdFromShortClaimId(claimName, claimId); // need to create this function
.then(result => { } else { // if no claim id provided
switch (result.length) { return getTopFreeClaimIdByClaimName(claimName);
case 0:
return reject(new Error('That is an invalid Short Id'));
default: // note results must be sorted
return resolve(result[0].claimId);
} }
})
.catch(error => {
reject(error);
});
});
}, },
getShortIdFromClaimId (claimId, height, name) { getShortClaimIdFromLongClaimId (claimId, claimName) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.debug('finding short claim id from full claim id'); logger.debug('finding short channel id');
db.sequelize.query(`SELECT claimId, height FROM Claim WHERE name = '${name}' ORDER BY claimId;`, { type: db.sequelize.QueryTypes.SELECT }) db.sequelize.query(`SELECT claimId, height FROM Claim WHERE name = '${claimName}' ORDER BY height;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
return reject(new Error('That is an invalid Claim Id')); return reject(new Error('That is an invalid claim name'));
default: // note results must be sorted default:
const shortId = determineShortClaimId(claimId, height, result); return resolve(sortResult(result, claimId));
logger.debug('short claim id ===', shortId);
return resolve(shortId);
} }
}) })
.catch(error => { .catch(error => {
@ -129,7 +172,7 @@ module.exports = {
}, },
getAllFreeClaims (name) { getAllFreeClaims (name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.sequelize.query(`SELECT * FROM Claim WHERE name = '${name}' ORDER BY amount DESC, height ASC`, { type: db.sequelize.QueryTypes.SELECT }) db.sequelize.query(`SELECT name, claimId, outpoint, height, address FROM Claim WHERE name = '${name}' ORDER BY amount DESC, height ASC`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
@ -143,25 +186,9 @@ module.exports = {
}); });
}); });
}, },
getTopFreeClaim (name) {
return new Promise((resolve, reject) => {
db.sequelize.query(`SELECT * FROM Claim WHERE name = '${name}' ORDER BY amount DESC, height ASC LIMIT 1`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
return resolve(null);
default:
return resolve(result[0]);
}
})
.catch(error => {
reject(error);
});
});
},
resolveAgainstClaimTable (name, claimId) { resolveAgainstClaimTable (name, claimId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.sequelize.query(`SELECT * FROM Claim WHERE name = '${name}' AND claimId = '${claimId}'`, { type: db.sequelize.QueryTypes.SELECT }) db.sequelize.query(`SELECT name, claimId, outpoint, height, address, title, description FROM Claim WHERE name = '${name}' AND claimId = '${claimId}'`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
@ -177,4 +204,64 @@ module.exports = {
}); });
}); });
}, },
getClaimIdByLongChannelId (channelId, claimName) {
return new Promise((resolve, reject) => {
logger.debug(`finding claim id for claim "${claimName}" from channel "${channelId}"`);
db.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${claimName}' AND certificateId = '${channelId}' LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
return reject(new Error('There is no such claim for that channel'));
default:
return resolve(result[0].claimId);
}
})
.catch(error => {
reject(error);
});
});
},
getAllChannelClaims (channelId) {
return new Promise((resolve, reject) => {
logger.debug(`finding all claims in channel "${channelId}"`);
db.sequelize.query(`SELECT name, claimId, outpoint, height, address, contentType, title, description, license FROM Claim WHERE certificateId = '${channelId}' ORDeR BY height DESC;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
return resolve(null);
default:
return resolve(result);
}
})
.catch(error => {
reject(error);
});
});
},
getLongChannelId (channelName, channelId) {
if (channelId && (channelId.length === 40)) { // full channel id
return new Promise((resolve, reject) => resolve(channelId));
} else if (channelId && channelId.length < 40) { // short channel id
return getLongChannelIdFromShortChannelId(channelName, channelId);
} else {
return getLongChannelIdFromChannelName(channelName);
}
},
getShortChannelIdFromLongChannelId (channelName, longChannelId) {
return new Promise((resolve, reject) => {
logger.debug('finding short channel id');
db.sequelize.query(`SELECT claimId, height FROM Certificate WHERE name = '${channelName}' ORDER BY height;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
return reject(new Error('That is an invalid channel name'));
default:
return resolve(sortResult(result, longChannelId));
}
})
.catch(error => {
reject(error);
});
});
},
}; };

91
models/certificate.js Normal file
View file

@ -0,0 +1,91 @@
module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, ARRAY, DECIMAL, DOUBLE }) => {
const Certificate = sequelize.define(
'Certificate',
{
address: {
type : STRING,
default: null,
},
amount: {
type : STRING,
default: null,
},
claimId: {
type : STRING,
default: null,
},
claimSequence: {
type : INTEGER,
default: null,
},
decodedClaim: {
type : BOOLEAN,
default: null,
},
depth: {
type : INTEGER,
default: null,
},
effectiveAmount: {
type : STRING,
default: null,
},
hasSignature: {
type : BOOLEAN,
default: null,
},
height: {
type : STRING,
default: null,
},
hex: {
type : TEXT('long'),
default: null,
},
name: {
type : STRING,
default: null,
},
nout: {
type : INTEGER,
default: null,
},
txid: {
type : STRING,
default: null,
},
validAtHeight: {
type : STRING,
default: null,
},
outpoint: {
type : STRING,
default: null,
},
valueVersion: {
type : STRING,
default: null,
},
claimType: {
type : STRING,
default: null,
},
certificateVersion: {
type : STRING,
default: null,
},
keyType: {
type : STRING,
default: null,
},
publicKey: {
type : TEXT('long'),
default: null,
},
},
{
freezeTableName: true,
}
);
return Certificate;
};

View file

@ -163,7 +163,7 @@ button.copy-button {
overflow: auto; overflow: auto;
} }
.all-claims-img { .all-claims-asset {
width: 20%; width: 20%;
float: left; float: left;
margin: 5px 30px 5px 0px; margin: 5px 30px 5px 0px;
@ -187,7 +187,7 @@ button.copy-button {
@media (max-width: 750px) { @media (max-width: 750px) {
.all-claims-img { .all-claims-asset {
width:30%; width:30%;
} }
@ -214,7 +214,7 @@ button.copy-button {
margin-right: 2em; margin-right: 2em;
} }
.all-claims-img { .all-claims-asset {
width:50%; width:50%;
} }

View file

@ -1,6 +1,6 @@
const errorHandlers = require('../helpers/errorHandlers.js'); const errorHandlers = require('../helpers/errorHandlers.js');
const { getAllFreeClaims } = require('../helpers/serveHelpers.js'); const { getAllFreeClaims } = require('../helpers/serveHelpers.js');
const { postToStats, getStatsSummary, getTrendingClaims } = require('../controllers/statsController.js'); const { postToStats, getStatsSummary, getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js');
module.exports = (app) => { module.exports = (app) => {
// route to show 'about' page for spee.ch // route to show 'about' page for spee.ch
@ -9,7 +9,7 @@ module.exports = (app) => {
res.status(200).render('about'); res.status(200).render('about');
}); });
// route to display a list of the trending images // route to display a list of the trending images
app.get('/trending', ({ params, headers }, res) => { app.get('/popular', ({ params, headers }, res) => {
const startDate = new Date(); const startDate = new Date();
startDate.setDate(startDate.getDate() - 1); startDate.setDate(startDate.getDate() - 1);
getTrendingClaims(startDate) getTrendingClaims(startDate)
@ -21,6 +21,19 @@ module.exports = (app) => {
errorHandlers.handleRequestError(error, res); errorHandlers.handleRequestError(error, res);
}); });
}); });
// route to display a list of the trending images
app.get('/new', ({ params, headers }, res) => {
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1);
getRecentClaims(startDate)
.then(result => {
// logger.debug(result);
res.status(200).render('new', { newClaims: result });
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
});
});
// route to show statistics for spee.ch // route to show statistics for spee.ch
app.get('/stats', ({ ip, originalUrl }, res) => { app.get('/stats', ({ ip, originalUrl }, res) => {
// get and render the content // get and render the content

View file

@ -1,72 +1,7 @@
const logger = require('winston'); const logger = require('winston');
const { serveFile, showFile, showFileLite, getShortIdFromClaimId, resolveAgainstClaimTable } = require('../helpers/serveHelpers.js'); const { getAssetByClaim, getChannelContents, getAssetByChannel, serveOrShowAsset } = require('../controllers/serveController.js');
const { getAssetByChannel, getAssetByShortId, getAssetByClaimId, getAssetByName } = require('../controllers/serveController.js');
const { handleRequestError } = require('../helpers/errorHandlers.js'); const { handleRequestError } = require('../helpers/errorHandlers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js'); const { SERVE, SHOW, SHOWLITE, CHANNEL, CLAIM, CHANNELID_INDICATOR } = require('../helpers/constants.js');
const SERVE = 'SERVE';
const SHOW = 'SHOW';
const SHOWLITE = 'SHOWLITE';
const CHANNEL = 'CHANNEL';
const SHORTURL = 'SHORTURL';
const CLAIMID = 'CLAIMID';
const NAME = 'NAME';
function getAsset (claimType, channelName, shortId, fullClaimId, name) {
switch (claimType) {
case CHANNEL:
return getAssetByChannel(channelName, name);
case SHORTURL:
return getAssetByShortId(shortId, name);
case CLAIMID:
return getAssetByClaimId(fullClaimId, name);
case NAME:
return getAssetByName(name);
default:
return new Error('that claim type was not found');
}
}
function serveOrShowAsset (fileInfo, extension, method, headers, originalUrl, ip, res) {
// add file extension to the file info
if (extension === '.gifv') {
fileInfo['fileExt'] = '.gifv';
} else {
fileInfo['fileExt'] = fileInfo.fileName.substring(fileInfo.fileName.lastIndexOf('.'));
}
// serve or show
switch (method) {
case SERVE:
serveFile(fileInfo, res);
sendGoogleAnalytics(method, headers, ip, originalUrl);
postToStats('serve', originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
return fileInfo;
case SHOWLITE:
showFileLite(fileInfo, res);
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
return fileInfo;
case SHOW:
return getShortIdFromClaimId(fileInfo.claimId, fileInfo.height, fileInfo.name)
.then(shortId => {
fileInfo['shortId'] = shortId;
return resolveAgainstClaimTable(fileInfo.name, fileInfo.claimId);
})
.then(resolveResult => {
logger.debug('resolve result', resolveResult);
fileInfo['title'] = resolveResult.title;
fileInfo['description'] = resolveResult.description;
showFile(fileInfo, res);
postToStats('show', originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
return fileInfo;
})
.catch(error => {
console.log('thowing error...');
throw error;
});
default:
logger.error('I did not recognize that method');
break;
}
}
function isValidClaimId (claimId) { function isValidClaimId (claimId) {
return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId)); return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
@ -80,6 +15,17 @@ function isValidShortIdOrClaimId (input) {
return (isValidClaimId(input) || isValidShortId(input)); return (isValidClaimId(input) || isValidShortId(input));
} }
function getAsset (claimType, channelName, channelId, name, claimId) {
switch (claimType) {
case CHANNEL:
return getAssetByChannel(channelName, channelId, name);
case CLAIM:
return getAssetByClaim(name, claimId);
default:
return new Error('that claim type was not found');
}
}
module.exports = (app) => { module.exports = (app) => {
// route to serve a specific asset // route to serve a specific asset
app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => { app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => {
@ -87,8 +33,8 @@ module.exports = (app) => {
let name = params.name; let name = params.name;
let claimType; let claimType;
let channelName = null; let channelName = null;
let shortId = null; let claimId = null;
let fullClaimId = null; let channelId = null;
let method; let method;
let extension; let extension;
// parse the name // parse the name
@ -121,24 +67,21 @@ module.exports = (app) => {
logger.debug('method =', method); logger.debug('method =', method);
// parse identifier for whether it is a channel, short url, or claim_id // parse identifier for whether it is a channel, short url, or claim_id
if (identifier.charAt(0) === '@') { if (identifier.charAt(0) === '@') {
channelName = identifier.substring(1); channelName = identifier;
logger.debug('channel name =', channelName);
claimType = CHANNEL; claimType = CHANNEL;
} else if (identifier.length === 40) { const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR);
fullClaimId = identifier; if (channelIdIndex !== -1) {
logger.debug('full claim id =', fullClaimId); channelId = channelName.substring(channelIdIndex + 1);
claimType = CLAIMID; channelName = channelName.substring(0, channelIdIndex);
} else if (identifier.length < 40) { }
shortId = identifier; logger.debug('channel name =', channelName);
logger.debug('short claim id =', shortId);
claimType = SHORTURL;
} else { } else {
logger.error('The URL provided could not be parsed'); claimId = identifier;
res.send('that url is invalid'); logger.debug('claim id =', claimId);
return; claimType = CLAIM;
}; }
// 1. retrieve the asset and information // 1. retrieve the asset and information
getAsset(claimType, channelName, shortId, fullClaimId, name) getAsset(claimType, channelName, channelId, name, claimId)
// 2. serve or show // 2. serve or show
.then(fileInfo => { .then(fileInfo => {
logger.debug('fileInfo', fileInfo); logger.debug('fileInfo', fileInfo);
@ -162,6 +105,35 @@ module.exports = (app) => {
let name = params.name; let name = params.name;
let method; let method;
let fileExtension; let fileExtension;
let channelName = null;
let channelId = null;
if (name.charAt(0) === '@') {
channelName = name;
const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR);
if (channelIdIndex !== -1) {
channelId = channelName.substring(channelIdIndex + 1);
channelName = channelName.substring(0, channelIdIndex);
}
logger.debug('channel name =', channelName);
logger.debug('channel Id =', channelId);
// 1. retrieve the channel contents
getChannelContents(channelName, channelId)
// 2. respond to the request
.then(channelContents => {
if (!channelContents) {
res.status(200).render('noChannel');
} else {
const handlebarsData = {
channelName,
channelContents,
};
res.status(200).render('channel', handlebarsData);
}
})
.catch(error => {
handleRequestError('serve', originalUrl, ip, error, res);
});
} else {
if (name.indexOf('.') !== -1) { if (name.indexOf('.') !== -1) {
method = SERVE; method = SERVE;
if (headers['accept'] && headers['accept'].split(',').includes('text/html')) { if (headers['accept'] && headers['accept'].split(',').includes('text/html')) {
@ -179,8 +151,8 @@ module.exports = (app) => {
logger.debug('claim name = ', name); logger.debug('claim name = ', name);
logger.debug('method =', method); logger.debug('method =', method);
// 1. retrieve the asset and information // 1. retrieve the asset and information
getAsset(NAME, null, null, null, name) getAsset(CLAIM, null, null, name, null)
// 2. serve or show // 2. respond to the request
.then(fileInfo => { .then(fileInfo => {
if (!fileInfo) { if (!fileInfo) {
res.status(200).render('noClaims'); res.status(200).render('noClaims');
@ -195,5 +167,6 @@ module.exports = (app) => {
.catch(error => { .catch(error => {
handleRequestError('serve', originalUrl, ip, error, res); handleRequestError('serve', originalUrl, ip, error, res);
}); });
}
}); });
}; };

View file

@ -5,7 +5,7 @@
<p>These are all the free, public assets at that claim. You can publish more at <a href="/">spee.ch</a>.</p> <p>These are all the free, public assets at that claim. You can publish more at <a href="/">spee.ch</a>.</p>
{{#each claims}} {{#each claims}}
<div class="all-claims-item"> <div class="all-claims-item">
<img class="all-claims-img" src="/{{this.claimId}}/{{this.name}}.test" /> <img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.test" />
<div class="all-claims-details"> <div class="all-claims-details">
<ul style="list-style-type:none"> <ul style="list-style-type:none">
<li>claim: {{this.name}}</li> <li>claim: {{this.name}}</li>

29
views/channel.handlebars Normal file
View file

@ -0,0 +1,29 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>{{this.channelName}}</h3>
<p>Below are all the free claims in this channel.</p>
{{#each channelContents}}
<div class="all-claims-item">
<a href="/{{this.channelName}}:{{this.shortChannelId}}/{{this.name}}.{{this.fileExtension}}">
{{#ifConditional this.contentType '===' 'video/mp4'}}
<img class="all-claims-asset" src="/assets/img/content-freedom-large.png"/>
{{else}}
<img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.{{this.fileExtension}}" />
{{/ifConditional}}
</a>
<div class="all-claims-details">
<ul style="list-style-type:none">
<li><strong>{{this.title}}</strong></li>
<li><i> {{this.description}}</i></li>
<li><a href="/{{this.channelName}}:{{this.shortChannelId}}/{{this.name}}.{{this.fileExtension}}">spee.ch/{{this.channelName}}:{{this.shortChannelId}}/{{this.name}}.{{this.fileExtension}}</a></li>
<li>License: {{this.license}}</li>
<li>Claim: {{this.name}}</li>
<li>Claim Id: {{this.claimId}}</li>
</ul>
</div>
</div>
{{/each}}
</div>
{{> footer}}
</div>

View file

@ -15,10 +15,10 @@
<meta property="og:image" content="https://spee.ch/assets/img/content-freedom-64px.png"> <meta property="og:image" content="https://spee.ch/assets/img/content-freedom-64px.png">
<meta property="og:url" content="http://spee.ch/"> <meta property="og:url" content="http://spee.ch/">
<meta property="og:description" content="Open-source, decentralized image and video hosting."> <meta property="og:description" content="Open-source, decentralized image and video hosting.">
<!-- google analytics -->
{{ googleAnalytics }}
</head> </head>
<body> <body>
{{{ body }}} {{{ body }}}
<!-- google analytics -->
{{ googleAnalytics }}
</body> </body>
</html> </html>

View file

@ -12,11 +12,11 @@
{{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}} {{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}}
{{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description}}} {{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description}}}
{{/unless}} {{/unless}}
<!-- google analytics -->
{{ googleAnalytics }}
</head> </head>
<body> <body>
{{{ body }}} {{{ body }}}
<!-- google analytics -->
{{ googleAnalytics }}
</body> </body>
</html> </html>

26
views/new.handlebars Normal file
View file

@ -0,0 +1,26 @@
<div class="wrapper">
{{> topBar}}
<div>
<h2>New on Spee.ch</h2>
<p><i>The 25 most recent publishes on spee.ch</i></p>
{{#each newClaims}}
<div class="all-claims-item">
<a href="/{{this.claimId}}/{{this.name}}">
{{#ifConditional this.fileType '===' 'video/mp4'}}
<img class="all-claims-asset" src="/assets/img/content-freedom-large.png"/>
{{else}}
<img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.ext" />
{{/ifConditional}}
</a>
<div class="all-claims-details">
<ul style="list-style-type:none">
<li><bold>{{this.name}}</bold></li>
<li><i>Claim Id: {{this.claimId}}</i></li>
<li><a href="/{{this.claimId}}/{{this.name}}">Link</a></li>
</ul>
</div>
</div>
{{/each}}
</div>
{{> footer}}
</div>

View file

@ -0,0 +1,8 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>No Claims</h3>
<p>There are no free assets on this channel.</p>
<p><i>If you think this message is an error, contact us in the <a href="https://lbry.slack.com/" target="_blank">LBRY slack!</a></i></p>
</div>
</div>

View file

@ -2,7 +2,7 @@
{{> topBar}} {{> topBar}}
<div> <div>
<h3>No Claims</h3> <h3>No Claims</h3>
<p>There are no free, public images at that claim. You should publish one at <a href="/">spee.ch</a>.</p> <p>There are no free assets at that claim. You should publish one at <a href="/">spee.ch</a>.</p>
<p>NOTE: it is possible your claim was published, but it is still being processed by the blockchain</p> <p>NOTE: it is possible your claim was published, but it is still being processed by the blockchain</p>
</div> </div>
</div> </div>

View file

@ -20,16 +20,6 @@
<input type="text" id="long-link" class="link" readonly onclick="select()" spellcheck="false" value="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}{{fileInfo.fileExt}}"/> <input type="text" id="long-link" class="link" readonly onclick="select()" spellcheck="false" value="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}{{fileInfo.fileExt}}"/>
<button class="copy-button" data-elementtocopy="long-link" onclick="copyToClipboard(event)">copy</button> <button class="copy-button" data-elementtocopy="long-link" onclick="copyToClipboard(event)">copy</button>
</div> </div>
{{!-- gif v link --}}
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
<div class="share-option">
<a href="/{{fileInfo.shortId}}/{{fileInfo.name}}.gifv">gifv</a>
<div class="input-error" id="input-error-copy-gifv-text" hidden="true"></div>
<br/>
<input type="text" id="gifv-text" class="link" readonly onclick="select()" spellcheck="false" value='https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}.gifv'/>
<button class="copy-button" data-elementtocopy="gifv-text" onclick="copyToClipboard(event)">copy</button>
</div>
{{/ifConditional}}
{{!-- html text for embedding asset--}} {{!-- html text for embedding asset--}}
<div class="share-option"> <div class="share-option">
Embed HTML Embed HTML

View file

@ -1,7 +1,7 @@
<div class="top-bar"> <div class="top-bar">
<a href="https://en.wikipedia.org/wiki/Freedom_of_information" target="_blank"><img id="logo" src="/assets/img/content-freedom-64px.png"/></a> <a href="https://en.wikipedia.org/wiki/Freedom_of_information" target="_blank"><img id="logo" src="/assets/img/content-freedom-64px.png"/></a>
<h1 id="title"><a href="/">Spee.ch</a></h1><span class="top-bar-left">(beta)</span> <h1 id="title"><a href="/">Spee.ch</a></h1><span class="top-bar-left">(beta)</span>
<a href="/trending" class="top-bar-right">trending</a> <a href="/popular" class="top-bar-right">popular</a>
<a href="https://github.com/lbryio/spee.ch" target="_blank" class="top-bar-right">source</a> <a href="https://github.com/lbryio/spee.ch" target="_blank" class="top-bar-right">source</a>
<a href="/about" class="top-bar-right">help</a> <a href="/about" class="top-bar-right">help</a>
<div class="top-bar-tagline">Open-source, decentralized image and video hosting.</div> <div class="top-bar-tagline">Open-source, decentralized image and video hosting.</div>

View file

@ -2,22 +2,25 @@
<div class="full"> <div class="full">
<h2>Trending</h2> <h2>Trending</h2>
<p><i>The 25 most popular assets on spee.ch</i></p>
<div class="grid" data-masonry='{ "itemSelector": ".grid-item" }'> <div class="grid" data-masonry='{ "itemSelector": ".grid-item" }'>
{{#each trendingAssets}} {{#each trendingAssets}}
{{#unless this.nsfw}} <div class="all-claims-item">
<a href="/{{this.claimId}}/{{this.name}}"> <a href="/{{this.claimId}}/{{this.name}}">
{{#ifConditional this.fileType '===' 'video/mp4'}} {{#ifConditional this.fileType '===' 'video/mp4'}}
<video class="grid-item trending-video" controls onloadeddata="resetTrendingLayout()"> <img class="all-claims-asset" src="/assets/img/content-freedom-large.png"/>
<source src="/media/{{this.fileName}}" >
{{!--fallback--}}
Your browser does not support the <code>video</code> element.
</video>
{{else}} {{else}}
<img class="grid-item trending-image" src="/media/{{this.fileName}}" onload="resetTrendingLayout()"/> <img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.ext" />
{{/ifConditional}} {{/ifConditional}}
</a> </a>
{{/unless}} <div class="all-claims-details">
<ul style="list-style-type:none">
<li><bold>{{this.name}}</bold></li>
<li><i>Claim Id: {{this.claimId}}</i></li>
<li><strong>{{this.title}}</strong></li>
</ul>
</div>
</div>
{{/each}} {{/each}}
</div> </div>
</div> </div>