diff --git a/.gitignore b/.gitignore
index 6dae747..946f815 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,5 @@ Icon
# Files that might appear on external disk
.Spotlight-V100
.Trashes
+node_modules
+coverage
diff --git a/README.md b/README.md
index 664168b..26d52c3 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,14 @@ Youtube Lite allows you to lazyload youtube videos into your NodeBB Forum with n
##Changes in V3
+ 0.6.0
+ - Works with NodeBB 1.0
+ - Moved most of the work to the server, to include API calls (keeps API key
+ from being exposed to users).
+ - LRU cache to reduce quota usage.
+ - Supports `t=`, '`start=`', `end=` query string parameters
+ - With valid API key displays title, channel and duration information
+ - Support `#` in query strings
0.5.0
- Plugin now supports [Youtube API v3](https://developers.google.com/youtube/v3/?hl=en)
diff --git a/library.js b/library.js
index 33f8014..57687f2 100644
--- a/library.js
+++ b/library.js
@@ -1,44 +1,379 @@
"use strict";
+// https://developers.google.com/youtube/v3/docs/videos
+var LRU = require('lru-cache'),
+ cache = LRU( 500 );
+var https = require('https');
var controllers = require('./lib/controllers');
-var YoutubeLite = {},
- embed = '
/g;
+var YoutubeLite = {};
+YoutubeLite.apiKey = null;
+YoutubeLite.cache = cache;
-YoutubeLite.init = function(params, callback) {
- var router = params.router,
- hostMiddleware = params.middleware,
- hostControllers = params.controllers;
-
- // We create two routes for every view. One API call, and the actual route itself.
- // Just add the buildHeader middleware to your route and NodeBB will take care of everything for you.
+YoutubeLite.youtubeUrl = /(|^)((]*?>)(\4\7)<\/a>)( |<\/p>)/m;
- router.get('/admin/plugins/youtube-lite', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
- router.get('/api/admin/plugins/youtube-lite', controllers.renderAdminPage);
+YoutubeLite.init = function(params, callback) {
+ var router = params.router,
+ hostMiddleware = params.middleware,
+ hostControllers = params.controllers,
+ db = module.parent.require('./database');
+ // We create two routes for every view. One API call, and the actual route itself.
+ // Just add the buildHeader middleware to your route and NodeBB will take care of everything for you.
- callback();
+ router.get('/admin/plugins/youtube-lite', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
+ router.get('/api/admin/plugins/youtube-lite', controllers.renderAdminPage);
+
+ db.getObjectField('settings:youtube-lite', 'id', function(err, result){
+ YoutubeLite.apiKey = result;
+ callback();
+ });
};
YoutubeLite.addAdminNavigation = function(header, callback) {
- header.plugins.push({
- route: '/plugins/youtube-lite',
- icon: 'fa-youtube',
- name: 'Youtube Lite'
- });
+ header.plugins.push({
+ route: '/plugins/youtube-lite',
+ icon: 'fa-youtube',
+ name: 'Youtube Lite'
+ });
- callback(null, header);
+ callback(null, header);
};
-YoutubeLite.parse = function(data, callback) {
- if (!data || !data.postData || !data.postData.content) {
- return callback(null, data);
+YoutubeLite.apiRequest = function( videoId, callback ){
+ var req = https.request({
+ host: 'www.googleapis.com',
+ path: '/youtube/v3/videos?id=' + videoId + '&key=' + YoutubeLite.apiKey + '&part=snippet,contentDetails&fields=items(snippet(title,channelTitle,thumbnails),contentDetails(duration))',
+ port: 443,
+ agent: false,
+ json: true,
+ method: 'GET'
+ },(res) => {
+ res.setEncoding('utf8');
+ var videos = '';
+ res.on('data', (data) => {
+ videos += data;
+ });
+ res.on('end', function(){
+ callback(null, videos);
+ });
+ });
+ req.end();
+
+ req.on('error', (err) => {
+ winston.error('[youtube-lite] error looking up video id: [' + videoId + ']' );
+ winston.error( err );
+ callback( err );
+ });
+}
+
+YoutubeLite.fetchSnippet = function( videoId, callback ){
+ var cachedSnippet = cache.get(videoId);
+ if( cachedSnippet ){
+ return callback(null, cachedSnippet);
+ }
+ else{
+ if( YoutubeLite.apiKey ){
+ return YoutubeLite.apiRequest( videoId, function(err, videos){
+ if( err ){
+ return callback(err);
+ }
+ videos = JSON.parse(videos);
+ if( !videos.items || videos.items.length == 0 ){
+ cache.set( videoId, null );
+ return callback(null, null);
+ }
+ var snippet = videos.items[0].snippet;
+ snippet.title = replaceAll( snippet.title, '<', '<');
+ snippet.channelTitle = replaceAll( snippet.channelTitle, '<', '<');
+ snippet.duration = timeToString( parseDuration( videos.items[0].contentDetails.duration ) );
+ cache.set( videoId, snippet );
+ callback( null, snippet );
+ });
+ }
+ else{
+ var snippet = {
+ title: 'Youtube Video',
+ thumbnails: {
+ medium: {url: 'https://i.ytimg.com/vi/' + videoId + '/mqdefault.jpg'},
+ default: {url: 'https://i.ytimg.com/vi/' + videoId + '/default.jpg'},
+ high: {url: 'https://i.ytimg.com/vi/' + videoId + '/hqdefault.jpg'},
+ standard: {url: 'https://i.ytimg.com/vi/' + videoId + '/sddefault.jpg'}
+ }
+ };
+ callback(null, snippet);
+ }
+ }
+}
+
+function replaceAll(text, search, replace) {
+ if (replace === undefined) {
+ return text.toString();
+ }
+ return text.split(search).join(replace);
+}
+
+function spliceSlice(str, index, count, add) {
+return str.slice(0, index) + (add || "") + str.slice(index + count);
+}
+
+/*
+[
+0 'https://youtu.be/ggCuyOeDl5M?t=30s&end=50s
',
+1 '',
+2 'https://youtu.be/ggCuyOeDl5M?t=30s&end=50s ',
+3 '',
+4 'https://youtu.be/',
+5 undefined,
+6 'https://',
+7 'ggCuyOeDl5M?t=30s&end=50s',
+8 'ggCuyOeDl5M',
+9 't=30s&end=50s',
+10 'https://youtu.be/ggCuyOeDl5M?t=30s&end=50s',
+ index: 11,
+ input: 'foo
\nhttps://youtu.be/ggCuyOeDl5M?t=30s&end=50s
\n' ]
+
+0: whole match
+1: opening paragraph tag (optional)
+2: entire ... tag
+3: tag
+4: https//url inside tag
+5: http://url inside tag
+6: https
+7: query string
+8: video id
+9: rest of query string (minus starting character)
+10: text between ...
+11: ending tag
+*/
+
+function getParams( params ){
+ var result = {};
+ var queryString = [];
+ for( var i = 0; i < params.length; ++i ){
+ var param = params[i];
+ if( param.indexOf('t=') == 0 ){
+ var val = parseTime( param.substring( param.indexOf('=')+1) );
+ queryString.push( 'start=' + val );
+ result.start = timeToString( parseInt( val, 10 ) );
+ }
+ else if(
+ param.indexOf('start=') == 0 ||
+ param.indexOf('end=') == 0
+ ){
+ var ex = param.indexOf('=');
+ var val = parseTime( param.substring( ex+1 ) );
+ queryString.push( param.substring(0, ex+1) + val );
+ result[param.substring(0, ex)] = timeToString( parseInt( val, 10 ) );
+ }
+ else if(
+ param.indexOf('iv_load_policy=') == 0
+ ){
+ queryString.push( param );
+ }
+ }
+ result.queryString = queryString.join('&');
+ return result;
+}
+
+
+function filter(data, match, preview, callback){
+ if(match){
+ var videoId = match[8];
+ YoutubeLite.fetchSnippet(videoId,
+ function(err, snippet){
+ if( err ){
+ return callback(err);
+ }
+ if( !snippet ){
+ // not a valid video, skip it
+ return callback(null, data);
+ }
+
+ var params = getParams( (match[9] || '').split('&') );
+ var queryString = params.queryString;
+ var content = match[1] + match[3] + ' ' + snippet.title;
+
+ if( snippet.duration ){
+ content += ' – ';
+
+ if( params.start ){
+ if( params.end ){
+ content += '[' + params.start + '..' + params.end + ']';
+ }
+ else{
+ content += '[' + params.start + '..' + snippet.duration + ']';
+ }
+ }
+ else if( params.end ){
+ content += '[00:00..' + params.end + ']';
+ }
+
+ content += ' ' + snippet.duration + ' — ' + snippet.channelTitle +' ';
+ }
+ else if( params.start || params.end ){
+ // No API key, but we still have a start / end we can display
+ content += ' – ';
+
+ if( params.start ){
+ if( params.end ){
+ content += '[' + params.start + '..' + params.end + ']';
+ }
+ else{
+ content += '[' + params.start + '..]';
+ }
+ }
+ else if( params.end ){
+ content += '[00:00..' + params.end + ']';
+ }
+ content += ' ';
+ }
+ content += '';
+ var thumbnails = snippet.thumbnails;
+ if( preview ){
+ var img = thumbnails.medium || thumbnails.default || thumbnails.high || thumbnails.standard;
+ content += ' ';
+ }
+ else{
+
+ var img = thumbnails.high || thumbnails.standard || thumbnails.default || thumbnails.medium;
+ content += '' +
+ '\n
' +
+ '\n
' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n
' +
+ '\n
';
+ }
+
+ content += match[11];
+ data = data.substring(0, match.index) + content + data.substring( match.index + match[0].length ) ;
+
+ // Check for more...
+ filter(data, data.match(YoutubeLite.youtubeUrl), preview, callback);
+ });
+ }
+ else{
+ callback(null, data);
+ }
+}
+function parseTime(PT) {
+ var output = [];
+ var seconds = 0;
+ var matches = PT.match(/(?:(\d*)H)?(?:(\d*)M)?(?:(\d*)S)?(\d*)?/i);
+ var parts = [
+ { // hours
+ pos: 1,
+ multiplier: 3600
+ },
+ { // minutes
+ pos: 2,
+ multiplier: 60
+ },
+ { // seconds
+ pos: 3,
+ multiplier: 1
+ },
+ { // seconds (raw)
+ pos: 4,
+ multiplier: 1
+ }
+ ];
+
+ for (var i = 0; i < parts.length; i++) {
+ if (typeof matches[parts[i].pos] != 'undefined') {
+ seconds += parseInt(matches[parts[i].pos]) * parts[i].multiplier;
}
- if (data.postData.content.match(regularUrl)) {
- data.postData.content = data.postData.content.replace(regularUrl, embed);
+ }
+
+ return seconds;
+};
+
+YoutubeLite.parseRaw = function(data, callback){
+ if (!data ) {
+ return callback(null, data);
+ }
+ filter(data, data.match(YoutubeLite.youtubeUrl), true, callback);
+};
+
+YoutubeLite.parsePost = function(data, callback) {
+ if (!data || !data.postData || !data.postData.content) {
+ return callback(null, data);
+ }
+ var content = data.postData.content;
+ filter(content, content.match(YoutubeLite.youtubeUrl), false, function(err, content){
+ if(err){
+ winston.error('[youtube-lite] error parsing pid ' + data.postData.pid );
+ return callback(null, data);
}
+ data.postData.content = content;
callback(null, data);
+ });
+};
+
+function parseDuration(PT, settings) {
+ var durationInSec = 0;
+ var matches = PT.match(/P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)W)?(?:(\d*)D)?T(?:(\d*)H)?(?:(\d*)M)?(?:(\d*)S)?/i);
+ var parts = [
+ { // years
+ pos: 1,
+ multiplier: 86400 * 365
+ },
+ { // months
+ pos: 2,
+ multiplier: 86400 * 30
+ },
+ { // weeks
+ pos: 3,
+ multiplier: 604800
+ },
+ { // days
+ pos: 4,
+ multiplier: 86400
+ },
+ { // hours
+ pos: 5,
+ multiplier: 3600
+ },
+ { // minutes
+ pos: 6,
+ multiplier: 60
+ },
+ { // seconds
+ pos: 7,
+ multiplier: 1
+ }
+ ];
+
+ for (var i = 0; i < parts.length; i++) {
+ if (typeof matches[parts[i].pos] != 'undefined') {
+ durationInSec += parseInt(matches[parts[i].pos]) * parts[i].multiplier;
+ }
+ }
+ return durationInSec;
+
+};
- };
+function timeToString(seconds){
+ var output = [];
+ // Hours extraction
+ if (seconds > 3599) {
+ output.push(parseInt(seconds / 3600));
+ seconds %= 3600;
+ }
+ // Minutes extraction with leading zero
+ output.push(('0' + parseInt(seconds / 60)).slice(-2));
+ // Seconds extraction with leading zero
+ output.push(('0' + seconds % 60).slice(-2));
+
+ return output.join(':');
+}
module.exports = YoutubeLite;
diff --git a/package.json b/package.json
index 2e2e027..76b46db 100644
--- a/package.json
+++ b/package.json
@@ -2,17 +2,27 @@
"name": "nodebb-plugin-youtube-lite",
"version": "0.5.0",
"nbbpm": {
- "compatibility": "^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0"
+ "compatibility": "^1.0.3 "
},
"description": "NodeBB Youtube Lite Plugin",
"main": "library.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./tests -t 100000"
},
"repository": {
"type": "git",
"url": "git+https://github.com/a5mith/nodebb-plugin-youtube-lite.git"
},
+ "dependencies":{
+ "lru-cache": "^4.0.1"
+ },
+ "devDependencies": {
+ "chai": "^3.5.0",
+ "istanbul": "^0.4.2",
+ "mocha": "^2.4.5",
+ "winston": "^2.2.0",
+ "sinon": "^1.17"
+ },
"keywords": [
"nodebb",
"plugin",
diff --git a/plugin.json b/plugin.json
index 94ecbdf..33ef55d 100644
--- a/plugin.json
+++ b/plugin.json
@@ -5,14 +5,14 @@
"url": "https://github.com/a5mith/nodebb-plugin-youtube-lite",
"library": "./library.js",
"hooks": [
- { "hook": "filter:parse.post", "method": "parse" },
- { "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
- { "hook": "static:app.load", "method": "init" }
+ { "hook": "filter:parse.raw", "method": "parseRaw", "priority": 10 },
+ { "hook": "filter:parse.post", "method": "parsePost", "priority": 10 },
+ { "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
+ { "hook": "static:app.load", "method": "init" }
],
"templates": "templates",
"scripts": [
- "static/lib/lazyYT.js",
- "static/lib/main.js"
+ "static/lib/lazyYT.js"
],
"staticDirs": {
"static": "./static"
diff --git a/static/lib/lazyYT.js b/static/lib/lazyYT.js
index f9b6cce..173f8f0 100644
--- a/static/lib/lazyYT.js
+++ b/static/lib/lazyYT.js
@@ -1,220 +1,13 @@
;(function ($) {
'use strict';
-
- function setUp($el, settings) {
- var width = $el.data('width'),
- height = $el.data('height'),
- title = $el.attr('title') || $el.data('title'),
- display_title = $el.data('display-title'),
- ratio = ($el.data('ratio')) ? $el.data('ratio') : settings.default_ratio,
- display_duration = $el.data('display-duration'),
+ $.fn.lazyYT = function (e) {
+
+ var $el = $(e.parentElement.parentElement),
id = $el.data('youtube-id'),
- padding_bottom,
- innerHtml = [],
- $thumb,
- thumb_img,
- loading_text = $el.text() ? $el.text() : settings.loading_text,
youtube_parameters = $el.data('parameters') || '';
-
- ratio = ratio.split(":");
-
- youtube_parameters += '&' + settings.youtube_parameters;
-
- if (typeof display_title != "boolean") {
- display_title = settings.display_title;
- }
-
- if (typeof display_duration != "boolean") {
- display_duration = settings.display_duration;
- }
-
- // width and height might override default_ratio value
- if (typeof width === 'number' && typeof height === 'number') {
- $el.width(width);
- padding_bottom = height + 'px';
- } else if (typeof width === 'number') {
- $el.width(width);
- padding_bottom = (width * ratio[1] / ratio[0]) + 'px';
- } else {
- width = $el.width();
-
- // no width means that container is fluid and will be the size of its parent
- if (width == 0) {
- width = $el.parent().width();
- }
-
- padding_bottom = (ratio[1] / ratio[0] * 100) + '%';
- }
-
- //
- // This HTML will be placed inside 'lazyYT' container
-
- innerHtml.push('');
-
- // Play button from YouTube (exactly as it is in YouTube)
- innerHtml.push('
');
- innerHtml.push('');
- innerHtml.push(' ');
- innerHtml.push(' ');
- innerHtml.push(' ');
- innerHtml.push(' ');
- innerHtml.push(' '); // end of .ytp-large-play-button
-
- // video time from YouTube (exactly as it is in YouTube)
- if (display_duration) {
- innerHtml.push('
');
- }
- innerHtml.push('
'); // end of .ytp-thumbnail
-
- // Video title (info bar)
- if (display_title) {
- innerHtml.push('
');
- innerHtml.push('');
- innerHtml.push('
');
- innerHtml.push('
'); // /.ytp-title-text
- innerHtml.push('
'); // /.ytp-title
- innerHtml.push('
'); // /.ytp-chrome-top
- }
-
- $el.css({
- 'padding-bottom': padding_bottom
- })
- .html(innerHtml.join(''));
-
- if (width > 320) {
- thumb_img = 'hqdefault.jpg';
- } else if (width > 120) {
- thumb_img = 'mqdefault.jpg';
- } else if (width == 0) { // sometimes it fails on fluid layout
- thumb_img = 'hqdefault.jpg';
- } else {
- thumb_img = 'default.jpg';
- }
-
- $thumb = $el.find('.ytp-thumbnail').css({
- 'background-image': ['url(http://img.youtube.com/vi/', id, '/', thumb_img, ')'].join('')
- })
- .addClass('lazyYT-image-loaded')
- .on('click', function (e) {
- e.preventDefault();
- if (!$el.hasClass('lazyYT-video-loaded') && $thumb.hasClass('lazyYT-image-loaded')) {
- $el.html('')
- .addClass(settings.video_loaded_class);
-
- // execute callback
- if (typeof settings.callback == 'function') { // make sure the callback is a function
- settings.callback.call($el); // brings the scope to the callback
- }
- }
- });
-
- if ((!title && display_title) || display_duration) {
- var youtube_data_url = ['https://www.googleapis.com/youtube/v3/videos?id=', id, '&key=', settings.yt_api_key, '&part=snippet'];
- if (display_duration) youtube_data_url.push(',contentDetails'); // this extra info now costs some quota points, so we retrieve it only when necessary. More on quota: https://developers.google.com/youtube/v3/getting-started#quota
-
- $.getJSON(youtube_data_url.join(''), function (data) {
- var item = data.items[0];
- // console.log(item.snippet.title);
-
- $el.find('#lazyYT-title-' + id).text(item.snippet.title);
-
- if (display_duration) {
- $el.find('.video-time')
- .text(parseDuration(item.contentDetails.duration, settings))
- .show();
- }
-
- });
- }
-
- };
-
- function parseDuration(PT, settings) {
- var output = [];
- var durationInSec = 0;
- var matches = PT.match(/P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)W)?(?:(\d*)D)?T(?:(\d*)H)?(?:(\d*)M)?(?:(\d*)S)?/i);
- var parts = [
- { // years
- pos: 1,
- multiplier: 86400 * 365
- },
- { // months
- pos: 2,
- multiplier: 86400 * 30
- },
- { // weeks
- pos: 3,
- multiplier: 604800
- },
- { // days
- pos: 4,
- multiplier: 86400
- },
- { // hours
- pos: 5,
- multiplier: 3600
- },
- { // minutes
- pos: 6,
- multiplier: 60
- },
- { // seconds
- pos: 7,
- multiplier: 1
- }
- ];
-
- for (var i = 0; i < parts.length; i++) {
- if (typeof matches[parts[i].pos] != 'undefined') {
- durationInSec += parseInt(matches[parts[i].pos]) * parts[i].multiplier;
- }
- }
-
- // Hours extraction
- if (durationInSec > 3599) {
- output.push(parseInt(durationInSec / 3600));
- durationInSec %= 3600;
- }
- // Minutes extraction with leading zero
- output.push(('0' + parseInt(durationInSec / 60)).slice(-2));
- // Seconds extraction with leading zero
- output.push(('0' + durationInSec % 60).slice(-2));
-
- return output.join(':');
- };
-
- $.fn.lazyYT = function (yt_api_key, newSettings) {
- var defaultSettings = {
- yt_api_key: yt_api_key,
-
- youtube_parameters: 'rel=0',
- loading_text: 'Loading...',
- display_title: true,
- default_ratio: '16:9',
- display_duration: false,
- callback: null,
-
- // Advanced settings
- video_loaded_class: 'lazyYT-video-loaded',
- container_class: 'lazyYT-container'
- };
- var settings = $.extend(defaultSettings, newSettings);
-
- return this.each(function () {
- var $el = $(this).addClass(settings.container_class);
- setUp($el, settings);
- });
- };
-
+ $el.html(
+ 'VIDEO ')
+ .addClass('lazyYT-video-loaded');
+ };
}(jQuery));
+
diff --git a/static/lib/main.js b/static/lib/main.js
deleted file mode 100644
index f9a97bf..0000000
--- a/static/lib/main.js
+++ /dev/null
@@ -1,10 +0,0 @@
-"use strict";
-
- $(window).on('action:widgets.loaded', function() {
- $('.js-lazyYT').lazyYT();
- });
-
-
- $(window).on('action:posts.loaded', function(){
- $('.js-lazyYT').delay(500).lazyYT();
- });
diff --git a/static/style.less b/static/style.less
index 1b04d59..fc34cf0 100644
--- a/static/style.less
+++ b/static/style.less
@@ -17,6 +17,7 @@ iframe.lazytube {
padding: 0 0 56.25% 0;
overflow: hidden;
background-color: @body-bg;
+ margin-bottom: 10px;
}
.lazyYT-container iframe {
diff --git a/tests/googleapi.js b/tests/googleapi.js
new file mode 100644
index 0000000..fb4c178
--- /dev/null
+++ b/tests/googleapi.js
@@ -0,0 +1,64 @@
+'use strict';
+var winston = require('winston');
+var sinon = require('sinon');
+var expect = require("chai").expect;
+var youtubeLite = require("../library");
+
+process.on('uncaughtException', function (err) {
+ winston.error('Encountered error while running test suite: ' + err.message);
+});
+
+
+describe('Youtube API call',function(){
+ var sandbox = null;
+ beforeEach(function(){
+ sandbox = sinon.sandbox.create();
+ youtubeLite.apiKey = 'fakekey';
+ });
+ afterEach(function(){
+ sandbox.restore();
+ });
+
+ it("doesn't crash on a bad video id",function(){
+ sandbox.stub( youtubeLite, 'apiRequest').yields( null, "{}" );
+ youtubeLite.fetchSnippet( "badVideo", function( err, snippet ){
+ expect( snippet ).to.be.null;
+ });
+ });
+
+ it("returns {title, channelTitle, thumbnails, duration} on a good video id",function(){
+ sandbox.stub( youtubeLite, 'apiRequest').yields( null, JSON.stringify(
+ {
+ items: [
+ {
+ snippet: {
+ title: 'Video title!',
+ channelTitle: 'Channel Title!',
+ thumbnails:{
+ default:{ url:'https://i.ytimg.com/vi/goodvideo/default.jpg'},
+ high:{ url:'https://i.ytimg.com/vi/goodvideo/hqdefault.jpg'},
+ medium:{ url:'https://i.ytimg.com/vi/goodvideo/mqdefault.jpg'},
+ standard:{ url:'https://i.ytimg.com/vi/goodvideo/sddefault.jpg'},
+ }
+ },
+ contentDetails: {duration: 'PT3H2M31S'}
+ }
+ ]
+ }) );
+ youtubeLite.fetchSnippet( "goodvideo", function( err, snippet ){
+ expect( snippet ).to.deep.equal(
+ {
+ title: 'Video title!',
+ channelTitle: 'Channel Title!',
+ duration: '3:02:31',
+ thumbnails:{
+ default:{ url:'https://i.ytimg.com/vi/goodvideo/default.jpg'},
+ high:{ url:'https://i.ytimg.com/vi/goodvideo/hqdefault.jpg'},
+ medium:{ url:'https://i.ytimg.com/vi/goodvideo/mqdefault.jpg'},
+ standard:{ url:'https://i.ytimg.com/vi/goodvideo/sddefault.jpg'},
+ }
+ } );
+
+ });
+ });
+});
diff --git a/tests/regex.js b/tests/regex.js
new file mode 100644
index 0000000..0be8689
--- /dev/null
+++ b/tests/regex.js
@@ -0,0 +1,175 @@
+'use strict';
+var winston = require('winston');
+
+process.on('uncaughtException', function (err) {
+ winston.error('Encountered error while running test suite: ' + err.message);
+});
+
+function replaceAll(text, searchReplace) {
+ if (searchReplace === undefined) {
+ return text.toString();
+ }
+ if( !Array.isArray(searchReplace) ){
+ searchReplace = [searchReplace];
+ }
+ for( var i = 0; i < searchReplace.length; ++i ){
+ var sr = searchReplace[i];
+ text = text.split( sr.search ).join( sr.replace );
+ }
+ return text;
+}
+
+var expect = require("chai").expect;
+
+var youtubeLite = require("../library");
+var rawTitlePrefix = ' Youtube Video';
+var rawTimes = ' – {time} ';
+var rawTitleSuffix = ' '
+var rawSimpleTemplate = rawTitlePrefix + rawTitleSuffix;
+var rawTimesTemplate = rawTitlePrefix + rawTimes + rawTitleSuffix;
+var postQueryStringPrefixTemplate = ' Youtube Video – {time} ';
+var postSimplePrefixTemplate = ' Youtube Video ';
+var postSuffix =
+'' +
+'\n
' +
+'\n
' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n ' +
+'\n
' +
+'\n
';
+var postSimpleTemplate = postSimplePrefixTemplate + postSuffix;
+var postQueryStringTemplate = postQueryStringPrefixTemplate + postSuffix;
+
+var videoIdSearchReplace = {search:'{videoId}', replace:'fXhUgV9qzI0'};
+var blankQueryStringSearchReplace = {search:'{queryString}', replace:''};
+var blankDataParameterstringSearchReplace = {search:'{dataParameters}', replace:''};
+
+var posts = [
+ {
+ description: 'Simple post with only the youtube link',
+ content: 'https://youtu.be/fXhUgV9qzI0
',
+ expected:{
+ raw: '' + replaceAll( rawSimpleTemplate, [videoIdSearchReplace, blankQueryStringSearchReplace]) + '
',
+ post: '' + replaceAll( postSimpleTemplate, [videoIdSearchReplace, blankQueryStringSearchReplace, blankDataParameterstringSearchReplace]) + '
'
+ }
+ },
+ {
+ description: 'Markdown link with a youtube URL should be left alone',
+ content: 'linked
',
+ expected: {
+ raw: 'linked
',
+ post: 'linked
'
+ }
+ },
+ {
+ description: 'Video URLs on consecutive lines',
+ videoId: 'fXhUgV9qzI0',
+ content: 'https://youtu.be/fXhUgV9qzI0 \nhttps://youtu.be/fXhUgV9qzI0
',
+ expected: {
+ raw: '' + replaceAll( rawSimpleTemplate, [videoIdSearchReplace, blankQueryStringSearchReplace]) + ' \n' +
+ replaceAll( rawSimpleTemplate, [videoIdSearchReplace, blankQueryStringSearchReplace]) + '
',
+ post: '' + replaceAll( postSimpleTemplate, [videoIdSearchReplace, blankQueryStringSearchReplace, blankDataParameterstringSearchReplace]) + ' \n' +
+ replaceAll( postSimpleTemplate, [videoIdSearchReplace, blankQueryStringSearchReplace, blankDataParameterstringSearchReplace]) + '
'
+ }
+ },
+ {
+ description: 'Link with text in front should be left alone',
+ content: 'Look at thishttps://youtu.be/fXhUgV9qzI0
',
+ expected: {
+ raw: 'Look at thishttps://youtu.be/fXhUgV9qzI0
',
+ post: 'Look at thishttps://youtu.be/fXhUgV9qzI0
'
+ }
+ },
+ {
+ description: 'Link with text behind should be left alone',
+ content: 'https://youtu.be/fXhUgV9qzI0 uh huh
',
+ expected: {
+ raw: 'https://youtu.be/fXhUgV9qzI0 uh huh
',
+ post: 'https://youtu.be/fXhUgV9qzI0 uh huh
'
+ }
+ },
+ {
+ description: 'Link with text in front and behind should be left alone',
+ content: 'Look: https://youtu.be/fXhUgV9qzI0 uh huh
',
+ expected: {
+ raw: 'Look: https://youtu.be/fXhUgV9qzI0 uh huh
',
+ post: 'Look: https://youtu.be/fXhUgV9qzI0 uh huh
'
+ }
+ },
+ {
+ description: 'Simple post with only the youtu.be link with a start time',
+ videoId: 'fXhUgV9qzI0',
+ content: 'https://youtu.be/fXhUgV9qzI0?t=55s
',
+ expected: {
+ raw: '' + replaceAll( rawTimesTemplate, [videoIdSearchReplace,{search:'{time}',replace:'[00:55..]'}, {search:'{queryString}', replace:'?t=55s'}]) + '
',
+ post: '' + replaceAll( postQueryStringTemplate, [
+ videoIdSearchReplace,
+ {search:'{queryString}', replace:'t=55s'},
+ {search:'{time}',replace:'[00:55..]'},
+ {search:'{dataParameters}', replace:'start=55'}
+ ]) + '
'
+ }
+ },
+ {
+ description: 'Simple post with only the youtube.com link with a start time',
+ videoId: 'fXhUgV9qzI0',
+ content: 'https://youtube.com/watch?v=fXhUgV9qzI0&t=55s
',
+ expected: {
+ raw: '' + replaceAll( rawTimesTemplate, [
+ {search:'youtu.be/',replace:'youtube.com/watch?v='},
+ videoIdSearchReplace,
+ {search:'{time}',replace:'[00:55..]'},
+ {search:'{queryString}', replace:'&t=55s'}]) + '
',
+ post: '' + replaceAll( postQueryStringTemplate, [
+ videoIdSearchReplace,
+ {search:'youtu.be/',replace:'youtube.com/watch?v='},
+ {search:'?{queryString}',replace:'&{queryString}'},
+ {search:'{queryString}', replace:'t=55s'},
+ {search:'{time}',replace:'[00:55..]'},
+ {search:'{dataParameters}', replace:'start=55'}
+ ]) + '
'
+ }
+ }
+];
+
+
+describe( 'Parsing a ', function(){
+ var i;
+ for( i = 0; i < posts.length; ++i ){
+ var post = posts[i];
+ describe( post.description + ' in preview', function(){
+
+ var data = new Object();
+ var expectedValue = post.expected.raw;
+ data = post.content;
+ it('converts the post correctly', function(){
+ youtubeLite.parseRaw( data, function( callback, theData ){
+ expect( theData ).to.equal( expectedValue );
+ });
+
+ });
+ });
+ describe( post.description + ' in the final post', function(){
+
+ var data = new Object();
+ var expectedValue = post.expected.post;
+ data.postData = new Object();
+ data.postData.content = post.content;
+ it('converts the post correctly', function(){
+ youtubeLite.parsePost( data, function( callback, theData ){
+ expect( theData.postData.content ).to.equal( expectedValue );
+ });
+
+ });
+ });
+ }
+});
+
+