|
<!DOCTYPE html> |
|
<html vocab="http://schema.org"> |
|
<head> |
|
<title>YouTube Viewer</title> |
|
<meta charset="utf-8" /> |
|
<meta name="description" content="検索したいタイプ(Channel,Video,Playlist)を選択、検索ボックスにキーワードを入力し、エンターキーを押すと検索結果をサムネイルで表示します。チャンネルやプレイリストをクリックすると動画一覧を表示します。動画一覧にマウスカーソルをポイントするとプレビュー再生します。" /> |
|
<meta name="keywords" content="Youtube,d3.js,Q.js,jquery" /> |
|
<meta name="author" content="sfpgmr" /> |
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> |
|
<!-- Latest compiled and minified CSS --> |
|
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> |
|
<!-- Latest compiled and minified JavaScript --> |
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/q.js/1.0.1/q.js"></script> |
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.js"></script> |
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> |
|
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script> |
|
<script type="text/javascript" src="//www.sfpgmr.net/scripts/youtube_apikey.js"></script> |
|
<style> |
|
html { |
|
position: relative; |
|
height: 100%; |
|
} |
|
|
|
body { |
|
/* Margin bottom by footer height */ |
|
margin-top: 50px; |
|
margin-bottom: 100px; |
|
} |
|
|
|
.footer { |
|
position: absolute; |
|
bottom: 0; |
|
width: 100%; |
|
/* Set the fixed height of the footer here */ |
|
height: 60px; |
|
background-color: #f5f5f5; |
|
} |
|
|
|
div#playerObj { |
|
display: none; |
|
position: absolute; |
|
} |
|
|
|
.thumbnail-title { |
|
color: white; |
|
position: absolute; |
|
text-shadow: 1px 1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, -1px -1px 0 #000; |
|
font-size: 14px; |
|
} |
|
</style> |
|
|
|
<script type="text/javascript"> |
|
if (!location.href.match(/localhost/)) { |
|
var _gaq = _gaq || []; |
|
_gaq.push(['_setAccount', 'UA-15457703-11']); |
|
_gaq.push(['_trackPageview']); |
|
|
|
(function () { |
|
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; |
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; |
|
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); |
|
})(); |
|
} |
|
</script> |
|
</head> |
|
<body> |
|
<nav class="navbar navbar-default navbar-fixed-top " role="navigation"> |
|
<div class="container"> |
|
<!-- Brand and toggle get grouped for better mobile display --> |
|
<div class="navbar-header"> |
|
<button type="button" class="navbar-toggle navbar-toggle-sm collapsed" data-toggle="collapse" data-target="#navbar-collapse"> |
|
<span class="sr-only">Toggle navigation</span> |
|
<span class="icon-bar"></span> |
|
<span class="icon-bar"></span> |
|
<span class="icon-bar"></span> |
|
</button> |
|
<div><form class="navbar-form navbar-left" role="search" id="search"><div class="form-group"><div class="input-group input-group-sm"><select id="type" style="width:100px" class="form-control"><option value="channel">Channel</option><option value="video">Video</option><option value="playlist">PlayList</option></select><input id="keyword" style="width:160px" type="text" class="form-control" placeholder="キーワードを入力" /></div></div></form></div> |
|
</div> |
|
<div class="collapse navbar-collapse" id="navbar-collapse"> |
|
<p class="navbar-text navbar-text-sm navbar-right"><a id="homeLink" href="/" class="navbar-link"><span class="glyphicon glyphicon-home"></span>ホーム</a></p> |
|
</div> |
|
</div> |
|
</nav> |
|
<article id="article" typeof="Article"> |
|
<header class="container"><h2>YouTube Viewer</h2> |
|
<p>検索したいタイプ(Channel,Video,Playlist)を選択、検索ボックスにキーワードを入力し、エンターキーを押すと検索結果をサムネイルで表示します。チャンネルやプレイリストをクリックすると動画一覧を表示します。動画一覧にマウスカーソルをポイントするとプレビュー再生します。</p> |
|
<ol class="breadcrumb" id="breadcrumb"> |
|
<li>test</li> |
|
</ol> |
|
</header> |
|
<div class="container"> |
|
<section id="articles" class="row"></section> |
|
<aside style="margin-left:auto;margin-right:auto;display:block;"> |
|
<style> |
|
.blocks { |
|
width: 320px; |
|
height: 50px; |
|
} |
|
|
|
@media(min-width: 500px) { |
|
.blocks { |
|
width: 468px; |
|
height: 60px; |
|
} |
|
} |
|
|
|
@media(min-width: 800px) { |
|
.blocks { |
|
width: 728px; |
|
height: 90px; |
|
} |
|
} |
|
</style> |
|
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script> |
|
<!-- blocks --> |
|
<ins class="adsbygoogle blocks" |
|
style="display:inline-block;text-align:center;" |
|
data-ad-client="ca-pub-4402137407996189" |
|
data-ad-slot="3288576128"></ins> |
|
<script> |
|
(adsbygoogle = window.adsbygoogle || []).push({}); |
|
</script> |
|
</aside> |
|
</div> |
|
|
|
</article> |
|
<footer id="footer" class="navbar navbar-default navbar-fixed-bottom"> |
|
<div class="container"> |
|
<div class="row"> |
|
<div class="col-xs-3" property="creator" id="creator"> |
|
<div typeof="person" id="creatorPerson"> |
|
<span property="name" id="creatorName">By <a href="http://www.sfpgmr.net/">S.F.</a></span> |
|
</div> |
|
</div> |
|
<div class="col-xs-3 text-center"></div> |
|
<div class="col-xs-6 text-right"><small><time id="date" property="dc:date" datetime="2015-01-17T01:47:04.299Z">2015-01-17T01:47:04.299Z</time></small></div> |
|
</div> |
|
</div> |
|
</footer> |
|
<div id="playerObj"> |
|
<div id="player"> |
|
</div> |
|
</div> |
|
<script type="text/javascript"> |
|
/// <reference path="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.js" /> |
|
"use strict"; |
|
|
|
var player; |
|
var playerWidth = 640; |
|
var playerHeight = 360; |
|
var fetchController; |
|
// Youtube Player APIのロード |
|
d3.select('head').append('script').attr('src', '//www.youtube.com/iframe_api'); |
|
|
|
function onYouTubeIframeAPIReady() { |
|
var player_ = new YT.Player('player', { |
|
width: playerWidth, |
|
height: playerHeight, |
|
playerVars: { |
|
'fs': 1, |
|
'hd': 1 |
|
}, |
|
events: { |
|
'onReady': onPlayerReady |
|
}, |
|
enablejsapi: '1' |
|
}); |
|
|
|
function onPlayerReady(event) { |
|
player = player_; |
|
d3.select('#player').on('mouseleave', stopVideo); |
|
d3.select(window).on('mouseleave', stopVideo); |
|
} |
|
} |
|
|
|
function stopVideo() { |
|
if (player) { |
|
player.stopVideo(); |
|
var p = d3.select('#playerObj'); |
|
p.style('display', 'none'); |
|
|
|
} |
|
}; |
|
|
|
function previewVideo(this_,videoId) { |
|
var o = d3.select(this_); |
|
var rect = o.node().getBoundingClientRect(); |
|
var p = d3.select('#playerObj'); |
|
player.setSize(rect.width, rect.height); |
|
p.style('width', rect.width + 'px') |
|
.style('height', rect.height + 'px') |
|
.style('left', (rect.left + window.scrollX) + 'px') |
|
.style('top', (rect.top + window.scrollY) + 'px') |
|
.style('display', 'block'); |
|
player.loadVideoById({ videoId: videoId }); |
|
d3.event.preventDefault(); |
|
}; |
|
|
|
if (window.location.href.match(/bl\.ocks\.org/i)) { |
|
d3.select('#homeLink').attr('href', '//bl.ocks.org/sfpgmr/'); |
|
} |
|
|
|
var keywordSelection = d3.select('#keyword'); |
|
var typeSelection = d3.select('#type'); |
|
var breadcrumbSelection = d3.select('#breadcrumb'); |
|
|
|
keywordSelection.node().value = 'SFPG'; |
|
|
|
//d3.select('#displayPlayLists').on('click', function () { |
|
// d3.event.preventDefault(); |
|
// doPlayLists(d3.select('#channelID').node().value); |
|
//}); |
|
|
|
var typeJumpTable = { |
|
channel: doChannelList, |
|
video: doVideoList, |
|
playlist: doPlayList |
|
} |
|
|
|
function query() { |
|
var type = typeSelection.node(); |
|
var typeValue = type.options[type.selectedIndex].value; |
|
typeJumpTable[typeValue](keywordSelection.node().value); |
|
breadcrumbSelection.selectAll('li').remove(); |
|
breadcrumbSelection |
|
.append('li') |
|
.append('a') |
|
.attr('href', '#') |
|
.text(typeValue) |
|
.on('click', function () { |
|
d3.event.preventDefault(); |
|
query(); |
|
}) |
|
breadcrumbSelection |
|
.append('li') |
|
.classed('active', true) |
|
.text(keywordSelection.node().value); |
|
}; |
|
|
|
d3.select('#search').on('submit', function () { |
|
d3.event.preventDefault(); |
|
query(); |
|
}); |
|
|
|
|
|
var json = Q.nfbind(d3.json); |
|
var apiEntryPonint = 'https://www.googleapis.com/youtube/v3/'; |
|
|
|
|
|
// Youtube APIの呼び出し |
|
function callYoutubeDataAPI(command, params) { |
|
var queryString = ''; |
|
for (var p in params) { |
|
queryString += p + '=' + encodeURIComponent(params[p]) + '&'; |
|
} |
|
queryString += 'key=' + youtube_apikey; |
|
var defer = Q.defer(); |
|
json(apiEntryPonint + command + '?' + queryString) |
|
.then(function (d) { |
|
defer.resolve(d); |
|
}); |
|
return defer.promise; |
|
} |
|
|
|
// データフェッチ制御 |
|
function FetchController(apiFunc, resultFunc, doNotAutoFetch) { |
|
|
|
this.nextPageToken = ''; |
|
this.fetching = false; |
|
this.apiFunc = apiFunc; |
|
this.resultFunc = resultFunc; |
|
if (!doNotAutoFetch) { |
|
d3.select(window) |
|
.on("scroll", this.maybeFetch.bind(this)) |
|
.on("resize", this.maybeFetch.bind(this)); |
|
} |
|
} |
|
|
|
FetchController.prototype = { |
|
maybeFetch: function () { |
|
if (!this.fetching && this.nextPageToken |
|
&& d3.select("#article").node().getBoundingClientRect().top < window.innerHeight) { |
|
this.fetch(this.nextPageToken); |
|
} |
|
}, |
|
fetch: function () { |
|
var this_ = this; |
|
this_.fetching = true; |
|
this_.apiFunc(this_.nextPageToken) |
|
.then(function (result) { |
|
this_.nextPageToken = (!result.nextPageToken) ? '' : result.nextPageToken; |
|
Q(this_.resultFunc(result)).then(function () { this_.fetching = false; }); |
|
}); |
|
} |
|
} |
|
|
|
// サムネイル追加 |
|
function appendThumbnail(selection) { |
|
return selection.append('div') |
|
.classed({ 'col-xs-6': true, 'col-sm-3': true, 'col-md-2': true, 'col-lg-2': true }) |
|
.append('div') |
|
.classed('thumbnail', true) |
|
.style('overflow', 'auto') |
|
.style('min-height', '125px'); |
|
} |
|
|
|
// タイトル表示追加 |
|
function appendTitle(selection) { |
|
return selection |
|
.append('p') |
|
.text(function (d) { return d.snippet.title; }) |
|
.classed('thumbnail-title', true) |
|
.each(function (d) { |
|
var o = d3.select(this.parentNode); |
|
var rect = o.node().getBoundingClientRect(); |
|
d3.select(this).style('top', '5px') |
|
.style('left', '20px') |
|
.style('width', (rect.width - 10) + 'px') |
|
.style('height', '40px'); |
|
|
|
}); |
|
} |
|
|
|
// イメージ追加 |
|
function appendImage(selection) { |
|
return selection.append('img') |
|
.attr('src', function (d) { return d.snippet.thumbnails.default.url; }) |
|
.attr('alt', function (d) { return d.snippet.title; }) |
|
.style({'width':'auto','height':'110px'}); |
|
} |
|
|
|
// キーワードからチャンネル一覧を作成 |
|
function doChannelList(keyword) { |
|
stopVideo(); |
|
d3.select('#articles').selectAll('div').remove(); |
|
|
|
|
|
var channelList = []; |
|
fetchController = new FetchController( |
|
function (pageToken) { |
|
return callYoutubeDataAPI('search', |
|
{ |
|
part: 'snippet', |
|
type: 'channel', |
|
q: keyword, |
|
order: 'date', |
|
maxResults: '50', |
|
'pageToken': pageToken |
|
} |
|
); |
|
} |
|
, function (result) { |
|
|
|
Array.prototype.push.apply(channelList, result.items); |
|
appendThumbnail(d3.select('#articles').selectAll('div') |
|
.data(channelList, function (d) { return d.id.channelId; }) |
|
.enter()) |
|
.append('a') |
|
.attr('href', '#') |
|
.attr('target', '_blank') |
|
.on('click', function (d) { |
|
d3.event.preventDefault(); |
|
breadcrumbSelection |
|
.append('li') |
|
.classed('active', true) |
|
.append('a').attr('href','#') |
|
.text('Videos') |
|
.on('click', function () { |
|
var last = breadcrumbSelection.select('li:last-child'); |
|
if (last.text() != 'PlayLists') { |
|
last.remove(); |
|
} |
|
doVideoList(null,d.id.channelId);} |
|
); |
|
doVideoList(null, d.id.channelId); |
|
}) |
|
.call(appendImage) |
|
.call(appendTitle); |
|
|
|
}); |
|
fetchController.fetch(); |
|
|
|
} |
|
|
|
// 公開動画一覧を表示 |
|
function doVideoList(keyword,channelId) { |
|
stopVideo(); |
|
d3.select('#articles').selectAll('div').remove(); |
|
if (!keyword) keyword = ''; |
|
if (!channelId) { channelId = '' } |
|
else { |
|
if (breadcrumbSelection.select('li:last-child').text() != 'PlayLists') { |
|
breadcrumbSelection |
|
.append('li') |
|
.attr('data-type', 'Playlists') |
|
.classed('active', false) |
|
.append('a').attr('href', '#') |
|
.text('PlayLists') |
|
.on('click', function () { |
|
var last = breadcrumbSelection.select('li:last-child'); |
|
if (last.text() != 'PlayLists') { |
|
last.remove(); |
|
} |
|
doPlayList(null, channelId); |
|
}); |
|
|
|
|
|
} |
|
}; |
|
|
|
var videoList = []; |
|
fetchController = new FetchController( |
|
function (pageToken) { |
|
return callYoutubeDataAPI('search', |
|
{ |
|
part: 'snippet', |
|
type: 'video', |
|
'channelId':channelId, |
|
q: keyword, |
|
order: 'date', |
|
maxResults: '50', |
|
'pageToken': pageToken |
|
} |
|
); |
|
} |
|
, function (result) { |
|
Array.prototype.push.apply(videoList, result.items.filter(function (d) { |
|
return d.snippet.thumbnails; |
|
})); |
|
|
|
appendThumbnail(d3.select('#articles').selectAll('div') |
|
.data(videoList, function (d) { return d.id.videoId; }) |
|
.enter()) |
|
.call(appendImage) |
|
.call(appendTitle) |
|
.on('mouseenter', function (d) { |
|
previewVideo(this, d.id.videoId); |
|
}); |
|
}); |
|
fetchController.fetch(); |
|
} |
|
|
|
// プレイリストを表示 |
|
function doPlayList(keyword,channelId) { |
|
stopVideo(); |
|
if (!keyword) keyword = ''; |
|
if (!channelId) { channelId = '' } |
|
|
|
d3.select('#articles').selectAll('div').remove(); |
|
d3.select('#playListTitle').text('情報取得中..').style('color', 'blue'); |
|
|
|
fetchController = new FetchController( |
|
function (pageToken) { |
|
return callYoutubeDataAPI( |
|
'search' |
|
, { part: 'snippet',type:'playlist', channelId : channelId,q: keyword, order: 'date', maxResults: '50', pageToken: pageToken } |
|
); |
|
}, |
|
displayPlayLists); |
|
|
|
var playListItems = []; |
|
|
|
fetchController.fetch(); |
|
|
|
function displayPlayLists(result) { |
|
Array.prototype.push.apply(playListItems, result.items); |
|
d3.select('#playListTitle').text(''); |
|
appendThumbnail(d3.select('#articles').selectAll('div') |
|
.data(playListItems, function (d) { return d.id.playlistId; }) |
|
.enter()) |
|
.append('a') |
|
.attr('href', '#') |
|
.attr('target', '_blank') |
|
.on('click', function (d) { |
|
d3.event.preventDefault(); |
|
breadcrumbSelection |
|
.append('li') |
|
.classed('active',true) |
|
.text(d.snippet.title); |
|
doThumbnailVideos(d); |
|
}) |
|
.call(appendTitle) |
|
.call(appendImage); |
|
} |
|
} |
|
|
|
function doThumbnailVideos(d) { |
|
var playlistID = d.id.playlistId; |
|
d3.select('#articles').selectAll('div').remove(); |
|
d3.select('#playListTitle') |
|
.text(function (dt) { return d.snippet.title; }); |
|
|
|
var videoItems = []; |
|
|
|
fetchController = new FetchController( |
|
function (pageToken) { |
|
return callYoutubeDataAPI( |
|
'playlistItems' |
|
, { part: 'snippet', 'playlistId': playlistID, order: 'date', maxResults: '50', 'pageToken': pageToken } |
|
); |
|
}, |
|
displayThumbnail |
|
); |
|
|
|
fetchController.fetch(); |
|
|
|
function displayThumbnail(d) { |
|
Array.prototype.push.apply(videoItems, d.items.filter(function (d) { |
|
return !(!d.snippet.thumbnails); |
|
})); |
|
appendThumbnail(d3.select('#articles').selectAll('div') |
|
.data(videoItems, function (d) { return d.id; }) |
|
.enter()) |
|
.call(appendImage) |
|
.on('mouseenter', function (d) { |
|
previewVideo(this, d.snippet.resourceId.videoId); |
|
}) |
|
.call(appendTitle); |
|
|
|
} |
|
|
|
} |
|
|
|
query(); |
|
</script> |
|
</body> |
|
</html> |