Resolve channels #150

Merged
bones7242 merged 23 commits from resolve-channels into master 2017-08-25 18:35:40 +02:00
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');
kauffj commented 2017-08-25 19:21:21 +02:00 (Migrated from github.com)
Review

This looks like it could use better organization. I'd be inclined to put a lot of these inside of the modal folder, but I'd suggest looking at what other conventions are used in this framework before doing that.

This looks like it could use better organization. I'd be inclined to put a lot of these inside of the `modal` folder, but I'd suggest looking at what other conventions are used in this framework before doing that.
bones7242 commented 2017-09-08 02:27:09 +02:00 (Migrated from github.com)
Review

I get a little confused sometimes on the line between controller and model responsibilities. But, yes, after looking at it and re-reading some MVC notes, I think that all the database related functions should go in the model. I rearranged as such.
af046e9d36

I get a little confused sometimes on the line between controller and model responsibilities. But, yes, after looking at it and re-reading some MVC notes, I think that all the database related functions should go in the model. I rearranged as such. https://github.com/lbryio/spee.ch/commit/af046e9d3655a57bf9cd67d3a67a54d4d44ed802
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 })
kauffj commented 2017-08-25 19:27:57 +02:00 (Migrated from github.com)
Review

Try to avoid SQL in your controllers and keep it located in the modal layer. This helps facilitates future modal refactoring/redesign and keeps controllers simpler (controllers are typically then thinnest of the 3 MVC layers).

Try to avoid SQL in your controllers and keep it located in the modal layer. This helps facilitates future modal refactoring/redesign and keeps controllers simpler (controllers are typically then thinnest of the 3 MVC layers).
bones7242 commented 2017-09-07 21:37:15 +02:00 (Migrated from github.com)
Review

moved to model 4d94acee77

moved to model https://github.com/lbryio/spee.ch/commit/4d94acee77cd90341f23ab5d39d2ba90bc7edf2c
.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 = {
kauffj commented 2017-08-25 19:33:20 +02:00 (Migrated from github.com)
Review

I'd be more inclined to keep these where they were, or to put them on the object/class they relate to, then to create a single constants file like this.

I'd be more inclined to keep these where they were, or to put them on the object/class they relate to, then to create a single constants file like this.
bones7242 commented 2017-09-08 00:33:42 +02:00 (Migrated from github.com)
Review

moved to their individual modules f0435be0b2

moved to their individual modules https://github.com/lbryio/spee.ch/commit/f0435be0b27d25333fbc74939f204b3bd94a444f
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);
kauffj commented 2017-08-25 19:34:59 +02:00 (Migrated from github.com)
Review

The old URL ought to be 301 redirect to the new one.

The old URL ought to be 301 redirect to the new one.
bones7242 commented 2017-09-07 21:41:26 +02:00 (Migrated from github.com)
Review

added redirect 7a81d53c35

added redirect https://github.com/lbryio/spee.ch/commit/7a81d53c3570d07e8666aee2fbfd7209e7646827
}); });
}); });
// 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,38 +105,68 @@ module.exports = (app) => {
let name = params.name; let name = params.name;
let method; let method;
let fileExtension; let fileExtension;
if (name.indexOf('.') !== -1) { let channelName = null;
method = SERVE; let channelId = null;
if (headers['accept'] && headers['accept'].split(',').includes('text/html')) { if (name.charAt(0) === '@') {
method = SHOWLITE; channelName = name;
const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR);
if (channelIdIndex !== -1) {
channelId = channelName.substring(channelIdIndex + 1);
channelName = channelName.substring(0, channelIdIndex);
} }
fileExtension = name.substring(name.indexOf('.')); logger.debug('channel name =', channelName);
name = name.substring(0, name.indexOf('.')); logger.debug('channel Id =', channelId);
logger.debug('file extension =', fileExtension); // 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 { } else {
method = SHOW; if (name.indexOf('.') !== -1) {
if (headers['accept'] && !headers['accept'].split(',').includes('text/html')) {
method = SERVE; method = SERVE;
} if (headers['accept'] && headers['accept'].split(',').includes('text/html')) {
} method = SHOWLITE;
logger.debug('claim name = ', name); }
logger.debug('method =', method); fileExtension = name.substring(name.indexOf('.'));
// 1. retrieve the asset and information name = name.substring(0, name.indexOf('.'));
getAsset(NAME, null, null, null, name) logger.debug('file extension =', fileExtension);
// 2. serve or show
.then(fileInfo => {
if (!fileInfo) {
res.status(200).render('noClaims');
} else { } else {
return serveOrShowAsset(fileInfo, null, method, headers, originalUrl, ip, res); method = SHOW;
if (headers['accept'] && !headers['accept'].split(',').includes('text/html')) {
method = SERVE;
}
} }
}) logger.debug('claim name = ', name);
// 3. update the database logger.debug('method =', method);
.then(fileInfoForUpdate => { // 1. retrieve the asset and information
// if needed, this is where we would update the file getAsset(CLAIM, null, null, name, null)
}) // 2. respond to the request
.catch(error => { .then(fileInfo => {
handleRequestError('serve', originalUrl, ip, error, res); if (!fileInfo) {
}); res.status(200).render('noClaims');
} else {
return serveOrShowAsset(fileInfo, null, method, headers, originalUrl, ip, res);
}
})
// 3. update the database
.then(fileInfoForUpdate => {
// if needed, this is where we would update the file
})
.catch(error => {
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}}" > {{else}}
{{!--fallback--}} <img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.ext" />
Your browser does not support the <code>video</code> element. {{/ifConditional}}
</video>
{{else}}
<img class="grid-item trending-image" src="/media/{{this.fileName}}" onload="resetTrendingLayout()"/>
{{/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>