Merge branch 'master' into update-passwords
This commit is contained in:
commit
d3c8a5dbd8
39 changed files with 1073 additions and 820 deletions
22
README.md
22
README.md
|
@ -24,19 +24,23 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
|
|||
## API
|
||||
|
||||
#### GET
|
||||
* /api/resolve/:name
|
||||
* example: `curl https://spee.ch/api/resolve/doitlive`
|
||||
* /api/claim_list/:name
|
||||
* example: `curl https://spee.ch/api/claim_list/doitlive`
|
||||
* /api/isClaimAvailable/:name (returns `true`/`false` for whether a name is available through spee.ch)
|
||||
* example: `curl https://spee.ch/api/isClaimAvailable/doitlive`
|
||||
* /api/claim-resolve/:name
|
||||
* example: `curl https://spee.ch/api/claim-resolve/doitlive`
|
||||
* /api/claim-list/:name
|
||||
* example: `curl https://spee.ch/api/claim-list/doitlive`
|
||||
* /api/claim-is-available/:name (
|
||||
* returns `true`/`false` for whether a name is available through spee.ch
|
||||
* example: `curl https://spee.ch/api/claim-is-available/doitlive`
|
||||
* /api/channel-is-available/:name (
|
||||
* returns `true`/`false` for whether a channel is available through spee.ch
|
||||
* example: `curl https://spee.ch/api/channel-is-available/@CoolChannel`
|
||||
|
||||
#### POST
|
||||
* /api/publish
|
||||
* example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/publish`
|
||||
* /api/claim-publish
|
||||
* example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim-publish`
|
||||
* Parameters:
|
||||
* `name`
|
||||
* `file` (.mp4, .jpeg, .jpg, .gif, or .png)
|
||||
* `file` (must be type .mp4, .jpeg, .jpg, .gif, or .png)
|
||||
* `nsfw` (optional)
|
||||
* `license` (optional)
|
||||
* `title` (optional)
|
||||
|
|
|
@ -4,6 +4,7 @@ const winstonSlackWebHook = require('winston-slack-webhook').SlackWebHook;
|
|||
module.exports = (winston) => {
|
||||
if (config.logging.slackWebHook) {
|
||||
// add a transport for errors to slack
|
||||
if (config.logging.slackErrorChannel) {
|
||||
winston.add(winstonSlackWebHook, {
|
||||
name : 'slack-errors-transport',
|
||||
level : 'warn',
|
||||
|
@ -12,6 +13,8 @@ module.exports = (winston) => {
|
|||
username : 'spee.ch',
|
||||
iconEmoji : ':face_with_head_bandage:',
|
||||
});
|
||||
};
|
||||
if (config.logging.slackInfoChannel) {
|
||||
winston.add(winstonSlackWebHook, {
|
||||
name : 'slack-info-transport',
|
||||
level : 'info',
|
||||
|
@ -20,11 +23,11 @@ module.exports = (winston) => {
|
|||
username : 'spee.ch',
|
||||
iconEmoji : ':nerd_face:',
|
||||
});
|
||||
};
|
||||
// send test message
|
||||
winston.error('Slack "error" logging is online.');
|
||||
winston.warn('Slack "warning" logging is online.');
|
||||
winston.info('Slack "info" logging is online.');
|
||||
} else {
|
||||
winston.warn('Slack logging is not enabled because no SLACK_WEB_HOOK env var provided.');
|
||||
winston.warn('Slack logging is not enabled because no slackWebHook config var provided.');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,253 +1,90 @@
|
|||
const lbryApi = require('../helpers/lbryApi.js');
|
||||
const db = require('../models');
|
||||
const logger = require('winston');
|
||||
const { serveFile, showFile, showFileLite } = require('../helpers/serveHelpers.js');
|
||||
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
|
||||
const { returnPaginatedChannelViewData } = require('../helpers/channelPagination.js');
|
||||
|
||||
const SERVE = 'SERVE';
|
||||
const SHOW = 'SHOW';
|
||||
const SHOWLITE = 'SHOWLITE';
|
||||
const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png';
|
||||
const NO_CHANNEL = 'NO_CHANNEL';
|
||||
const NO_CLAIM = 'NO_CLAIM';
|
||||
|
||||
function checkForLocalAssetByClaimId (claimId, name) {
|
||||
logger.debug(`checkForLocalAssetsByClaimId(${claimId}, ${name}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
db.File
|
||||
.findOne({where: { name, claimId }})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
resolve(result.dataValues);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
name,
|
||||
claimId,
|
||||
outpoint,
|
||||
height,
|
||||
address,
|
||||
fileName: '',
|
||||
filePath: '',
|
||||
fileType: '',
|
||||
nsfw,
|
||||
};
|
||||
}
|
||||
|
||||
function getAssetByLongClaimId (fullClaimId, name) {
|
||||
logger.debug('...getting asset by claim Id...');
|
||||
return new Promise((resolve, reject) => {
|
||||
// 1. check locally for claim
|
||||
checkForLocalAssetByClaimId(fullClaimId, name)
|
||||
.then(dataValues => {
|
||||
// if a result was found, return early with the result
|
||||
if (dataValues) {
|
||||
logger.debug('found a local file for this name and claimId');
|
||||
resolve(dataValues);
|
||||
return;
|
||||
}
|
||||
logger.debug('no local file found for this name and claimId');
|
||||
// 2. if no local claim, resolve and get the claim
|
||||
db.Claim
|
||||
.resolveClaim(name, fullClaimId)
|
||||
.then(resolveResult => {
|
||||
// if no result, return early (claim doesn't exist or isn't free)
|
||||
if (!resolveResult) {
|
||||
resolve(NO_CLAIM);
|
||||
return;
|
||||
}
|
||||
logger.debug('resolve result >> ', resolveResult.dataValues);
|
||||
let fileRecord = {};
|
||||
// get the claim
|
||||
lbryApi.getClaim(`${name}#${fullClaimId}`)
|
||||
.then(getResult => {
|
||||
logger.debug('getResult >>', getResult);
|
||||
fileRecord = createFileRecord(resolveResult);
|
||||
fileRecord = addGetResultsToFileRecord(fileRecord, getResult);
|
||||
// insert a record in the File table & Update Claim table
|
||||
return db.File.create(fileRecord);
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('File record successfully updated');
|
||||
resolve(fileRecord);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function chooseThumbnail (claimInfo, defaultThumbnail) {
|
||||
if (!claimInfo.thumbnail || claimInfo.thumbnail.trim() === '') {
|
||||
return defaultThumbnail;
|
||||
}
|
||||
return claimInfo.thumbnail;
|
||||
}
|
||||
const NO_FILE = 'NO_FILE';
|
||||
|
||||
module.exports = {
|
||||
getAssetByClaim (claimName, claimId) {
|
||||
logger.debug(`getAssetByClaim(${claimName}, ${claimId})`);
|
||||
return new Promise((resolve, reject) => {
|
||||
db.Claim.getLongClaimId(claimName, claimId) // 1. get the long claim id
|
||||
.then(result => { // 2. get the asset using the long claim id
|
||||
logger.debug('long claim id ===', result);
|
||||
if (result === NO_CLAIM) {
|
||||
logger.debug('resolving NO_CLAIM');
|
||||
resolve(NO_CLAIM);
|
||||
return;
|
||||
}
|
||||
resolve(getAssetByLongClaimId(result, claimName));
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
getAssetByChannel (channelName, channelId, claimName) {
|
||||
logger.debug('getting asset by channel');
|
||||
return new Promise((resolve, reject) => {
|
||||
db.Certificate.getLongChannelId(channelName, channelId) // 1. get the long channel id
|
||||
.then(result => { // 2. get the long claim Id
|
||||
if (result === NO_CHANNEL) {
|
||||
resolve(NO_CHANNEL);
|
||||
return;
|
||||
}
|
||||
return db.Claim.getClaimIdByLongChannelId(result, claimName);
|
||||
})
|
||||
.then(result => { // 3. get the asset using the long claim id
|
||||
logger.debug('asset claim id =', result);
|
||||
if (result === NO_CHANNEL || result === NO_CLAIM) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
resolve(getAssetByLongClaimId(result, claimName));
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
getChannelContents (channelName, channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let longChannelId;
|
||||
let shortChannelId;
|
||||
db.Certificate.getLongChannelId(channelName, channelId) // 1. get the long channel Id
|
||||
.then(result => { // 2. get all claims for that channel
|
||||
if (result === NO_CHANNEL) {
|
||||
return NO_CHANNEL;
|
||||
}
|
||||
longChannelId = result;
|
||||
return db.Certificate.getShortChannelIdFromLongChannelId(longChannelId, channelName);
|
||||
})
|
||||
.then(result => { // 3. get all Claim records for this channel
|
||||
if (result === NO_CHANNEL) {
|
||||
return NO_CHANNEL;
|
||||
}
|
||||
shortChannelId = result;
|
||||
return db.Claim.getAllChannelClaims(longChannelId);
|
||||
})
|
||||
.then(result => { // 4. add extra data not available from Claim table
|
||||
if (result === NO_CHANNEL) {
|
||||
resolve(NO_CHANNEL);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
result.forEach(element => {
|
||||
const fileExtenstion = element.contentType.substring(element.contentType.lastIndexOf('/') + 1);
|
||||
element['showUrlLong'] = `/${channelName}:${longChannelId}/${element.name}`;
|
||||
element['directUrlLong'] = `/${channelName}:${longChannelId}/${element.name}.${fileExtenstion}`;
|
||||
element['showUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}`;
|
||||
element['directUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}.${fileExtenstion}`;
|
||||
element['thumbnail'] = chooseThumbnail(element, DEFAULT_THUMBNAIL);
|
||||
});
|
||||
}
|
||||
resolve({
|
||||
channelName,
|
||||
longChannelId,
|
||||
shortChannelId,
|
||||
claims: result,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
serveOrShowAsset (fileInfo, extension, method, headers, originalUrl, ip, res) {
|
||||
// add file extension to the file info
|
||||
if (extension === 'gifv') {
|
||||
fileInfo['fileExt'] = 'gifv';
|
||||
getClaimId (channelName, channelClaimId, name, claimId) {
|
||||
if (channelName) {
|
||||
return module.exports.getClaimIdByChannel(channelName, channelClaimId, name);
|
||||
} else {
|
||||
fileInfo['fileExt'] = fileInfo.fileName.substring(fileInfo.fileName.lastIndexOf('.') + 1);
|
||||
return module.exports.getClaimIdByClaim(name, claimId);
|
||||
}
|
||||
// add a record to the stats table
|
||||
postToStats(method, originalUrl, ip, fileInfo.name, fileInfo.claimId, 'success');
|
||||
// serve or show
|
||||
switch (method) {
|
||||
case SERVE:
|
||||
serveFile(fileInfo, res);
|
||||
sendGoogleAnalytics(method, headers, ip, originalUrl);
|
||||
return fileInfo;
|
||||
case SHOWLITE:
|
||||
return db.Claim.resolveClaim(fileInfo.name, fileInfo.claimId)
|
||||
.then(claimRecord => {
|
||||
fileInfo['title'] = claimRecord.title;
|
||||
fileInfo['description'] = claimRecord.description;
|
||||
showFileLite(fileInfo, res);
|
||||
return fileInfo;
|
||||
},
|
||||
getClaimIdByClaim (claimName, claimId) {
|
||||
logger.debug(`getClaimIdByClaim(${claimName}, ${claimId})`);
|
||||
return new Promise((resolve, reject) => {
|
||||
db.Claim.getLongClaimId(claimName, claimId)
|
||||
.then(longClaimId => {
|
||||
if (!longClaimId) {
|
||||
resolve(NO_CLAIM);
|
||||
}
|
||||
resolve(longClaimId);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('throwing serverOrShowAsset SHOWLITE error...');
|
||||
throw error;
|
||||
reject(error);
|
||||
});
|
||||
case SHOW:
|
||||
return db.Claim
|
||||
.getShortClaimIdFromLongClaimId(fileInfo.claimId, fileInfo.name)
|
||||
.then(shortId => {
|
||||
fileInfo['shortId'] = shortId;
|
||||
return db.Claim.resolveClaim(fileInfo.name, fileInfo.claimId);
|
||||
});
|
||||
},
|
||||
getClaimIdByChannel (channelName, channelClaimId, claimName) {
|
||||
logger.debug(`getClaimIdByChannel(${channelName}, ${channelClaimId}, ${claimName})`);
|
||||
return new Promise((resolve, reject) => {
|
||||
db.Certificate.getLongChannelId(channelName, channelClaimId) // 1. get the long channel id
|
||||
.then(longChannelId => {
|
||||
if (!longChannelId) {
|
||||
return [null, null];
|
||||
}
|
||||
return Promise.all([longChannelId, db.Claim.getClaimIdByLongChannelId(longChannelId, claimName)]); // 2. get the long claim id
|
||||
})
|
||||
.then(resolveResult => {
|
||||
logger.debug('resolve result >>', resolveResult.dataValues);
|
||||
fileInfo['thumbnail'] = chooseThumbnail(resolveResult, DEFAULT_THUMBNAIL);
|
||||
fileInfo['title'] = resolveResult.title;
|
||||
fileInfo['description'] = resolveResult.description;
|
||||
if (resolveResult.certificateId) { fileInfo['certificateId'] = resolveResult.certificateId };
|
||||
if (resolveResult.channelName) { fileInfo['channelName'] = resolveResult.channelName };
|
||||
showFile(fileInfo, res);
|
||||
return fileInfo;
|
||||
.then(([longChannelId, longClaimId]) => {
|
||||
if (!longChannelId) {
|
||||
return resolve(NO_CHANNEL);
|
||||
}
|
||||
if (!longClaimId) {
|
||||
return resolve(NO_CLAIM);
|
||||
}
|
||||
resolve(longClaimId);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('throwing serverOrShowAsset SHOW error...');
|
||||
throw error;
|
||||
reject(error);
|
||||
});
|
||||
default:
|
||||
logger.error('I did not recognize that method');
|
||||
break;
|
||||
});
|
||||
},
|
||||
getChannelViewData (channelName, channelClaimId, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 1. get the long channel Id (make sure channel exists)
|
||||
db.Certificate.getLongChannelId(channelName, channelClaimId)
|
||||
.then(longChannelClaimId => {
|
||||
if (!longChannelClaimId) {
|
||||
return [null, null, null];
|
||||
}
|
||||
// 2. get the short ID and all claims for that channel
|
||||
return Promise.all([longChannelClaimId, db.Certificate.getShortChannelIdFromLongChannelId(longChannelClaimId, channelName), db.Claim.getAllChannelClaims(longChannelClaimId)]);
|
||||
})
|
||||
.then(([longChannelClaimId, shortChannelClaimId, channelClaimsArray]) => {
|
||||
if (!longChannelClaimId) {
|
||||
return resolve(NO_CHANNEL);
|
||||
}
|
||||
// 3. format the data for the view, including pagination
|
||||
let paginatedChannelViewData = returnPaginatedChannelViewData(channelName, longChannelClaimId, shortChannelClaimId, channelClaimsArray, query);
|
||||
// 4. return all the channel information and contents
|
||||
resolve(paginatedChannelViewData);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
getLocalFileRecord (claimId, name) {
|
||||
return db.File.findOne({where: {claimId, name}})
|
||||
.then(file => {
|
||||
if (!file) {
|
||||
return NO_FILE;
|
||||
}
|
||||
return file.dataValues;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -70,25 +70,23 @@ module.exports = {
|
|||
});
|
||||
},
|
||||
getTrendingClaims (startDate) {
|
||||
logger.debug('retrieving trending requests');
|
||||
logger.debug('retrieving trending');
|
||||
return new Promise((resolve, reject) => {
|
||||
// get the raw requests data
|
||||
db.getTrendingClaims(startDate)
|
||||
.then(results => {
|
||||
if (results) {
|
||||
results.forEach(element => {
|
||||
const fileExtenstion = element.fileType.substring(element.fileType.lastIndexOf('/') + 1);
|
||||
element['showUrlLong'] = `/${element.claimId}/${element.name}`;
|
||||
element['directUrlLong'] = `/${element.claimId}/${element.name}.${fileExtenstion}`;
|
||||
element['directUrlShort'] = `/${element.claimId}/${element.name}.${fileExtenstion}`;
|
||||
element['contentType'] = element.fileType;
|
||||
element['thumbnail'] = 'https://spee.ch/assets/img/video_thumb_default.png';
|
||||
db.getTrendingFiles(startDate)
|
||||
.then(fileArray => {
|
||||
let claimsPromiseArray = [];
|
||||
if (fileArray) {
|
||||
fileArray.forEach(file => {
|
||||
claimsPromiseArray.push(db.Claim.resolveClaim(file.name, file.claimId));
|
||||
});
|
||||
return Promise.all(claimsPromiseArray);
|
||||
}
|
||||
resolve(results);
|
||||
})
|
||||
.then(claimsArray => {
|
||||
resolve(claimsArray);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('sequelize error >>', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
|
71
helpers/channelPagination.js
Normal file
71
helpers/channelPagination.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
const CLAIMS_PER_PAGE = 10;
|
||||
|
||||
module.exports = {
|
||||
returnPaginatedChannelViewData (channelName, longChannelClaimId, shortChannelClaimId, claims, query) {
|
||||
const totalPages = module.exports.determineTotalPages(claims);
|
||||
const paginationPage = module.exports.getPageFromQuery(query);
|
||||
const viewData = {
|
||||
channelName : channelName,
|
||||
longChannelClaimId : longChannelClaimId,
|
||||
shortChannelClaimId: shortChannelClaimId,
|
||||
claims : module.exports.extractPageFromClaims(claims, paginationPage),
|
||||
previousPage : module.exports.determinePreviousPage(paginationPage),
|
||||
currentPage : paginationPage,
|
||||
nextPage : module.exports.determineNextPage(totalPages, paginationPage),
|
||||
totalPages : totalPages,
|
||||
totalResults : module.exports.determineTotalClaims(claims),
|
||||
};
|
||||
return viewData;
|
||||
},
|
||||
getPageFromQuery (query) {
|
||||
if (query.p) {
|
||||
return parseInt(query.p);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
extractPageFromClaims (claims, pageNumber) {
|
||||
if (!claims) {
|
||||
return []; // if no claims, return this default
|
||||
}
|
||||
// logger.debug('claims is array?', Array.isArray(claims));
|
||||
// logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber));
|
||||
const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE;
|
||||
const claimEndIndex = claimStartIndex + 10;
|
||||
const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex);
|
||||
return pageOfClaims;
|
||||
},
|
||||
determineTotalPages (claims) {
|
||||
if (!claims) {
|
||||
return 0;
|
||||
} else {
|
||||
const totalClaims = claims.length;
|
||||
if (totalClaims < CLAIMS_PER_PAGE) {
|
||||
return 1;
|
||||
}
|
||||
const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE);
|
||||
const remainder = totalClaims % CLAIMS_PER_PAGE;
|
||||
if (remainder === 0) {
|
||||
return fullPages;
|
||||
}
|
||||
return fullPages + 1;
|
||||
}
|
||||
},
|
||||
determinePreviousPage (currentPage) {
|
||||
if (currentPage === 1) {
|
||||
return null;
|
||||
}
|
||||
return currentPage - 1;
|
||||
},
|
||||
determineNextPage (totalPages, currentPage) {
|
||||
if (currentPage === totalPages) {
|
||||
return null;
|
||||
}
|
||||
return currentPage + 1;
|
||||
},
|
||||
determineTotalClaims (claims) {
|
||||
if (!claims) {
|
||||
return 0;
|
||||
}
|
||||
return claims.length;
|
||||
},
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
const logger = require('winston');
|
||||
const { postToStats } = require('../controllers/statsController.js');
|
||||
|
||||
module.exports = {
|
||||
returnErrorMessageAndStatus: function (error) {
|
||||
|
@ -33,17 +32,15 @@ module.exports = {
|
|||
}
|
||||
return [status, message];
|
||||
},
|
||||
handleRequestError: function (action, originalUrl, ip, error, res) {
|
||||
handleRequestError: function (originalUrl, ip, error, res) {
|
||||
logger.error(`Request Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
|
||||
postToStats(action, originalUrl, ip, null, null, error);
|
||||
const [status, message] = module.exports.returnErrorMessageAndStatus(error);
|
||||
res
|
||||
.status(status)
|
||||
.render('requestError', module.exports.createErrorResponsePayload(status, message));
|
||||
},
|
||||
handleApiError: function (action, originalUrl, ip, error, res) {
|
||||
logger.error(`Api ${action} Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
|
||||
postToStats(action, originalUrl, ip, null, null, error);
|
||||
handleApiError: function (originalUrl, ip, error, res) {
|
||||
logger.error(`Api Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
|
||||
const [status, message] = module.exports.returnErrorMessageAndStatus(error);
|
||||
res
|
||||
.status(status)
|
||||
|
|
|
@ -2,6 +2,10 @@ const Handlebars = require('handlebars');
|
|||
const config = require('../config/speechConfig.js');
|
||||
|
||||
module.exports = {
|
||||
placeCommonHeaderTags () {
|
||||
const headerBoilerplate = `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Spee.ch</title><link rel="stylesheet" href="/assets/css/reset.css" type="text/css"><link rel="stylesheet" href="/assets/css/general.css" type="text/css"><link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">`;
|
||||
return new Handlebars.SafeString(headerBoilerplate);
|
||||
},
|
||||
googleAnalytics () {
|
||||
const googleApiKey = config.analytics.googleId;
|
||||
const gaCode = `<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
|
@ -12,41 +16,35 @@ module.exports = {
|
|||
ga('send', 'pageview');</script>`;
|
||||
return new Handlebars.SafeString(gaCode);
|
||||
},
|
||||
addOpenGraph (title, mimeType, showUrl, source, description, thumbnail) {
|
||||
if (title === null || title.trim() === '') {
|
||||
title = 'Spee.ch';
|
||||
}
|
||||
if (description === null || description.trim() === '') {
|
||||
description = 'Open-source, decentralized image and video sharing.';
|
||||
}
|
||||
const ogTitle = `<meta property="og:title" content="${title}" >`;
|
||||
const ogUrl = `<meta property="og:url" content="${showUrl}" >`;
|
||||
const ogSiteName = `<meta property="og:site_name" content="Spee.ch" >`;
|
||||
const ogDescription = `<meta property="og:description" content="${description}" >`;
|
||||
const ogImageWidth = '<meta property="og:image:width" content="600" >';
|
||||
const ogImageHeight = '<meta property="og:image:height" content="315" >';
|
||||
const basicTags = `${ogTitle} ${ogUrl} ${ogSiteName} ${ogDescription} ${ogImageWidth} ${ogImageHeight}`;
|
||||
let ogImage = `<meta property="og:image" content="${source}" >`;
|
||||
let ogImageType = `<meta property="og:image:type" content="${mimeType}" >`;
|
||||
let ogType = `<meta property="og:type" content="article" >`;
|
||||
if (mimeType === 'video/mp4') {
|
||||
const ogVideo = `<meta property="og:video" content="${source}" >`;
|
||||
const ogVideoSecureUrl = `<meta property="og:video:secure_url" content="${source}" >`;
|
||||
const ogVideoType = `<meta property="og:video:type" content="${mimeType}" >`;
|
||||
ogImage = `<meta property="og:image" content="${thumbnail}" >`;
|
||||
ogImageType = `<meta property="og:image:type" content="image/png" >`;
|
||||
ogType = `<meta property="og:type" content="video" >`;
|
||||
return new Handlebars.SafeString(`${basicTags} ${ogImage} ${ogImageType} ${ogType} ${ogVideo} ${ogVideoSecureUrl} ${ogVideoType}`);
|
||||
addOpenGraph ({ ogTitle, contentType, ogDescription, thumbnail, showUrl, source, ogThumbnailContentType }) {
|
||||
const ogTitleTag = `<meta property="og:title" content="${ogTitle}" >`;
|
||||
const ogUrlTag = `<meta property="og:url" content="${showUrl}" >`;
|
||||
const ogSiteNameTag = `<meta property="og:site_name" content="Spee.ch" >`;
|
||||
const ogDescriptionTag = `<meta property="og:description" content="${ogDescription}" >`;
|
||||
const ogImageWidthTag = '<meta property="og:image:width" content="600" >';
|
||||
const ogImageHeightTag = '<meta property="og:image:height" content="315" >';
|
||||
const basicTags = `${ogTitleTag} ${ogUrlTag} ${ogSiteNameTag} ${ogDescriptionTag} ${ogImageWidthTag} ${ogImageHeightTag}`;
|
||||
let ogImageTag = `<meta property="og:image" content="${source}" >`;
|
||||
let ogImageTypeTag = `<meta property="og:image:type" content="${contentType}" >`;
|
||||
let ogTypeTag = `<meta property="og:type" content="article" >`;
|
||||
if (contentType === 'video/mp4') {
|
||||
const ogVideoTag = `<meta property="og:video" content="${source}" >`;
|
||||
const ogVideoSecureUrlTag = `<meta property="og:video:secure_url" content="${source}" >`;
|
||||
const ogVideoTypeTag = `<meta property="og:video:type" content="${contentType}" >`;
|
||||
ogImageTag = `<meta property="og:image" content="${thumbnail}" >`;
|
||||
ogImageTypeTag = `<meta property="og:image:type" content="${ogThumbnailContentType}" >`;
|
||||
ogTypeTag = `<meta property="og:type" content="video" >`;
|
||||
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag} ${ogVideoTag} ${ogVideoSecureUrlTag} ${ogVideoTypeTag}`);
|
||||
} else {
|
||||
if (mimeType === 'image/gif') {
|
||||
ogType = `<meta property="og:type" content="video.other" >`;
|
||||
if (contentType === 'image/gif') {
|
||||
ogTypeTag = `<meta property="og:type" content="video.other" >`;
|
||||
};
|
||||
return new Handlebars.SafeString(`${basicTags} ${ogImage} ${ogImageType} ${ogType}`);
|
||||
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag}`);
|
||||
}
|
||||
},
|
||||
addTwitterCard (mimeType, source, embedUrl, directFileUrl) {
|
||||
addTwitterCard ({ contentType, source, embedUrl, directFileUrl }) {
|
||||
const basicTwitterTags = `<meta name="twitter:site" content="@spee_ch" >`;
|
||||
if (mimeType === 'video/mp4') {
|
||||
if (contentType === 'video/mp4') {
|
||||
const twitterName = '<meta name="twitter:card" content="player" >';
|
||||
const twitterPlayer = `<meta name="twitter:player" content="${embedUrl}" >`;
|
||||
const twitterPlayerWidth = '<meta name="twitter:player:width" content="600" >';
|
||||
|
|
|
@ -10,7 +10,6 @@ function handleLbrynetResponse ({ data }, resolve, reject) {
|
|||
reject(data.result.error);
|
||||
return;
|
||||
};
|
||||
// logger.debug('data.result', data.result);
|
||||
resolve(data.result);
|
||||
return;
|
||||
}
|
||||
|
|
90
helpers/lbryUri.js
Normal file
90
helpers/lbryUri.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
const logger = require('winston');
|
||||
|
||||
module.exports = {
|
||||
REGEXP_INVALID_CLAIM : /[^A-Za-z0-9-]/g,
|
||||
REGEXP_INVALID_CHANNEL: /[^A-Za-z0-9-@]/g,
|
||||
REGEXP_ADDRESS : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/,
|
||||
CHANNEL_CHAR : '@',
|
||||
parseIdentifier : function (identifier) {
|
||||
logger.debug('parsing identifier:', identifier);
|
||||
const componentsRegex = new RegExp(
|
||||
'([^:$#/]*)' + // value (stops at the first separator or end)
|
||||
'([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)
|
||||
);
|
||||
const [proto, value, modifierSeperator, modifier] = componentsRegex
|
||||
.exec(identifier)
|
||||
.map(match => match || null);
|
||||
logger.debug(`${proto}, ${value}, ${modifierSeperator}, ${modifier}`);
|
||||
|
||||
// Validate and process name
|
||||
const isChannel = value.startsWith(module.exports.CHANNEL_CHAR);
|
||||
const channelName = isChannel ? value : null;
|
||||
let claimId;
|
||||
if (isChannel) {
|
||||
if (!channelName) {
|
||||
throw new Error('No channel name after @.');
|
||||
}
|
||||
const nameBadChars = (channelName).match(module.exports.REGEXP_INVALID_CHANNEL);
|
||||
if (nameBadChars) {
|
||||
throw new Error(`Invalid characters in channel name: ${nameBadChars.join(', ')}.`);
|
||||
}
|
||||
} else {
|
||||
claimId = value;
|
||||
}
|
||||
|
||||
// Validate and process modifier
|
||||
let channelClaimId;
|
||||
if (modifierSeperator) {
|
||||
if (!modifier) {
|
||||
throw new Error(`No modifier provided after separator ${modifierSeperator}.`);
|
||||
}
|
||||
|
||||
if (modifierSeperator === ':') {
|
||||
channelClaimId = modifier;
|
||||
} else {
|
||||
throw new Error(`The ${modifierSeperator} modifier is not currently supported.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
isChannel,
|
||||
channelName,
|
||||
channelClaimId,
|
||||
claimId,
|
||||
};
|
||||
},
|
||||
parseName: function (name) {
|
||||
logger.debug('parsing name:', name);
|
||||
const componentsRegex = new RegExp(
|
||||
'([^:$#/.]*)' + // name (stops at the first modifier)
|
||||
'([:$#.]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)
|
||||
);
|
||||
const [proto, claimName, modifierSeperator, modifier] = componentsRegex
|
||||
.exec(name)
|
||||
.map(match => match || null);
|
||||
logger.debug(`${proto}, ${claimName}, ${modifierSeperator}, ${modifier}`);
|
||||
|
||||
// Validate and process name
|
||||
if (!claimName) {
|
||||
throw new Error('No claim name provided before .');
|
||||
}
|
||||
const nameBadChars = (claimName).match(module.exports.REGEXP_INVALID_CLAIM);
|
||||
if (nameBadChars) {
|
||||
throw new Error(`Invalid characters in claim name: ${nameBadChars.join(', ')}.`);
|
||||
}
|
||||
// Validate and process modifier
|
||||
let isServeRequest = false;
|
||||
if (modifierSeperator) {
|
||||
if (!modifier) {
|
||||
throw new Error(`No file extension provided after separator ${modifierSeperator}.`);
|
||||
}
|
||||
if (modifierSeperator !== '.') {
|
||||
throw new Error(`The ${modifierSeperator} modifier is not supported in the claim name`);
|
||||
}
|
||||
isServeRequest = true;
|
||||
}
|
||||
return {
|
||||
claimName,
|
||||
isServeRequest,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,23 +1,23 @@
|
|||
module.exports = {
|
||||
returnShortId: function (result, longId) {
|
||||
returnShortId: function (claimsArray, longId) {
|
||||
let claimIndex;
|
||||
let shortId = longId.substring(0, 1); // default sort id is the first letter
|
||||
let shortId = longId.substring(0, 1); // default short id is the first letter
|
||||
let shortIdLength = 0;
|
||||
// find the index of this claim id
|
||||
claimIndex = result.findIndex(element => {
|
||||
claimIndex = claimsArray.findIndex(element => {
|
||||
return element.claimId === longId;
|
||||
});
|
||||
if (claimIndex < 0) {
|
||||
throw new Error('claim id not found in claims list');
|
||||
}
|
||||
// get an array of all claims with lower height
|
||||
let possibleMatches = result.slice(0, claimIndex);
|
||||
let possibleMatches = claimsArray.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 (element.claimId && (element.claimId.substring(0, shortIdLength) === shortId));
|
||||
});
|
||||
}
|
||||
return shortId;
|
||||
|
|
|
@ -1,45 +1,23 @@
|
|||
const logger = require('winston');
|
||||
|
||||
function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) {
|
||||
return {
|
||||
embedUrl : `https://spee.ch/embed/${claimId}/${name}`,
|
||||
showUrl : `https://spee.ch/${claimId}/${name}`,
|
||||
source : `https://spee.ch/${claimId}/${name}.${fileExt}`,
|
||||
directFileUrl: `https://spee.ch/${claimId}/${name}.${fileExt}`,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
serveFile ({ fileName, fileType, filePath }, res) {
|
||||
logger.verbose(`serving file ${fileName}`);
|
||||
// set default options
|
||||
let options = {
|
||||
serveFile ({ filePath, fileType }, claimId, name, res) {
|
||||
logger.verbose(`serving ${name}#${claimId}`);
|
||||
// set response options
|
||||
const headerContentType = fileType || 'image/jpeg';
|
||||
const options = {
|
||||
headers: {
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Content-Type' : fileType,
|
||||
'Content-Type' : headerContentType,
|
||||
},
|
||||
};
|
||||
// adjust default options as needed
|
||||
switch (fileType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/gif':
|
||||
case 'image/png':
|
||||
case 'video/mp4':
|
||||
break;
|
||||
default:
|
||||
logger.warn('sending file with unknown type as .jpeg');
|
||||
options['headers']['Content-Type'] = 'image/jpeg';
|
||||
break;
|
||||
}
|
||||
// send the file
|
||||
res.status(200).sendFile(filePath, options);
|
||||
},
|
||||
showFile (fileInfo, res) {
|
||||
const openGraphInfo = createOpenGraphInfo(fileInfo);
|
||||
res.status(200).render('show', { layout: 'show', fileInfo, openGraphInfo });
|
||||
showFile (claimInfo, shortId, res) {
|
||||
res.status(200).render('show', { layout: 'show', claimInfo, shortId });
|
||||
},
|
||||
showFileLite (fileInfo, res) {
|
||||
const openGraphInfo = createOpenGraphInfo(fileInfo);
|
||||
res.status(200).render('showLite', { layout: 'showlite', fileInfo, openGraphInfo });
|
||||
showFileLite (claimInfo, shortId, res) {
|
||||
res.status(200).render('showLite', { layout: 'showlite', claimInfo, shortId });
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const logger = require('winston');
|
||||
const { returnShortId } = require('../helpers/sequelizeHelpers.js');
|
||||
const NO_CHANNEL = 'NO_CHANNEL';
|
||||
|
||||
module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
||||
const Certificate = sequelize.define(
|
||||
|
@ -123,14 +122,14 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
});
|
||||
};
|
||||
|
||||
Certificate.getLongChannelIdFromShortChannelId = function (channelName, channelId) {
|
||||
Certificate.getLongChannelIdFromShortChannelId = function (channelName, channelClaimId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.findAll({
|
||||
where: {
|
||||
name : channelName,
|
||||
claimId: {
|
||||
$like: `${channelId}%`,
|
||||
$like: `${channelClaimId}%`,
|
||||
},
|
||||
},
|
||||
order: [['height', 'ASC']],
|
||||
|
@ -138,7 +137,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
.then(result => {
|
||||
switch (result.length) {
|
||||
case 0:
|
||||
return resolve(NO_CHANNEL);
|
||||
return resolve(null);
|
||||
default: // note results must be sorted
|
||||
return resolve(result[0].claimId);
|
||||
}
|
||||
|
@ -160,7 +159,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
.then(result => {
|
||||
switch (result.length) {
|
||||
case 0:
|
||||
return resolve(NO_CHANNEL);
|
||||
return resolve(null);
|
||||
default:
|
||||
return resolve(result[0].claimId);
|
||||
}
|
||||
|
@ -171,12 +170,29 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
});
|
||||
};
|
||||
|
||||
Certificate.getLongChannelId = function (channelName, channelId) {
|
||||
logger.debug(`getLongChannelId(${channelName}, ${channelId})`);
|
||||
if (channelId && (channelId.length === 40)) { // if a full channel id is provided
|
||||
return new Promise((resolve, reject) => resolve(channelId));
|
||||
} else if (channelId && channelId.length < 40) { // if a short channel id is provided
|
||||
return this.getLongChannelIdFromShortChannelId(channelName, channelId);
|
||||
Certificate.validateLongChannelId = function (name, claimId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.findOne({
|
||||
where: {name, claimId},
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Certificate.getLongChannelId = function (channelName, channelClaimId) {
|
||||
logger.debug(`getLongChannelId(${channelName}, ${channelClaimId})`);
|
||||
if (channelClaimId && (channelClaimId.length === 40)) { // if a full channel id is provided
|
||||
return this.validateLongChannelId(channelName, channelClaimId);
|
||||
} else if (channelClaimId && channelClaimId.length < 40) { // if a short channel id is provided
|
||||
return this.getLongChannelIdFromShortChannelId(channelName, channelClaimId);
|
||||
} else {
|
||||
return this.getLongChannelIdFromChannelName(channelName); // if no channel id provided
|
||||
}
|
||||
|
|
152
models/claim.js
152
models/claim.js
|
@ -1,6 +1,89 @@
|
|||
const logger = require('winston');
|
||||
const { returnShortId } = require('../helpers/sequelizeHelpers.js');
|
||||
const NO_CLAIM = 'NO_CLAIM';
|
||||
const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png';
|
||||
const DEFAULT_TITLE = 'Spee<ch';
|
||||
const DEFAULT_DESCRIPTION = 'Decentralized video and content hosting.';
|
||||
|
||||
function determineFileExtensionFromContentType (contentType) {
|
||||
switch (contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
return 'jpg';
|
||||
case 'image/png':
|
||||
return 'png';
|
||||
case 'image/gif':
|
||||
return 'gif';
|
||||
case 'video/mp4':
|
||||
return 'mp4';
|
||||
default:
|
||||
logger.debug('setting unknown file type as file extension jpg');
|
||||
return 'jpg';
|
||||
}
|
||||
};
|
||||
|
||||
function determineContentTypeFromFileExtension (fileExtension) {
|
||||
switch (fileExtension) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
return 'image/jpg';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'mp4':
|
||||
return 'video/mp4';
|
||||
default:
|
||||
logger.debug('setting unknown file type as type image/jpg');
|
||||
return 'image/jpg';
|
||||
}
|
||||
};
|
||||
|
||||
function ifEmptyReturnOther (value, replacement) {
|
||||
if (value === '') {
|
||||
return replacement;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function determineThumbnail (storedThumbnail, defaultThumbnail) {
|
||||
return ifEmptyReturnOther(storedThumbnail, defaultThumbnail);
|
||||
};
|
||||
|
||||
function determineOgTitle (storedTitle, defaultTitle) {
|
||||
return ifEmptyReturnOther(storedTitle, defaultTitle);
|
||||
};
|
||||
|
||||
function determineOgDescription (storedDescription, defaultDescription) {
|
||||
return ifEmptyReturnOther(storedDescription, defaultDescription);
|
||||
};
|
||||
|
||||
function determineOgThumbnailContentType (thumbnail) {
|
||||
if (thumbnail) {
|
||||
if (thumbnail.lastIndexOf('.') !== -1) {
|
||||
return determineContentTypeFromFileExtension(thumbnail.substring(thumbnail.lastIndexOf('.')));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function addOpengraphDataToClaim (claim) {
|
||||
claim['embedUrl'] = `https://spee.ch/embed/${claim.claimId}/${claim.name}`;
|
||||
claim['showUrl'] = `https://spee.ch/${claim.claimId}/${claim.name}`;
|
||||
claim['source'] = `https://spee.ch/${claim.claimId}/${claim.name}.${claim.fileExt}`;
|
||||
claim['directFileUrl'] = `https://spee.ch/${claim.claimId}/${claim.name}.${claim.fileExt}`;
|
||||
claim['ogTitle'] = determineOgTitle(claim.title, DEFAULT_TITLE);
|
||||
claim['ogDescription'] = determineOgDescription(claim.description, DEFAULT_DESCRIPTION);
|
||||
claim['ogThumbnailContentType'] = determineOgThumbnailContentType(claim.thumbnail);
|
||||
return claim;
|
||||
};
|
||||
|
||||
function prepareClaimData (claim) {
|
||||
// logger.debug('preparing claim data based on resolved data:', claim);
|
||||
claim['thumbnail'] = determineThumbnail(claim.thumbnail, DEFAULT_THUMBNAIL);
|
||||
claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);
|
||||
claim = addOpengraphDataToClaim(claim);
|
||||
return claim;
|
||||
};
|
||||
|
||||
module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
||||
const Claim = sequelize.define(
|
||||
|
@ -159,7 +242,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
};
|
||||
|
||||
Claim.getShortClaimIdFromLongClaimId = function (claimId, claimName) {
|
||||
logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimId}#${claimId}`);
|
||||
logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.findAll({
|
||||
|
@ -180,20 +263,27 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
});
|
||||
};
|
||||
|
||||
Claim.getAllChannelClaims = function (channelId) {
|
||||
logger.debug(`Claim.getAllChannelClaims for ${channelId}`);
|
||||
Claim.getAllChannelClaims = function (channelClaimId) {
|
||||
logger.debug(`Claim.getAllChannelClaims for ${channelClaimId}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.findAll({
|
||||
where: { certificateId: channelId },
|
||||
where: { certificateId: channelClaimId },
|
||||
order: [['height', 'ASC']],
|
||||
raw : true, // returns an array of only data, not an array of instances
|
||||
})
|
||||
.then(result => {
|
||||
switch (result.length) {
|
||||
.then(channelClaimsArray => {
|
||||
// logger.debug('channelclaimsarray length:', channelClaimsArray.length);
|
||||
switch (channelClaimsArray.length) {
|
||||
case 0:
|
||||
return resolve(null);
|
||||
default:
|
||||
return resolve(result);
|
||||
channelClaimsArray.forEach(claim => {
|
||||
claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);
|
||||
claim['thumbnail'] = determineThumbnail(claim.thumbnail, DEFAULT_THUMBNAIL);
|
||||
return claim;
|
||||
});
|
||||
return resolve(channelClaimsArray);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -202,18 +292,18 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
});
|
||||
};
|
||||
|
||||
Claim.getClaimIdByLongChannelId = function (channelId, claimName) {
|
||||
logger.debug(`finding claim id for claim ${claimName} from channel ${channelId}`);
|
||||
Claim.getClaimIdByLongChannelId = function (channelClaimId, claimName) {
|
||||
logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.findAll({
|
||||
where: { name: claimName, certificateId: channelId },
|
||||
where: { name: claimName, certificateId: channelClaimId },
|
||||
order: [['id', 'ASC']],
|
||||
})
|
||||
.then(result => {
|
||||
switch (result.length) {
|
||||
case 0:
|
||||
return resolve(NO_CLAIM);
|
||||
return resolve(null);
|
||||
case 1:
|
||||
return resolve(result[0].claimId);
|
||||
default:
|
||||
|
@ -241,7 +331,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
.then(result => {
|
||||
switch (result.length) {
|
||||
case 0:
|
||||
return resolve(NO_CLAIM);
|
||||
return resolve(null);
|
||||
default: // note results must be sorted
|
||||
return resolve(result[0].claimId);
|
||||
}
|
||||
|
@ -260,12 +350,12 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
order: [['effectiveAmount', 'DESC'], ['height', 'ASC']], // note: maybe height and effective amount need to switch?
|
||||
})
|
||||
.then(result => {
|
||||
logger.debug('length of result', result.length);
|
||||
switch (result.length) {
|
||||
case 0:
|
||||
return resolve(NO_CLAIM);
|
||||
return resolve(null);
|
||||
default:
|
||||
logger.debug('getTopFreeClaimIdByClaimName result:', result.dataValues);
|
||||
return resolve(result[0].claimId);
|
||||
return resolve(result[0].dataValues.claimId);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -274,10 +364,27 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
});
|
||||
};
|
||||
|
||||
Claim.validateLongClaimId = function (name, claimId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.findOne({
|
||||
where: {name, claimId},
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Claim.getLongClaimId = function (claimName, claimId) {
|
||||
logger.debug(`getLongClaimId(${claimName}, ${claimId})`);
|
||||
if (claimId && (claimId.length === 40)) { // if a full claim id is provided
|
||||
return new Promise((resolve, reject) => resolve(claimId));
|
||||
return this.validateLongClaimId(claimName, claimId);
|
||||
} else if (claimId && claimId.length < 40) {
|
||||
return this.getLongClaimIdFromShortClaimId(claimName, claimId); // if a short claim id is provided
|
||||
} else {
|
||||
|
@ -291,15 +398,16 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
.findAll({
|
||||
where: { name, claimId },
|
||||
})
|
||||
.then(result => {
|
||||
switch (result.length) {
|
||||
.then(claimArray => {
|
||||
logger.debug('claims found on resolve:', claimArray.length);
|
||||
switch (claimArray.length) {
|
||||
case 0:
|
||||
return resolve(null);
|
||||
case 1:
|
||||
return resolve(result[0]);
|
||||
return resolve(prepareClaimData(claimArray[0].dataValues));
|
||||
default:
|
||||
logger.warn(`more than one entry matches that name (${name}) and claimID (${claimId})`);
|
||||
return resolve(result[0]);
|
||||
logger.error(`more than one entry matches that name (${name}) and claimID (${claimId})`);
|
||||
return resolve(prepareClaimData(claimArray[0].dataValues));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
|
@ -69,11 +69,12 @@ db.upsert = (Model, values, condition, tableName) => {
|
|||
})
|
||||
.catch(function (error) {
|
||||
logger.error(`${tableName}.upsert error`, error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// add a 'getTrendingClaims' method to the db object
|
||||
db.getTrendingClaims = (startDate) => {
|
||||
// add a 'getTrendingFiles' method to the db object. note: change this to get claims directly. might need new association between Request and Claim
|
||||
db.getTrendingFiles = (startDate) => {
|
||||
return db.sequelize.query(`SELECT COUNT(*), File.* FROM Request LEFT JOIN File ON Request.FileId = File.id WHERE FileId IS NOT NULL AND nsfw != 1 AND trendingEligible = 1 AND Request.createdAt > "${startDate}" GROUP BY FileId ORDER BY COUNT(*) DESC LIMIT 25;`, { type: db.sequelize.QueryTypes.SELECT });
|
||||
};
|
||||
|
||||
|
|
|
@ -99,6 +99,10 @@ h3, p {
|
|||
font-size: small;
|
||||
}
|
||||
|
||||
#show-body > .fine-print {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #4156C5;
|
||||
}
|
||||
|
@ -153,9 +157,7 @@ a, a:visited {
|
|||
.link--primary, .link--primary:visited {
|
||||
color: #4156C5;
|
||||
}
|
||||
.link--primary.fine-print {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link--nav {
|
||||
color: black;
|
||||
border-bottom: 2px solid white;
|
||||
|
@ -296,10 +298,6 @@ a, a:visited {
|
|||
color: red;
|
||||
}
|
||||
|
||||
.info-message-placeholder {
|
||||
|
||||
}
|
||||
|
||||
/* INPUT FIELDS */
|
||||
|
||||
/* blocks */
|
||||
|
@ -347,14 +345,6 @@ option {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#claim-name-input {
|
||||
|
||||
}
|
||||
|
||||
#input-success-claim-name {
|
||||
|
||||
}
|
||||
|
||||
.span--relative {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -511,27 +501,34 @@ table {
|
|||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
/* Show page */
|
||||
/* Assets */
|
||||
|
||||
.video-show, .gifv-show, .image-show {
|
||||
display: block;
|
||||
width: 100%;
|
||||
.asset {
|
||||
max-width: 100%;
|
||||
}
|
||||
#video-player, .showlite-asset {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
|
||||
#show-body #asset-boilerpate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#showlite-body #asset-display-component {
|
||||
max-width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* video */
|
||||
|
||||
#video-asset {
|
||||
background-color: #000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
#showlite-body #video-player {
|
||||
margin-top: 2%;
|
||||
#showlite-body #video-asset {
|
||||
background-color: #ffffff;
|
||||
width: calc(100% - 12px - 12px - 2px);
|
||||
margin: 6px;
|
||||
padding: 6px;
|
||||
max-width: 50%;
|
||||
border: 1px solid #d0d0d0;
|
||||
}
|
||||
.showlite-asset {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* item lists */
|
||||
|
||||
|
|
|
@ -36,10 +36,14 @@
|
|||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.showlite-asset {
|
||||
#showlite-body #asset-display-component {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#showlite-body #asset-status {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
|
|
139
public/assets/js/assetConstructor.js
Normal file
139
public/assets/js/assetConstructor.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
const Asset = function () {
|
||||
this.data = {};
|
||||
this.addPlayPauseToVideo = function () {
|
||||
const that = this;
|
||||
const video = document.getElementById('video-asset');
|
||||
if (video) {
|
||||
// add event listener for click
|
||||
video.addEventListener('click', ()=> {
|
||||
that.playOrPause(video);
|
||||
});
|
||||
// add event listener for space bar
|
||||
document.body.onkeyup = (event) => {
|
||||
if (event.keyCode == 32) {
|
||||
that.playOrPause(video);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
this.playOrPause = function(video){
|
||||
if (video.paused == true) {
|
||||
video.play();
|
||||
}
|
||||
else{
|
||||
video.pause();
|
||||
}
|
||||
};
|
||||
this.showAsset = function () {
|
||||
this.hideAssetStatus();
|
||||
this.showAssetHolder();
|
||||
if (!this.data.src) {
|
||||
return console.log('error: src is not set')
|
||||
}
|
||||
if (!this.data.contentType) {
|
||||
return console.log('error: contentType is not set')
|
||||
}
|
||||
if (this.data.contentType === 'video/mp4') {
|
||||
this.showVideo();
|
||||
} else {
|
||||
this.showImage();
|
||||
}
|
||||
};
|
||||
this.showVideo = function () {
|
||||
console.log('showing video', this.data.src);
|
||||
const video = document.getElementById('video-asset');
|
||||
const source = document.createElement('source');
|
||||
source.setAttribute('src', this.data.src);
|
||||
video.appendChild(source);
|
||||
video.play();
|
||||
};
|
||||
this.showImage = function () {
|
||||
console.log('showing image', this.data.src);
|
||||
const asset = document.getElementById('image-asset');
|
||||
asset.setAttribute('src', this.data.src);
|
||||
};
|
||||
this.hideAssetStatus = function () {
|
||||
const assetStatus = document.getElementById('asset-status');
|
||||
assetStatus.hidden = true;
|
||||
};
|
||||
this.showAssetHolder =function () {
|
||||
const assetHolder = document.getElementById('asset-holder');
|
||||
assetHolder.hidden = false;
|
||||
};
|
||||
this.showSearchMessage = function () {
|
||||
const searchMessage = document.getElementById('searching-message');
|
||||
searchMessage.hidden = false;
|
||||
};
|
||||
this.showFailureMessage = function (msg) {
|
||||
console.log(msg);
|
||||
const searchMessage = document.getElementById('searching-message');
|
||||
const failureMessage = document.getElementById('failure-message');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
searchMessage.hidden = true;
|
||||
failureMessage.hidden = false;
|
||||
errorMessage.innerText = msg;
|
||||
};
|
||||
this.checkFileAndRenderAsset = function () {
|
||||
const that = this;
|
||||
this.isFileAvailable()
|
||||
.then(isAvailable => {
|
||||
if (!isAvailable) {
|
||||
console.log('file is not yet available on spee.ch');
|
||||
that.showSearchMessage();
|
||||
return that.getAssetOnSpeech();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
that.showAsset();
|
||||
})
|
||||
.catch(error => {
|
||||
that.showFailureMessage(error);
|
||||
})
|
||||
};
|
||||
this.isFileAvailable = function () {
|
||||
console.log(`checking if file is available for ${this.data.claimName}#${this.data.claimId}`)
|
||||
const uri = `/api/file-is-available/${this.data.claimName}/${this.data.claimId}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.open("GET", uri, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
const response = JSON.parse(xhr.response);
|
||||
if (xhr.status == 200) {
|
||||
console.log('isFileAvailable succeeded:', response);
|
||||
if (response.message === true) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
} else {
|
||||
console.log('isFileAvailable failed:', response);
|
||||
reject('Well this sucks, but we can\'t seem to phone home');
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
})
|
||||
};
|
||||
this.getAssetOnSpeech = function() {
|
||||
console.log(`getting claim for ${this.data.claimName}#${this.data.claimId}`)
|
||||
const uri = `/api/claim-get/${this.data.claimName}/${this.data.claimId}`;
|
||||
const xhr = new XMLHttpRequest();
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.open("GET", uri, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
const response = JSON.parse(xhr.response);
|
||||
if (xhr.status == 200) {
|
||||
console.log('getAssetOnSpeech succeeded:', response)
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log('getAssetOnSpeech failed:', response);
|
||||
reject(response.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
})
|
||||
};
|
||||
};
|
45
public/assets/js/progressBarConstructor.js
Normal file
45
public/assets/js/progressBarConstructor.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
const ProgressBar = function() {
|
||||
this.data = {
|
||||
x: 0,
|
||||
adder: 1,
|
||||
bars: [],
|
||||
};
|
||||
this.barHolder = document.getElementById('bar-holder');
|
||||
this.createProgressBar = function (size) {
|
||||
this.data['size'] = size;
|
||||
for (var i = 0; i < size; i++) {
|
||||
const bar = document.createElement('span');
|
||||
bar.innerText = '| ';
|
||||
bar.setAttribute('class', 'progress-bar progress-bar--inactive');
|
||||
this.barHolder.appendChild(bar);
|
||||
this.data.bars.push(bar);
|
||||
}
|
||||
};
|
||||
this.startProgressBar = function () {
|
||||
this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300);
|
||||
};
|
||||
this.updateProgressBar = function () {
|
||||
const x = this.data.x;
|
||||
const adder = this.data.adder;
|
||||
const size = this.data.size;
|
||||
// update the appropriate bar
|
||||
if (x > -1 && x < size){
|
||||
if (adder === 1){
|
||||
this.data.bars[x].setAttribute('class', 'progress-bar progress-bar--active');
|
||||
} else {
|
||||
this.data.bars[x].setAttribute('class', 'progress-bar progress-bar--inactive');
|
||||
}
|
||||
}
|
||||
// update adder
|
||||
if (x === size){
|
||||
this.data['adder'] = -1;
|
||||
} else if ( x === -1){
|
||||
this.data['adder'] = 1;
|
||||
}
|
||||
// update x
|
||||
this.data['x'] = x + adder;
|
||||
};
|
||||
this.stopProgressBar = function () {
|
||||
clearInterval(this.updateInterval);
|
||||
};
|
||||
};
|
|
@ -102,7 +102,7 @@ const publishFileFunctions = {
|
|||
return fd;
|
||||
},
|
||||
publishFile: function (file, metadata) {
|
||||
var uri = "/api/publish";
|
||||
var uri = "/api/claim-publish";
|
||||
var xhr = new XMLHttpRequest();
|
||||
var fd = this.appendDataToFormData(file, metadata);
|
||||
var that = this;
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
function playOrPause(video){
|
||||
if (video.paused == true) {
|
||||
video.play();
|
||||
}
|
||||
else{
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
// if a video player is present, set the listeners
|
||||
const video = document.getElementById('video-player');
|
||||
if (video) {
|
||||
// add event listener for click
|
||||
video.addEventListener('click', ()=> {
|
||||
playOrPause(video);
|
||||
});
|
||||
// add event listener for space bar
|
||||
document.body.onkeyup = (event) => {
|
||||
if (event.keyCode == 32) {
|
||||
playOrPause(video);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -119,13 +119,13 @@ const validationFunctions = {
|
|||
checkClaimName: function (name) {
|
||||
const successDisplayElement = document.getElementById('input-success-claim-name');
|
||||
const errorDisplayElement = document.getElementById('input-error-claim-name');
|
||||
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateClaimName, 'Sorry, that ending is already taken', '/api/isClaimAvailable/');
|
||||
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateClaimName, 'Sorry, that ending is already taken', '/api/claim-is-available/');
|
||||
},
|
||||
checkChannelName: function (name) {
|
||||
const successDisplayElement = document.getElementById('input-success-channel-name');
|
||||
const errorDisplayElement = document.getElementById('input-error-channel-name');
|
||||
name = `@${name}`;
|
||||
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/isChannelAvailable/');
|
||||
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/channel-is-available/');
|
||||
},
|
||||
// validation function which checks all aspects of the publish submission
|
||||
validateFilePublishSubmission: function (stagedFiles, metadata) {
|
||||
|
@ -162,7 +162,7 @@ const validationFunctions = {
|
|||
return;
|
||||
}
|
||||
// if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?)
|
||||
return that.isNameAvailable(claimName, '/api/isClaimAvailable/')
|
||||
return that.isNameAvailable(claimName, '/api/claim-is-available/')
|
||||
.then(result => {
|
||||
if (result) {
|
||||
resolve();
|
||||
|
@ -193,7 +193,7 @@ const validationFunctions = {
|
|||
return reject(error);
|
||||
}
|
||||
// 3. if all validation passes, check availability of the name
|
||||
that.isNameAvailable(channelName, '/api/isChannelAvailable/') // validate the availability
|
||||
that.isNameAvailable(channelName, '/api/channel-is-available/') // validate the availability
|
||||
.then(function(result) {
|
||||
if (result) {
|
||||
resolve();
|
||||
|
|
|
@ -4,36 +4,93 @@ const config = require('../config/speechConfig.js');
|
|||
const multipartMiddleware = multipart({uploadDir: config.files.uploadDirectory});
|
||||
const db = require('../models');
|
||||
const { publish } = require('../controllers/publishController.js');
|
||||
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
|
||||
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
|
||||
const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseChannelName, cleanseUserName, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
|
||||
const errorHandlers = require('../helpers/errorHandlers.js');
|
||||
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
|
||||
const { authenticateOrSkip } = require('../auth/authentication.js');
|
||||
|
||||
function addGetResultsToFileData (fileInfo, getResult) {
|
||||
fileInfo.fileName = getResult.file_name;
|
||||
fileInfo.filePath = getResult.download_path;
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
function createFileData ({ name, claimId, outpoint, height, address, nsfw, contentType }) {
|
||||
return {
|
||||
name,
|
||||
claimId,
|
||||
outpoint,
|
||||
height,
|
||||
address,
|
||||
fileName: '',
|
||||
filePath: '',
|
||||
fileType: contentType,
|
||||
nsfw,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = (app) => {
|
||||
// route to run a claim_list request on the daemon
|
||||
app.get('/api/claim_list/:name', ({ headers, ip, originalUrl, params }, res) => {
|
||||
// google analytics
|
||||
sendGoogleAnalytics('SERVE', headers, ip, originalUrl);
|
||||
// serve the content
|
||||
app.get('/api/claim-list/:name', ({ ip, originalUrl, params }, res) => {
|
||||
getClaimList(params.name)
|
||||
.then(claimsList => {
|
||||
postToStats('serve', originalUrl, ip, null, null, 'success');
|
||||
res.status(200).json(claimsList);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError('claim_list', originalUrl, ip, error, res);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to see if asset is available locally
|
||||
app.get('/api/file-is-available/:name/:claimId', ({ ip, originalUrl, params }, res) => {
|
||||
const name = params.name;
|
||||
const claimId = params.claimId;
|
||||
let isLocalFileAvailable = false;
|
||||
db.File.findOne({where: {name, claimId}})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
isLocalFileAvailable = true;
|
||||
}
|
||||
res.status(200).json({status: 'success', message: isLocalFileAvailable});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to get an asset
|
||||
app.get('/api/claim-get/:name/:claimId', ({ ip, originalUrl, params }, res) => {
|
||||
const name = params.name;
|
||||
const claimId = params.claimId;
|
||||
// resolve the claim
|
||||
db.Claim.resolveClaim(name, claimId)
|
||||
.then(resolveResult => {
|
||||
// make sure a claim actually exists at that uri
|
||||
if (!resolveResult) {
|
||||
throw new Error('No matching uri found in Claim table');
|
||||
}
|
||||
let fileData = createFileData(resolveResult);
|
||||
// get the claim
|
||||
return Promise.all([fileData, getClaim(`${name}#${claimId}`)]);
|
||||
})
|
||||
.then(([ fileData, getResult ]) => {
|
||||
fileData = addGetResultsToFileData(fileData, getResult);
|
||||
return Promise.all([db.upsert(db.File, fileData, {name, claimId}, 'File'), getResult]);
|
||||
})
|
||||
.then(([ fileRecord, {message, completed} ]) => {
|
||||
res.status(200).json({ status: 'success', message, completed });
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
|
||||
// route to check whether spee.ch has published to a claim
|
||||
app.get('/api/isClaimAvailable/:name', ({ params }, res) => {
|
||||
// send response
|
||||
app.get('/api/claim-is-available/:name', ({ params }, res) => {
|
||||
checkClaimNameAvailability(params.name)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
logger.debug(`Rejecting '${params.name}' because that name has already been claimed on spee.ch`);
|
||||
// logger.debug(`Rejecting '${params.name}' because that name has already been claimed on spee.ch`);
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
|
@ -42,37 +99,32 @@ module.exports = (app) => {
|
|||
});
|
||||
});
|
||||
// route to check whether spee.ch has published to a channel
|
||||
app.get('/api/isChannelAvailable/:name', ({ params }, res) => {
|
||||
app.get('/api/channel-is-available/:name', ({ params }, res) => {
|
||||
checkChannelAvailability(params.name)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
logger.debug(`Rejecting '${params.name}' because that channel has already been claimed on spee.ch`);
|
||||
// logger.debug(`Rejecting '${params.name}' because that channel has already been claimed on spee.ch`);
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logger.debug('api/isChannelAvailable/ error', error);
|
||||
res.status(500).json(error);
|
||||
});
|
||||
});
|
||||
// route to run a resolve request on the daemon
|
||||
app.get('/api/resolve/:uri', ({ headers, ip, originalUrl, params }, res) => {
|
||||
// google analytics
|
||||
sendGoogleAnalytics('SERVE', headers, ip, originalUrl);
|
||||
// serve content
|
||||
app.get('/api/claim-resolve/:uri', ({ headers, ip, originalUrl, params }, res) => {
|
||||
resolveUri(params.uri)
|
||||
.then(resolvedUri => {
|
||||
postToStats('serve', originalUrl, ip, null, null, 'success');
|
||||
res.status(200).json(resolvedUri);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError('resolve', originalUrl, ip, error, res);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to run a publish request on the daemon
|
||||
app.post('/api/publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => {
|
||||
app.post('/api/claim-publish', multipartMiddleware, ({ body, files, ip, originalUrl, user }, res) => {
|
||||
let file, fileName, filePath, fileType, name, nsfw, license, title, description, thumbnail, anonymous, skipAuth, channelName, channelPassword;
|
||||
// validate that mandatory parts of the request are present
|
||||
try {
|
||||
|
@ -123,7 +175,7 @@ module.exports = (app) => {
|
|||
}
|
||||
}
|
||||
channelName = cleanseChannelName(channelName);
|
||||
logger.debug(`/api/publish > name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`);
|
||||
logger.debug(`name: ${name}, license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}" nsfw: "${nsfw}"`);
|
||||
// check channel authorization
|
||||
authenticateOrSkip(skipAuth, channelName, channelPassword)
|
||||
.then(authenticated => {
|
||||
|
@ -156,13 +208,11 @@ module.exports = (app) => {
|
|||
});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError('publish', originalUrl, ip, error, res);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
|
||||
// route to get a short claim id from long claim Id
|
||||
app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => {
|
||||
// serve content
|
||||
app.get('/api/claim-shorten-id/:longId/:name', ({ params }, res) => {
|
||||
db.Claim.getShortClaimIdFromLongClaimId(params.longId, params.name)
|
||||
.then(shortId => {
|
||||
res.status(200).json(shortId);
|
||||
|
@ -173,8 +223,7 @@ module.exports = (app) => {
|
|||
});
|
||||
});
|
||||
// route to get a short channel id from long channel Id
|
||||
app.get('/api/shortChannelId/:longId/:name', ({ ip, originalUrl, params }, res) => {
|
||||
// serve content
|
||||
app.get('/api/channel-shorten-id/:longId/:name', ({ ip, originalUrl, params }, res) => {
|
||||
db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name)
|
||||
.then(shortId => {
|
||||
logger.debug('sending back short channel id', shortId);
|
||||
|
@ -182,7 +231,7 @@ module.exports = (app) => {
|
|||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting short channel id', error);
|
||||
errorHandlers.handleApiError('short channel id', originalUrl, ip, error, res);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.put('/api/password', ({ body, ip, originalUrl }, res) => {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
const { postToStats } = require('../controllers/statsController.js');
|
||||
|
||||
module.exports = app => {
|
||||
// route for the home page
|
||||
app.get('/', (req, res) => {
|
||||
|
@ -7,8 +5,6 @@ module.exports = app => {
|
|||
});
|
||||
// a catch-all route if someone visits a page that does not exist
|
||||
app.use('*', ({ originalUrl, ip }, res) => {
|
||||
// post to stats
|
||||
postToStats('show', originalUrl, ip, null, null, 'Error: 404');
|
||||
// send response
|
||||
res.status(404).render('fourOhFour');
|
||||
});
|
||||
|
|
|
@ -30,24 +30,22 @@ module.exports = (app) => {
|
|||
const dateTime = startDate.toISOString().slice(0, 19).replace('T', ' ');
|
||||
getTrendingClaims(dateTime)
|
||||
.then(result => {
|
||||
// logger.debug(result);
|
||||
res.status(200).render('popular', {
|
||||
trendingAssets: result,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleRequestError('popular', originalUrl, ip, error, res);
|
||||
errorHandlers.handleRequestError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to display a list of the trending images
|
||||
app.get('/new', ({ ip, originalUrl }, res) => {
|
||||
getRecentClaims()
|
||||
.then(result => {
|
||||
// logger.debug(result);
|
||||
res.status(200).render('new', { newClaims: result });
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleRequestError('new', originalUrl, ip, error, res);
|
||||
errorHandlers.handleRequestError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to send embedable video player (for twitter)
|
||||
|
|
|
@ -1,257 +1,204 @@
|
|||
const logger = require('winston');
|
||||
const { getAssetByClaim, getChannelContents, getAssetByChannel, serveOrShowAsset } = require('../controllers/serveController.js');
|
||||
const { getClaimId, getChannelViewData, getLocalFileRecord } = require('../controllers/serveController.js');
|
||||
const serveHelpers = require('../helpers/serveHelpers.js');
|
||||
const { handleRequestError } = require('../helpers/errorHandlers.js');
|
||||
const { postToStats } = require('../controllers/statsController.js');
|
||||
const db = require('../models');
|
||||
const lbryUri = require('../helpers/lbryUri.js');
|
||||
|
||||
const SERVE = 'SERVE';
|
||||
const SHOW = 'SHOW';
|
||||
const SHOWLITE = 'SHOWLITE';
|
||||
const CHANNEL = 'CHANNEL';
|
||||
const CLAIM = 'CLAIM';
|
||||
const CLAIM_ID_CHAR = ':';
|
||||
const CHANNEL_CHAR = '@';
|
||||
const CLAIMS_PER_PAGE = 10;
|
||||
const NO_CHANNEL = 'NO_CHANNEL';
|
||||
const NO_CLAIM = 'NO_CLAIM';
|
||||
const NO_FILE = 'NO_FILE';
|
||||
|
||||
function isValidClaimId (claimId) {
|
||||
return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
|
||||
}
|
||||
|
||||
function isValidShortId (claimId) {
|
||||
return claimId.length === 1; // really it should evaluate the short url itself
|
||||
return claimId.length === 1; // it should really evaluate the short url itself
|
||||
}
|
||||
|
||||
function isValidShortIdOrClaimId (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);
|
||||
function sendChannelInfoAndContentToClient (channelPageData, res) {
|
||||
if (channelPageData === NO_CHANNEL) {
|
||||
res.status(200).render('noChannel');
|
||||
} else {
|
||||
res.status(200).render('channel', channelPageData);
|
||||
}
|
||||
}
|
||||
|
||||
function showChannelPageToClient (channelName, channelClaimId, originalUrl, ip, query, res) {
|
||||
// 1. retrieve the channel contents
|
||||
getChannelViewData(channelName, channelClaimId, query)
|
||||
.then(channelViewData => {
|
||||
sendChannelInfoAndContentToClient(channelViewData, res);
|
||||
})
|
||||
.catch(error => {
|
||||
handleRequestError(originalUrl, ip, error, res);
|
||||
});
|
||||
}
|
||||
|
||||
function clientAcceptsHtml (headers) {
|
||||
return headers['accept'] && headers['accept'].split(',').includes('text/html');
|
||||
}
|
||||
|
||||
function determineResponseType (isServeRequest, headers) {
|
||||
let responseType;
|
||||
if (isServeRequest) {
|
||||
responseType = SERVE;
|
||||
if (clientAcceptsHtml(headers)) { // this is in case a serve request comes from a browser
|
||||
responseType = SHOWLITE;
|
||||
}
|
||||
} else {
|
||||
responseType = SHOW;
|
||||
if (!clientAcceptsHtml(headers)) { // this is in case someone embeds a show url
|
||||
responseType = SERVE;
|
||||
}
|
||||
}
|
||||
return responseType;
|
||||
}
|
||||
|
||||
function showAssetToClient (claimId, name, res) {
|
||||
return Promise
|
||||
.all([db.Claim.resolveClaim(name, claimId), db.Claim.getShortClaimIdFromLongClaimId(claimId, name)])
|
||||
.then(([claimInfo, shortClaimId]) => {
|
||||
logger.debug('claimInfo:', claimInfo);
|
||||
logger.debug('shortClaimId:', shortClaimId);
|
||||
return serveHelpers.showFile(claimInfo, shortClaimId, res);
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function showLiteAssetToClient (claimId, name, res) {
|
||||
return Promise
|
||||
.all([db.Claim.resolveClaim(name, claimId), db.Claim.getShortClaimIdFromLongClaimId(claimId, name)])
|
||||
.then(([claimInfo, shortClaimId]) => {
|
||||
logger.debug('claimInfo:', claimInfo);
|
||||
logger.debug('shortClaimId:', shortClaimId);
|
||||
return serveHelpers.showFileLite(claimInfo, shortClaimId, res);
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function serveAssetToClient (claimId, name, res) {
|
||||
return getLocalFileRecord(claimId, name)
|
||||
.then(fileInfo => {
|
||||
logger.debug('fileInfo:', fileInfo);
|
||||
if (fileInfo === NO_FILE) {
|
||||
return res.status(307).redirect(`/api/claim-get/${name}/${claimId}`);
|
||||
}
|
||||
return serveHelpers.serveFile(fileInfo, claimId, name, res);
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function showOrServeAsset (responseType, claimId, claimName, res) {
|
||||
switch (responseType) {
|
||||
case SHOW:
|
||||
return showAssetToClient(claimId, claimName, res);
|
||||
case SHOWLITE:
|
||||
return showLiteAssetToClient(claimId, claimName, res);
|
||||
case SERVE:
|
||||
return serveAssetToClient(claimId, claimName, res);
|
||||
default:
|
||||
return new Error('that claim type was not found');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getPage (query) {
|
||||
if (query.p) {
|
||||
return parseInt(query.p);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function extractPageFromClaims (claims, pageNumber) {
|
||||
logger.debug('claims is array?', Array.isArray(claims));
|
||||
logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber));
|
||||
const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE;
|
||||
const claimEndIndex = claimStartIndex + 10;
|
||||
const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex);
|
||||
return pageOfClaims;
|
||||
}
|
||||
|
||||
function determineTotalPages (totalClaims) {
|
||||
if (totalClaims === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (totalClaims < CLAIMS_PER_PAGE) {
|
||||
return 1;
|
||||
}
|
||||
const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE);
|
||||
const remainder = totalClaims % CLAIMS_PER_PAGE;
|
||||
if (remainder === 0) {
|
||||
return fullPages;
|
||||
}
|
||||
return fullPages + 1;
|
||||
}
|
||||
|
||||
function determinePreviousPage (currentPage) {
|
||||
if (currentPage === 1) {
|
||||
return null;
|
||||
}
|
||||
return currentPage - 1;
|
||||
}
|
||||
|
||||
function determineNextPage (totalPages, currentPage) {
|
||||
if (currentPage === totalPages) {
|
||||
return null;
|
||||
}
|
||||
return currentPage + 1;
|
||||
}
|
||||
|
||||
module.exports = (app) => {
|
||||
// route to serve a specific asset
|
||||
app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => {
|
||||
let identifier = params.identifier;
|
||||
let name = params.name;
|
||||
let claimOrChannel;
|
||||
let channelName = null;
|
||||
let claimId = null;
|
||||
let channelId = null;
|
||||
let method;
|
||||
let fileExtension;
|
||||
// parse the name
|
||||
const positionOfExtension = name.indexOf('.');
|
||||
if (positionOfExtension >= 0) {
|
||||
fileExtension = name.substring(positionOfExtension + 1);
|
||||
name = name.substring(0, positionOfExtension);
|
||||
/* patch because twitter player preview adds '>' before file extension */
|
||||
if (name.indexOf('>') >= 0) {
|
||||
name = name.substring(0, name.indexOf('>'));
|
||||
}
|
||||
/* end patch */
|
||||
logger.debug('file extension =', fileExtension);
|
||||
if (headers['accept'] && headers['accept'].split(',').includes('text/html')) {
|
||||
method = SHOWLITE;
|
||||
} else {
|
||||
method = SERVE;
|
||||
}
|
||||
} else {
|
||||
method = SHOW;
|
||||
if (!headers['accept'] || !headers['accept'].split(',').includes('text/html')) {
|
||||
method = SERVE;
|
||||
}
|
||||
}
|
||||
/* patch for backwards compatability with spee.ch/name/claim_id */
|
||||
function flipClaimNameAndIdForBackwardsCompatibility (identifier, name) {
|
||||
// this is a patch for backwards compatability with 'spee.ch/name/claim_id' url format
|
||||
if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) {
|
||||
let tempName = name;
|
||||
const tempName = name;
|
||||
name = identifier;
|
||||
identifier = tempName;
|
||||
}
|
||||
/* end patch */
|
||||
logger.debug('claim name =', name);
|
||||
logger.debug('method =', method);
|
||||
// parse identifier for whether it is a channel, short url, or claim_id
|
||||
if (identifier.charAt(0) === '@') {
|
||||
channelName = identifier;
|
||||
claimOrChannel = CHANNEL;
|
||||
const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR);
|
||||
if (channelIdIndex !== -1) {
|
||||
channelId = channelName.substring(channelIdIndex + 1);
|
||||
channelName = channelName.substring(0, channelIdIndex);
|
||||
return [identifier, name];
|
||||
}
|
||||
logger.debug('channel name =', channelName);
|
||||
} else {
|
||||
claimId = identifier;
|
||||
logger.debug('claim id =', claimId);
|
||||
claimOrChannel = CLAIM;
|
||||
|
||||
function logRequestData (responseType, claimName, channelName, claimId) {
|
||||
logger.debug('responseType ===', responseType);
|
||||
logger.debug('claim name === ', claimName);
|
||||
logger.debug('channel name ===', channelName);
|
||||
logger.debug('claim id ===', claimId);
|
||||
}
|
||||
// 1. retrieve the asset and information
|
||||
getAsset(claimOrChannel, channelName, channelId, name, claimId)
|
||||
// 2. serve or show
|
||||
.then(result => {
|
||||
logger.debug('getAsset result:', result);
|
||||
if (result === NO_CLAIM) {
|
||||
res.status(200).render('noClaim');
|
||||
return;
|
||||
} else if (result === NO_CHANNEL) {
|
||||
res.status(200).render('noChannel');
|
||||
return;
|
||||
|
||||
module.exports = (app) => {
|
||||
// route to serve a specific asset using the channel or claim id
|
||||
app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => {
|
||||
let isChannel, channelName, channelClaimId, claimId, claimName, isServeRequest;
|
||||
try {
|
||||
({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(params.identifier));
|
||||
({ claimName, isServeRequest } = lbryUri.parseName(params.name));
|
||||
} catch (error) {
|
||||
return handleRequestError(originalUrl, ip, error, res);
|
||||
}
|
||||
return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res);
|
||||
})
|
||||
// 3. update the file
|
||||
.then(fileInfoForUpdate => {
|
||||
// if needed, this is where we would update the file
|
||||
if (!isChannel) {
|
||||
[claimId, claimName] = flipClaimNameAndIdForBackwardsCompatibility(claimId, claimName);
|
||||
}
|
||||
let responseType = determineResponseType(isServeRequest, headers);
|
||||
// log the request data for debugging
|
||||
logRequestData(responseType, claimName, channelName, claimId);
|
||||
// get the claim Id and then serve/show the asset
|
||||
getClaimId(channelName, channelClaimId, claimName, claimId)
|
||||
.then(fullClaimId => {
|
||||
if (fullClaimId === NO_CLAIM) {
|
||||
return res.status(200).render('noClaim');
|
||||
} else if (fullClaimId === NO_CHANNEL) {
|
||||
return res.status(200).render('noChannel');
|
||||
}
|
||||
showOrServeAsset(responseType, fullClaimId, claimName, res);
|
||||
postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
handleRequestError('serve', originalUrl, ip, error, res);
|
||||
handleRequestError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to serve the winning asset at a claim
|
||||
app.get('/:name', ({ headers, ip, originalUrl, params, query }, res) => {
|
||||
// parse name param
|
||||
let name = params.name;
|
||||
let method;
|
||||
let fileExtension;
|
||||
let channelName = null;
|
||||
let channelId = null;
|
||||
// (a) handle channel requests
|
||||
if (name.charAt(0) === CHANNEL_CHAR) {
|
||||
channelName = name;
|
||||
const paginationPage = getPage(query);
|
||||
const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR);
|
||||
if (channelIdIndex !== -1) {
|
||||
channelId = channelName.substring(channelIdIndex + 1);
|
||||
channelName = channelName.substring(0, channelIdIndex);
|
||||
// route to serve the winning asset at a claim or a channel page
|
||||
app.get('/:identifier', ({ headers, ip, originalUrl, params, query }, res) => {
|
||||
let isChannel, channelName, channelClaimId;
|
||||
try {
|
||||
({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(params.identifier));
|
||||
} catch (error) {
|
||||
return handleRequestError(originalUrl, ip, error, res);
|
||||
}
|
||||
logger.debug('channel name =', channelName);
|
||||
logger.debug('channel Id =', channelId);
|
||||
// 1. retrieve the channel contents
|
||||
getChannelContents(channelName, channelId)
|
||||
// 2. respond to the request
|
||||
.then(result => {
|
||||
if (result === NO_CHANNEL) { // no channel found
|
||||
res.status(200).render('noChannel');
|
||||
} else if (!result.claims) { // channel found, but no claims
|
||||
res.status(200).render('channel', {
|
||||
layout : 'channel',
|
||||
channelName : result.channelName,
|
||||
longChannelId : result.longChannelId,
|
||||
shortChannelId: result.shortChannelId,
|
||||
claims : [],
|
||||
previousPage : 0,
|
||||
currentPage : 0,
|
||||
nextPage : 0,
|
||||
totalPages : 0,
|
||||
totalResults : 0,
|
||||
});
|
||||
} else { // channel found, with claims
|
||||
const totalPages = determineTotalPages(result.claims.length);
|
||||
res.status(200).render('channel', {
|
||||
layout : 'channel',
|
||||
channelName : result.channelName,
|
||||
longChannelId : result.longChannelId,
|
||||
shortChannelId: result.shortChannelId,
|
||||
claims : extractPageFromClaims(result.claims, paginationPage),
|
||||
previousPage : determinePreviousPage(paginationPage),
|
||||
currentPage : paginationPage,
|
||||
nextPage : determineNextPage(totalPages, paginationPage),
|
||||
totalPages : totalPages,
|
||||
totalResults : result.claims.length,
|
||||
});
|
||||
if (isChannel) {
|
||||
// log the request data for debugging
|
||||
logRequestData(null, null, channelName, null);
|
||||
// handle showing the channel page
|
||||
showChannelPageToClient(channelName, channelClaimId, originalUrl, ip, query, res);
|
||||
} else {
|
||||
let claimName, isServeRequest;
|
||||
try {
|
||||
({claimName, isServeRequest} = lbryUri.parseName(params.identifier));
|
||||
} catch (error) {
|
||||
return handleRequestError(originalUrl, ip, error, res);
|
||||
}
|
||||
let responseType = determineResponseType(isServeRequest, headers);
|
||||
// log the request data for debugging
|
||||
logRequestData(responseType, claimName, null, null);
|
||||
// get the claim Id and then serve/show the asset
|
||||
getClaimId(null, null, claimName, null)
|
||||
.then(fullClaimId => {
|
||||
if (fullClaimId === NO_CLAIM) {
|
||||
return res.status(200).render('noClaim');
|
||||
}
|
||||
showOrServeAsset(responseType, fullClaimId, claimName, res);
|
||||
postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
handleRequestError('serve', originalUrl, ip, error, res);
|
||||
});
|
||||
// (b) handle stream requests
|
||||
} else {
|
||||
if (name.indexOf('.') !== -1) {
|
||||
method = SERVE;
|
||||
if (headers['accept'] && headers['accept'].split(',').includes('text/html')) {
|
||||
method = SHOWLITE;
|
||||
}
|
||||
fileExtension = name.substring(name.indexOf('.') + 1);
|
||||
name = name.substring(0, name.indexOf('.'));
|
||||
logger.debug('file extension =', fileExtension);
|
||||
} else {
|
||||
method = SHOW;
|
||||
if (!headers['accept'] || !headers['accept'].split(',').includes('text/html')) {
|
||||
method = SERVE;
|
||||
}
|
||||
}
|
||||
logger.debug('claim name = ', name);
|
||||
logger.debug('method =', method);
|
||||
// 1. retrieve the asset and information
|
||||
getAsset(CLAIM, null, null, name, null)
|
||||
// 2. respond to the request
|
||||
.then(result => {
|
||||
logger.debug('getAsset result', result);
|
||||
if (result === NO_CLAIM) {
|
||||
res.status(200).render('noClaim');
|
||||
} else {
|
||||
return serveOrShowAsset(result, fileExtension, 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);
|
||||
handleRequestError(originalUrl, ip, error, res);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
17
testpage.html
Normal file
17
testpage.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="https://staging.spee.ch/zackmath"/>
|
||||
<img src="https://staging.spee.ch/8/zackmath"/>
|
||||
<img src="https://staging.spee.ch/zackmath.ext"/>
|
||||
<img src="https://staging.spee.ch/8/zackmath.ext"/>
|
||||
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/LBRY-Hype"></video>
|
||||
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/a/LBRY-Hype"></video>
|
||||
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/LBRY-Hype.test"></video>
|
||||
<video width="50%" controls poster="https://spee.ch/assets/img/video_thumb_default.png" src="https://staging.spee.ch/a/LBRY-Hype.test"></video>
|
||||
</body>
|
||||
</html>
|
|
@ -1,10 +1,10 @@
|
|||
<div class="row row--padded">
|
||||
<div class="row">
|
||||
{{#ifConditional this.totalPages '===' 0}}
|
||||
<p>There is no content in {{this.channelName}}:{{this.longChannelId}} yet. Upload some!</p>
|
||||
<p>There is no content in {{this.channelName}}:{{this.longChannelClaimId}} yet. Upload some!</p>
|
||||
{{/ifConditional}}
|
||||
{{#ifConditional this.totalPages '>=' 1}}
|
||||
<p>Below are the contents for {{this.channelName}}:{{this.longChannelId}}</p>
|
||||
<p>Below are the contents for {{this.channelName}}:{{this.longChannelClaimId}}</p>
|
||||
<div class="grid">
|
||||
{{#each this.claims}}
|
||||
{{> gridItem}}
|
||||
|
@ -14,21 +14,21 @@
|
|||
{{#ifConditional this.totalPages '>' 1}}
|
||||
<div class="row">
|
||||
<div class="column column--3 align-content--left">
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p=1">First [1]</a>
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p=1">First [1]</a>
|
||||
</div><div class="column column--4 align-content-center">
|
||||
{{#if this.previousPage}}
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.previousPage}}">Previous</a>
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.previousPage}}">Previous</a>
|
||||
{{else}}
|
||||
<a disabled>Previous</a>
|
||||
{{/if}}
|
||||
|
|
||||
{{#if this.nextPage}}
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.nextPage}}">Next</a>
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.nextPage}}">Next</a>
|
||||
{{else}}
|
||||
<a disabled>Next</a>
|
||||
{{/if}}
|
||||
</div><div class="column column--3 align-content-right">
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelId}}?p={{this.totalPages}}">Last [{{this.totalPages}}]</a>
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.totalPages}}">Last [{{this.totalPages}}]</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/ifConditional}}
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Spee.ch</title>
|
||||
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
|
||||
{{ placeCommonHeaderTags }}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@spee_ch" />
|
||||
<meta property="og:title" content="{{this.channelName}} on Spee.ch">
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Spee.ch</title>
|
||||
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
|
||||
{{ placeCommonHeaderTags }}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@spee_ch" />
|
||||
<meta property="og:title" content="Spee.ch">
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Spee.ch</title>
|
||||
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
|
||||
{{ placeCommonHeaderTags }}
|
||||
<meta property="fb:app_id" content="1371961932852223">
|
||||
{{#unless fileInfo.nsfw}}
|
||||
{{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}}
|
||||
{{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}}
|
||||
{{#unless claimInfo.nsfw}}
|
||||
{{{addTwitterCard claimInfo }}}
|
||||
{{{addOpenGraph claimInfo }}}
|
||||
{{/unless}}
|
||||
<!--google font-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
|
||||
|
@ -21,9 +15,9 @@
|
|||
<body id="show-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
<script src="/assets/js/navBarFunctions.js"></script>
|
||||
<script src="/assets/js/assetConstructor.js"></script>
|
||||
{{> navBar}}
|
||||
{{{ body }}}
|
||||
<script src="/assets/js/showFunctions.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Spee.ch</title>
|
||||
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
|
||||
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
|
||||
{{ placeCommonHeaderTags }}
|
||||
<meta property="fb:app_id" content="1371961932852223">
|
||||
{{#unless fileInfo.nsfw}}
|
||||
{{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}}
|
||||
{{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}}
|
||||
{{#unless claimInfo.nsfw}}
|
||||
{{{addTwitterCard claimInfo }}}
|
||||
{{{addOpenGraph claimInfo }}}
|
||||
{{/unless}}
|
||||
<!-- google analytics -->
|
||||
{{ googleAnalytics }}
|
||||
</head>
|
||||
<body id="showlite-body">
|
||||
<script src="/assets/js/assetConstructor.js"></script>
|
||||
{{{ body }}}
|
||||
<script src="/assets/js/showFunctions.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -1,29 +1,37 @@
|
|||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||
{{#ifConditional fileInfo.fileExt '===' 'gifv'}}
|
||||
<video class="gifv-show" autoplay loop muted>
|
||||
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
<div id="asset-display-component">
|
||||
<div id="asset-status">
|
||||
<div id="searching-message" hidden="true">
|
||||
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
||||
{{> progressBar}}
|
||||
<p>Curious what magic is happening here? <a class="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
|
||||
</div>
|
||||
<div id="failure-message" hidden="true">
|
||||
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY discord</a>.</p>
|
||||
<i><p id="error-message"></p></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="asset-holder" hidden="true">
|
||||
{{#ifConditional claimInfo.contentType '===' 'video/mp4'}}
|
||||
{{> video}}
|
||||
{{else}}
|
||||
{{> image}}
|
||||
{{/ifConditional}}
|
||||
<div>
|
||||
<a id="asset-boilerpate" class="link--primary fine-print" href="/{{claimInfo.claimId}}/{{claimInfo.name}}">hosted via Spee<h</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video id="video-player" class="video-show video" controls poster="{{fileInfo.thumbnail}}">
|
||||
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('DOMContentLoaded', resizeVideoPlayer)
|
||||
window.addEventListener("resize", resizeVideoPlayer);
|
||||
function resizeVideoPlayer() {
|
||||
const div = document.getElementById('video-player');
|
||||
const width = div.offsetWidth;
|
||||
div.height = (width * 9 / 16);
|
||||
}
|
||||
|
||||
const asset = new Asset();
|
||||
asset.data['src'] = '/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}';
|
||||
asset.data['claimName'] = '{{claimInfo.name}}';
|
||||
asset.data['claimId'] = '{{claimInfo.claimId}}';
|
||||
asset.data['fileExt'] = '{{claimInfo.fileExt}}';
|
||||
asset.data['contentType'] = '{{claimInfo.contentType}}';
|
||||
console.log('asset data:', asset.data);
|
||||
asset.checkFileAndRenderAsset();
|
||||
|
||||
</script>
|
||||
{{/ifConditional}}
|
||||
{{else}}
|
||||
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
|
||||
<img class="image-show" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" />
|
||||
</a>
|
||||
{{/ifConditional}}
|
|
@ -1,28 +1,28 @@
|
|||
{{#if fileInfo.channelName}}
|
||||
{{#if claimInfo.channelName}}
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Channel:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
<span class="text"><a href="/{{fileInfo.channelName}}:{{fileInfo.certificateId}}">{{fileInfo.channelName}}</a></span>
|
||||
<span class="text"><a href="/{{claimInfo.channelName}}:{{claimInfo.certificateId}}">{{claimInfo.channelName}}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if fileInfo.description}}
|
||||
{{#if claimInfo.description}}
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<span class="text">{{fileInfo.description}}</span>
|
||||
<span class="text">{{claimInfo.description}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="row row--wide">
|
||||
<div id="show-short-link">
|
||||
<div class="column column--2 column--med-10">
|
||||
<a class="link--primary" href="/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"><span class="text">Link:</span></a>
|
||||
<a class="link--primary" href="/{{shortId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"><span class="text">Link:</span></a>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
<div class="row row--short row--wide">
|
||||
<div class="column column--7">
|
||||
<div class="input-error" id="input-error-copy-short-link" hidden="true"></div>
|
||||
<input type="text" id="short-link" class="input-disabled input-text--full-width" readonly spellcheck="false" value="https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" onclick="select()"/>
|
||||
<input type="text" id="short-link" class="input-disabled input-text--full-width" readonly spellcheck="false" value="https://spee.ch/{{shortId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}" onclick="select()"/>
|
||||
</div><div class="column column--1"></div><div class="column column--2">
|
||||
<button class="button--primary" data-elementtocopy="short-link" onclick="copyToClipboard(event)">copy</button>
|
||||
</div>
|
||||
|
@ -36,10 +36,10 @@
|
|||
<div class="row row--short row--wide">
|
||||
<div class="column column--7">
|
||||
<div class="input-error" id="input-error-copy-embed-text" hidden="true"></div>
|
||||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<video width="100%" controls poster="{{fileInfo.thumbnail}}" src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/></video>'/>
|
||||
{{#ifConditional claimInfo.contentType '===' 'video/mp4'}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<video width="100%" controls poster="{{claimInfo.thumbnail}}" src="https://spee.ch/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/></video>'/>
|
||||
{{else}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<img src="https://spee.ch/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}"/>'/>
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<img src="https://spee.ch/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/>'/>
|
||||
{{/ifConditional}}
|
||||
</div><div class="column column--1"></div><div class="column column--2">
|
||||
<button class="button--primary" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
|
||||
|
@ -55,10 +55,10 @@
|
|||
<span class="text">Share:</span>
|
||||
</div><div class="column column--7 column--med-10">
|
||||
<div class="row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap">
|
||||
<a class="link--primary" target="_blank" href="https://twitter.com/intent/tweet?text=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">twitter</a>
|
||||
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">facebook</a>
|
||||
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}">tumblr</a>
|
||||
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url=https://spee.ch/{{fileInfo.shortId}}/{{fileInfo.name}}&title={{fileInfo.name}}">reddit</a>
|
||||
<a class="link--primary" target="_blank" href="https://twitter.com/intent/tweet?text=https://spee.ch/{{shortId}}/{{claimInfo.name}}">twitter</a>
|
||||
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=https://spee.ch/{{shortId}}/{{claimInfo.name}}">facebook</a>
|
||||
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl=https://spee.ch/{{shortId}}/{{claimInfo.name}}">tumblr</a>
|
||||
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url=https://spee.ch/{{shortId}}/{{claimInfo.name}}&title={{claimInfo.name}}">reddit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,29 +73,29 @@
|
|||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Name:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{fileInfo.name}}
|
||||
{{claimInfo.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Claim Id:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{fileInfo.claimId}}
|
||||
{{claimInfo.claimId}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">File Name:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{fileInfo.fileName}}
|
||||
{{claimInfo.fileName}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">File Type:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{#if fileInfo.fileType}}
|
||||
{{fileInfo.fileType}}
|
||||
{{#if claimInfo.contentType}}
|
||||
{{claimInfo.contentType}}
|
||||
{{else}}
|
||||
unknown
|
||||
{{/if}}
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
{{#ifConditional this.contentType '===' 'video/mp4'}}
|
||||
<img class="grid-item-image" src="{{this.thumbnail}}"/>
|
||||
{{else}}
|
||||
<img class="grid-item-image" src="{{this.directUrlLong}}" />
|
||||
<img class="grid-item-image" src="{{this.claimId}}/{{this.name}}.{{this.fileExt}}" />
|
||||
{{/ifConditional}}
|
||||
|
||||
<div class="hidden" onclick="window.location='{{this.showUrlLong}}'">
|
||||
<div class="hidden" onclick="window.location='{{this.claimId}}/{{this.name}}'">
|
||||
<p class="grid-item-details-text">{{this.name}}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
1
views/partials/image.handlebars
Normal file
1
views/partials/image.handlebars
Normal file
|
@ -0,0 +1 @@
|
|||
<img id="image-asset" class="asset"/>
|
8
views/partials/progressBar.handlebars
Normal file
8
views/partials/progressBar.handlebars
Normal file
|
@ -0,0 +1,8 @@
|
|||
<p id="bar-holder"></p>
|
||||
|
||||
<script src="/assets/js/progressBarConstructor.js"></script>
|
||||
<script type="text/javascript">
|
||||
const progressBar = new ProgressBar();
|
||||
progressBar.createProgressBar(10);
|
||||
progressBar.startProgressBar();
|
||||
</script>
|
10
views/partials/video.handlebars
Normal file
10
views/partials/video.handlebars
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
<video id="video-asset" class="asset" controls poster="{{claimInfo.thumbnail}}">
|
||||
<source>
|
||||
<!--fallback-->
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
|
||||
<script type="text/javascript">
|
||||
showFunctions.addPlayPauseToVideo();
|
||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||
<div class="row row--tall row--padded">
|
||||
<div class="column column--10">
|
||||
<!-- title -->
|
||||
<span class="text--large">{{fileInfo.title}}</span>
|
||||
<span class="text--large">{{claimInfo.title}}</span>
|
||||
</div>
|
||||
<div class="column column--5 column--sml-10 align-content-top">
|
||||
<!-- asset -->
|
||||
|
|
|
@ -1,21 +1,3 @@
|
|||
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}
|
||||
{{#ifConditional fileInfo.fileExt '===' '.gifv'}}
|
||||
<video class="showlite-asset" autoplay loop muted>
|
||||
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
{{else}}
|
||||
<video class="showlite-asset" controls id="video-player">
|
||||
<source src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
|
||||
{{!--fallback--}}
|
||||
Your browser does not support the <code>video</code> element.
|
||||
</video>
|
||||
{{/ifConditional}}
|
||||
<br/>
|
||||
<a class="link--primary fine-print" href="/{{fileInfo.claimId}}/{{fileInfo.name}}">hosted via spee<h</a>
|
||||
{{else}}
|
||||
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}">
|
||||
<img class="showlite-asset" src="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}" alt="{{fileInfo.fileName}}"/>
|
||||
</a>
|
||||
{{/ifConditional}}
|
||||
<div class="row row--tall flex-container--column flex-container--center-center">
|
||||
{{> asset }}
|
||||
</div>
|
Loading…
Reference in a new issue