1/* 2 * YouTube TV 3 * 4 * Copyright 2013, Jacob Kelley - http://jakiestfu.com/ 5 * Released under the MIT Licence 6 * http://opensource.org/licenses/MIT 7 * 8 * Github: 9 * Version: 3.0.5 10 */ 11/*jslint browser: true, undef:true, unused:true, laxbreak:true, loopfunc:true*/ 12/*global define, module, ender */ 13 14(function(win, doc) { 15 'use strict'; 16 var apiKey = 'AIzaSyAv791fSByhxiHw9DEl1FCSgghnEPFABIA'; 17 var YTV = YTV || function(id, opts){ 18 19 var noop = function(){}, 20 settings = { 21 apiKey: apiKey, 22 element: null, 23 user: null, 24 channelId: null, 25 fullscreen: false, 26 accent: '#fff', 27 controls: true, 28 annotations: false, 29 autoplay: false, 30 chainVideos: true, 31 browsePlaylists: false, 32 playerTheme: 'dark', 33 listTheme: 'dark', 34 responsive: false, 35 playId:'', 36 sortList: false, 37 reverseList: false, 38 shuffleList: false, 39 wmode: 'opaque', 40 events: { 41 videoReady: noop, 42 stateChange: noop 43 } 44 }, 45 46 cache = { 47 data: {}, 48 remove: function (url) { 49 delete cache.data[url]; 50 }, 51 exist: function (url) { 52 return cache.data.hasOwnProperty(url) && cache.data[url] !== null; 53 }, 54 get: function (url) { 55 return cache.data[url]; 56 }, 57 set: function (url, data) { 58 cache.remove(url); 59 cache.data[url] = data; 60 } 61 }, 62 utils = { 63 events: { 64 addEvent: function addEvent(element, eventName, func) { 65 if (element.addEventListener) { 66 return element.addEventListener(eventName, func, false); 67 } else if (element.attachEvent) { 68 return element.attachEvent("on" + eventName, func); 69 } 70 }, 71 removeEvent: function addEvent(element, eventName, func) { 72 if (element.addEventListener) { 73 return element.removeEventListener(eventName, func, false); 74 } else if (element.attachEvent) { 75 return element.detachEvent("on" + eventName, func); 76 } 77 }, 78 prevent: function(e) { 79 if (e.preventDefault) { 80 e.preventDefault(); 81 } else { 82 e.returnValue = false; 83 } 84 } 85 }, 86 addCSS: function(css){ 87 var head = doc.getElementsByTagName('head')[0], 88 style = doc.createElement('style'); 89 style.type = 'text/css'; 90 if (style.styleSheet){ 91 style.styleSheet.cssText = css; 92 } else { 93 style.appendChild(doc.createTextNode(css)); 94 } 95 head.appendChild(style); 96 }, 97 addCommas: function(str){ 98 var x = str.split('.'), 99 x1 = x[0], 100 x2 = x.length > 1 ? '.' + x[1] : '', 101 rgx = /(\d+)(\d{3})/; 102 while (rgx.test(x1)) { 103 x1 = x1.replace(rgx, '$1' + ',' + '$2'); 104 } 105 return x1 + x2; 106 }, 107 parentUntil: function(el, attr) { 108 while (el.parentNode) { 109 if (el.getAttribute && el.getAttribute(attr)){ 110 return el; 111 } 112 el = el.parentNode; 113 } 114 return null; 115 }, 116 ajax: { 117 get: function(url, fn){ 118 if (cache.exist(url)) { 119 fn.call(this, JSON.parse(cache.get(url))); 120 } else { 121 var handle; 122 if (win.XDomainRequest && !(navigator.appVersion.indexOf("MSIE 8")==-1 || navigator.appVersion.indexOf("MSIE 9")==-1)) { // CORS for IE8,9 123 handle = new XDomainRequest(); 124 handle.onload = function(){ 125 cache.set(url, handle.responseText); 126 fn.call(this, JSON.parse(handle.responseText)); 127 if (Object.prototype.hasOwnProperty.call(JSON.parse(handle.responseText), 'error')){ 128 cache.remove(url); 129 var e = JSON.parse(handle.responseText); 130 console.log('Youtube-TV Error: Youtube API Response: '+e.error.errors[0].reason+'\n'+ 'Details: '+e.error.errors[0].message); 131 } 132 }; 133 } else if (win.XMLHttpRequest){ // Modern Browsers 134 handle = new XMLHttpRequest(); 135 } 136 handle.onreadystatechange = function(){ 137 if (handle.readyState === 4 && handle.status === 200){ 138 cache.set(url, handle.responseText); 139 fn.call(this, JSON.parse(handle.responseText)); 140 } else if (handle.readyState === 4){ 141 var e = JSON.parse(handle.responseText); 142 console.log('Youtube-TV Error: Youtube API Response: '+e.error.errors[0].reason+'\n'+ 'Details: '+e.error.errors[0].message); 143 } 144 }; 145 handle.open("GET",url,true); 146 handle.send(); 147 } 148 } 149 }, 150 endpoints: { 151 base: 'https://www.googleapis.com/youtube/v3/', 152 userInfo: function(){ 153 return utils.endpoints.base+'channels?'+settings.cid+'&key='+apiKey+'&part=snippet,contentDetails,statistics'; 154 }, 155 playlistInfo: function(pid){ 156 return utils.endpoints.base+'playlists?id='+pid+'&key='+apiKey+'&maxResults=50&part=snippet'; 157 }, 158 userPlaylists: function(){ 159 return utils.endpoints.base+'playlists?channelId='+settings.channelId+'&key='+apiKey+'&maxResults=50&part=snippet'; 160 }, 161 playlistVids: function(){ 162 return utils.endpoints.base+'playlistItems?playlistId='+settings.pid+'&key='+apiKey+'&maxResults=50&part=contentDetails'; 163 }, 164 videoInfo: function(){
165 return utils.endpoints.base+'videos?id='+settings.videoString+'&key='+apiKey+'&maxResults=50&part=snippet,contentDetails,status,statistics'; 166 } 167 }, 168 deepExtend: function(destination, source) { 169 var property; 170 for (property in source) { 171 if (source[property] && source[property].constructor && source[property].constructor === Object) { 172 destination[property] = destination[property] || {}; 173 utils.deepExtend(destination[property], source[property]); 174 } else { 175 destination[property] = source[property]; 176 } 177 } 178 return destination; 179 } 180 }, 181 prepare = { 182 youtube: function(){ 183 if(typeof YT=='undefined'){ 184 var tag = doc.createElement('script'); 185 tag.src = "https://www.youtube.com/iframe_api"; 186 var firstScriptTag = doc.getElementsByTagName('script')[0]; 187 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 188 } 189 }, 190 build: function(){ 191 if (settings.channelId){ 192 settings.cid = 'id='+settings.channelId; 193 } else if(settings.user){ 194 settings.cid = 'forUsername='+settings.user; 195 } 196 settings.element.className = "ytv-canvas"; 197 if(settings.fullscreen){ 198 settings.element.className += " ytv-full"; 199 } 200 utils.addCSS( '#'+id+' .ytv-list .ytv-active a{border-left-color: '+(settings.accent)+';}' ); 201 // Responsive CSS 202 if(settings.responsive){ 203 utils.addCSS('#'+id+' .ytv-video{' 204 +'position: relative; padding-bottom: 39.4%; /* 16:9 of 70%*/' 205 +'height: 0; width: 70%;' 206 +'} #'+id+' .ytv-video iframe{' 207 +'position: absolute; top: 0; left: 0;' 208 +'} #'+id+' .ytv-list{' 209 +'width: 30%;' 210 +'} #'+id+' .ytv-playlist-open .ytv-arrow{' 211 +'top: 0px;}' 212 +'@media only screen and (max-width:992px) {' 213 +'#'+id+' .ytv-list{' 214 +'position: relative; display: block;' 215 +'width: 0; padding-bottom: 40%;' 216 +'left: auto; right: auto;' 217 +'top: auto; width: 100%;' 218 +'} #'+id+' .ytv-video{' 219 +'position: relative; padding-bottom: 56.25%; /* 16:9 */' 220 +'height: 0; position: relative;' 221 +'display: block; left: auto;' 222 +'right: auto; top: auto; width: 100%;' 223 +'}}' 224 ); 225 } 226 // Temp Scroll Bar fix 227 if (settings.listTheme == 'dark'){ 228 utils.addCSS( ' #'+id+'.ytv-canvas ::-webkit-scrollbar{border-left: 1px solid #000;}' 229 + ' #'+id+'.ytv-canvas ::-webkit-scrollbar-thumb{background: rgba(255,255,255,0.2);}'); 230 } 231 // Optional Light List Theme 232 if(settings.listTheme == 'light'){ 233 utils.addCSS( ' #'+id+'.ytv-canvas{background: #ccc;}' 234 + ' #'+id+'.ytv-canvas ::-webkit-scrollbar{border-left: 1px solid rgba(28,28,28,0.1);}' 235 + ' #'+id+'.ytv-canvas ::-webkit-scrollbar-thumb{background: rgba(28,28,28,0.3);}' 236 + ' #'+id+' .ytv-list .ytv-active a{background: rgba(0,0,0,0.2);}' 237 + ' #'+id+' .ytv-list a{color: #282828; border-top: 1px solid rgba(0,0,0,0.1); border-bottom: 1px solid rgba(204,204,204,0.5);}' 238 + ' #'+id+' .ytv-list a:hover, #'+id+' .ytv-list-header .ytv-playlists a:hover{ background: rgba(0,0,0,0.2);}' 239 + ' #'+id+' .ytv-list a:active, #'+id+' .ytv-list-header .ytv-playlists a:active{ background: rgba(0,0,0,0.2);}' 240 + ' #'+id+' .ytv-list .ytv-thumb-stroke{outline: 1px solid rgba(0,0,0,0.1);}' 241 + ' #'+id+' .ytv-list .ytv-thumb{outline: 1px solid rgba(255,255,255,0.5);}' 242 + ' #'+id+' .ytv-list-header{-webkit-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.2); -moz-box-shadow: 0 1px 2px rgba(255, 255, 255, 0.2); box-shadow: 0 1px 2px rgba(255, 255, 255, 0.2);}' 243 + ' #'+id+' .ytv-list-header a{background: rgba(0,0,0,0.2);}' 244 + ' #'+id+' .ytv-playlists{background: #ccc;}' 245 ); 246 } 247 }, 248 userUploads: function(userInfo){ 249 if (userInfo && userInfo.items.length > 0){ 250 settings.pid = userInfo.items[0].contentDetails.relatedPlaylists.uploads; 251 utils.ajax.get( utils.endpoints.playlistVids(), prepare.compileVideos ); 252 } else console.log ('Youtube-TV Error: API returned no matches for: '+(settings.channelId ? settings.channelId : settings.user)+'\nPlease ensure it was entered correctly and in the appropriate field shown below. \nuser: \'username\' or channelId: \'UCxxxx...\''); 253 }, 254 selectedPlaylist: function(playlistInfo){ 255 if (playlistInfo && playlistInfo.items.length > 0) {
256 if (!settings.channelId && !settings.user){ 257 settings.cid = ('id='+(settings.channelId = playlistInfo.items[0].snippet.channelId)); 258 } 259 settings.currentPlaylist = playlistInfo.items[0].snippet.title; 260 settings.pid = playlistInfo.items[0].id; 261 utils.ajax.get( utils.endpoints.playlistVids(), prepare.compileVideos ); 262 } else console.log ('Youtube-TV Error: API returned no matches for playlist(s): '+settings.playlist); 263 }, 264 compileVideos: function(res){ 265 if (res && res.items.length > 0){ 266 var playlists = res.items, 267 i; 268 settings.videoString = ''; 269 for(i=0; i<playlists.length; i++){ 270 settings.videoString += playlists[i].contentDetails.videoId; 271 if (i<playlists.length-1){ settings.videoString += ',';} 272 } 273 utils.ajax.get( utils.endpoints.videoInfo(), prepare.compileList ); 274 } else console.log ('Youtube-TV Error: Empty playlist'); 275 }, 276 playlists: function(res){ 277 if(res && res.items.length > 0){ 278 var list = '<div class="ytv-playlists"><ul>', 279 playlists = res.items, 280 i; 281 for(i=0; i<playlists.length; i++){ 282 var data = { 283 title: playlists[i].snippet.title, 284 plid: playlists[i].id, 285 thumb: playlists[i].snippet.thumbnails.medium.url 286 }; 287 list += '<a href="#" data-ytv-playlist="'+(data.plid)+'">'; 288 list += '<div class="ytv-thumb"><div class="ytv-thumb-stroke"></div><img src="'+(data.thumb)+'"></div>'; 289 list += '<span>'+(data.title)+'</span>'; 290 list += '</a>'; 291 } 292 list += '</ul></div>'; 293 294 var lh = settings.element.getElementsByClassName('ytv-list-header')[0], 295 headerLink = lh.children[0]; 296 headerLink.href="#"; 297 headerLink.target=""; 298 headerLink.setAttribute('data-ytv-playlist-toggle', 'true'); 299 settings.element.getElementsByClassName('ytv-list-header')[0].innerHTML += list; 300 lh.className += ' ytv-has-playlists'; 301 } else console.log ('Youtube-TV Error: Returned no playlists'); 302 }, 303 compileList: function(data){ 304 if(data && data.items.length > 0){ 305 utils.ajax.get( utils.endpoints.userInfo(), function(userInfo){ 306 var list = '', 307 user = { 308 title: userInfo.items[0].snippet.title, 309 url: '//youtube.com/channel/'+userInfo.items[0].id, 310 thumb: userInfo.items[0].snippet.thumbnails['default'].url, 311 summary: userInfo.items[0].snippet.description, 312 subscribers: userInfo.items[0].statistics.subscriberCount, 313 views: userInfo.items[0].statistics.viewCount 314 }, 315 videos = data.items, 316 first = true, 317 i; 318 settings.channelId = userInfo.items[0].id; 319 if(settings.currentPlaylist) user.title += ' · '+(settings.currentPlaylist); 320 if (settings.sortList) videos.sort(function(a,b){if(a.snippet.publishedAt > b.snippet.publishedAt) return -1;if(a.snippet.publishedAt < b.snippet.publishedAt) return 1;return 0;}); 321 if (settings.reverseList) videos.reverse(); 322 if (settings.shuffleList) { 323 videos = function (){for(var j, x, i = videos.length; i; j = Math.floor(Math.random() * i), x = videos[--i], videos[i] = videos[j], videos[j] = x);return videos;}(); 324 } 325 326 list += '<div class="ytv-list-header">'; 327 list += '<a href="'+(user.url)+'" target="_blank">'; 328 list += '<img src="'+(user.thumb)+'">'; 329 list += '<span><i class="ytv-arrow down"></i>'+(user.title)+'</span>'; 330 list += '</a>'; 331 list += '</div>'; 332 333 list += '<div class="ytv-list-inner"><ul>'; 334 for(i=0; i<videos.length; i++){ 335 if(videos[i].status.embeddable){ 336 var video = { 337 title: videos[i].snippet.title, 338 slug: videos[i].id, 339 link: 'https://www.youtube.com/watch?v='+videos[i].id, 340 published: videos[i].snippet.publishedAt, 341 stats: videos[i].statistics, 342 duration: (videos[i].contentDetails.duration), 343 thumb: videos[i].snippet.thumbnails.medium.url 344 }; 345 346 var durationString = video.duration.match(/[0-9]+[HMS]/g); 347 var h = 0, m = 0, s = 0, time = ''; 348 durationString.forEach(function (duration) { 349 var unit = duration.charAt(duration.length-1); 350 var amount = parseInt(duration.slice(0,-1)); 351 switch (unit) { 352 case 'H': h = (amount > 9 ? '' + amount : '0' + amount); break; 353 case 'M': m = (amount > 9 ? '' + amount : '0' + amount); break; 354 case 'S': s = (amount > 9 ? '' + amount : '0' + amount); break; 355 } 356 }); 357 if (h){ time += h+':';} 358 if (m){ time += m+':';} else { time += '00:';} 359 if (s){ time += s;} else { time += '00';} 360 361 var isFirst = ''; 362 if(settings.playId==video.slug){ 363 isFirst = ' class="ytv-active"'; 364 first = video.slug; 365 } else if(first===true){ 366 first = video.slug; 367 } 368 369 list += '<li'+isFirst+'><a href="#" data-ytv="'+(video.slug)+'" class="ytv-clear">'; 370 list += '<div class="ytv-thumb"><div class="ytv-thumb-stroke"></div><span>'+(time)+'</span><img src="'+(video.thumb)+'"></div>'; 371 list += '<div class="ytv-content"><b>'+(video.title)+'</b>'; 372 if (video.stats) 373 { 374 list+='</b><span class="ytv-views">'+utils.addCommas(video.stats.viewCount)+' Views</span>'; 375 } 376 list += '</div></a></li>'; 377 } 378 } 379 list += '</ul></div>'; 380 settings.element.innerHTML = '<div class="ytv-relative"><div class="ytv-video"><div id="ytv-video-player"></div></div><div class="ytv-list">'+list+'</div></div>'; 381 if(settings.element.getElementsByClassName('ytv-active').length===0){ 382 settings.element.getElementsByTagName('li')[0].className = "ytv-active"; 383 } 384 var active = settings.element.getElementsByClassName('ytv-active')[0]; 385 active.parentNode.parentNode.scrollTop = active.offsetTop; 386 action.logic.loadVideo(first, settings.autoplay); 387 388 if (settings.playlist){ 389 utils.ajax.get( utils.endpoints.playlistInfo(settings.playlist), prepare.playlists ); 390 } else if(settings.browsePlaylists){ 391 utils.ajax.get( utils.endpoints.userPlaylists(), prepare.playlists ); 392 } 393 394 }); 395 } else console.log ('Youtube-TV Error: Empty video list'); 396 } 397 }, 398 action = { 399 400 logic: { 401 402 playerStateChange: function(d){ 403 console.log(d); 404 }, 405 406 loadVideo: function(slug, autoplay){ 407 var house = settings.element.getElementsByClassName('ytv-video')[0]; 408 var counter = settings.element.getElementsByClassName('ytv-video-playerContainer').length; 409 house.innerHTML = '<div id="ytv-video-player'+id+counter+'" class="ytv-video-playerContainer"></div>'; 410 411 cache.player = new YT.Player('ytv-video-player'+id+counter, { 412 videoId: slug, 413 events: { 414 onReady: settings.events.videoReady, 415 onStateChange: function(e){ 416 if( (e.target.getPlayerState()===0) && settings.chainVideos ){ 417 var ns = settings.element.getElementsByClassName('ytv-active')[0].nextSibling, 418 link = ns.children[0]; 419 link.click(); 420 } 421 settings.events.stateChange.call(this, e); 422 } 423 }, 424 playerVars: { 425 enablejsapi: 1, 426 origin: doc.domain, 427 controls: settings.controls ? 1 : 0, 428 rel: 0, 429 showinfo: 0, 430 iv_load_policy: settings.annotations ? '' : 3, 431 autoplay: autoplay ? 1 : 0, 432 theme: settings.playerTheme, 433 wmode: settings.wmode 434 } 435 }); 436 } 437 }, 438 439 endpoints: { 440 videoClick: function(e){ 441 var target = utils.parentUntil(e.target ? e.target : e.srcElement, 'data-ytv'); 442 if(target){ 443 if(target.getAttribute('data-ytv')){ 444 // Load Video 445 utils.events.prevent(e); 446 var activeEls = settings.element.getElementsByClassName('ytv-active'), 447 i; 448 for(i=0; i<activeEls.length; i++){ 449 activeEls[i].className=""; 450 } 451 target.parentNode.className="ytv-active"; 452 action.logic.loadVideo(target.getAttribute('data-ytv'), true); 453 } 454 } 455 }, 456 playlistToggle: function(e){ 457 var target = utils.parentUntil(e.target ? e.target : e.srcElement, 'data-ytv-playlist-toggle'); 458 if(target && target.getAttribute('data-ytv-playlist-toggle')){ 459 // Toggle Playlist 460 utils.events.prevent(e); 461 var lh = settings.element.getElementsByClassName('ytv-list-header')[0]; 462 if(lh.className.indexOf('ytv-playlist-open')===-1){ 463 lh.className += ' ytv-playlist-open'; 464 } else { 465 lh.className = lh.className.replace(' ytv-playlist-open', ''); 466 } 467 } 468 }, 469 playlistClick: function(e){ 470 var target = utils.parentUntil(e.target ? e.target : e.srcElement, 'data-ytv-playlist'); 471 472 if(target && target.getAttribute('data-ytv-playlist')){ 473 474 // Load Playlist 475 utils.events.prevent(e); 476 477 settings.pid = target.getAttribute('data-ytv-playlist'); 478 target.children[1].innerHTML = 'Loading...'; 479 480 utils.ajax.get( utils.endpoints.playlistInfo(settings.pid), function(res){ 481 var lh = settings.element.getElementsByClassName('ytv-list-header')[0]; 482 lh.className = lh.className.replace(' ytv-playlist-open', ''); 483 prepare.selectedPlaylist(res); 484 }); 485 } 486 } 487 }, 488 bindEvents: function(){ 489 utils.events.addEvent( settings.element, 'click', action.endpoints.videoClick ); 490 utils.events.addEvent( settings.element, 'click', action.endpoints.playlistToggle ); 491 utils.events.addEvent( settings.element, 'click', action.endpoints.playlistClick ); 492 } 493 }, 494 495 initialize = function(id, opts){ 496 utils.deepExtend(settings, opts); 497 if(settings.apiKey.length===0){ 498 alert("Missing APIkey in settings or as global vaiable."); 499 } 500 apiKey = settings.apiKey; 501 settings.element = (typeof id==='string') ? doc.getElementById(id) : id;
502 if(settings.element && (settings.user || settings.channelId || settings.playlist)){ 503 prepare.youtube(); 504 prepare.build(); 505 action.bindEvents(); 506 if (settings.playlist) { 507 utils.ajax.get( utils.endpoints.playlistInfo(settings.playlist), prepare.selectedPlaylist ); 508 } else { 509 utils.ajax.get( utils.endpoints.userInfo(), prepare.userUploads ); 510 } 511 } else console.log ('Youtube-TV Error: Missing either user, channelId, or playlist'); 512 }; 513 514 /* Public */ 515 516 this.destroy = function(){ 517 utils.events.removeEvent( settings.element, 'click', action.endpoints.videoClick ); 518 utils.events.removeEvent( settings.element, 'click', action.endpoints.playlistToggle ); 519 utils.events.removeEvent( settings.element, 'click', action.endpoints.playlistClick ); 520 settings.element.className = ''; 521 settings.element.innerHTML = ''; 522 }; 523 this.fullscreen = { 524 state: function(){ 525 return (settings.element.className).indexOf('ytv-full') !== -1; 526 }, 527 enter: function(){ 528 if( (settings.element.className).indexOf('ytv-full') === -1 ){ 529 settings.element.className += 'ytv-full'; 530 } 531 }, 532 exit: function(){ 533 if( (settings.element.className).indexOf('ytv-full') !== -1 ){ 534 settings.element.className = (settings.element.className).replace('ytv-full', ''); 535 } 536 } 537 }; 538 539 initialize(id, opts); 540 }; 541 542 if ((typeof module !== 'undefined') && module.exports) { 543 module.exports = YTV; 544 } 545 if (typeof ender === 'undefined') { 546 this.YTV = YTV; 547 } 548 if ((typeof define === "function") && define.amd) { 549 define("YTV", [], function() { 550 return YTV; 551 }); 552 } 553 if ((typeof jQuery !== 'undefined')) { 554 jQuery.fn.extend({ 555 ytv: function(options) { 556 return this.each(function() { 557 new YTV(this.id, options); 558 }); 559 } 560 }); 561 } 562}).call(this, window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.