Complete YouTube feature

This commit is contained in:
An Phan 2016-07-30 23:32:17 +08:00
parent b7a618fa8c
commit e3ee03db24
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
15 changed files with 265 additions and 16 deletions

View file

@ -9,6 +9,7 @@ use App\Models\Playlist;
use App\Models\Setting;
use App\Models\User;
use Lastfm;
use YouTube;
class DataController extends Controller
{
@ -34,6 +35,7 @@ class DataController extends Controller
'users' => auth()->user()->is_admin ? User::all() : [],
'currentUser' => auth()->user(),
'useLastfm' => Lastfm::used(),
'useYouTube' => YouTube::enabled(),
'allowDownload' => env('ALLOW_DOWNLOAD', true),
'cdnUrl' => app()->staticUrl(),
'currentVersion' => Application::VERSION,

View file

@ -9,6 +9,7 @@ use App\Http\Streamers\TranscodingStreamer;
use App\Http\Streamers\XAccelRedirectStreamer;
use App\Http\Streamers\XSendFileStreamer;
use App\Models\Song;
use YouTube;
class SongController extends Controller
{
@ -63,6 +64,7 @@ class SongController extends Controller
'lyrics' => $song->lyrics,
'album_info' => $song->album->getInfo(),
'artist_info' => $song->artist->getInfo(),
'youtube' => YouTube::searchVideosRelatedToSong($song),
]);
}

View file

@ -18,13 +18,6 @@ class YouTubeController extends Controller
*/
public function searchVideosRelatedToSong(Request $request, Song $song)
{
$q = $song->title;
// If the artist is worth noticing, include them into the search.
if (!$song->artist->isUnknown() && !$song->artist->isVarious()) {
$q .= ' '.$song->artist->name;
}
return response()->json(YouTube::search($q, $request->input('pageToken')));
return response()->json(YouTube::searchVideosRelatedToSong($song, $request->input('pageToken')));
}
}

View file

@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Song;
use Cache;
use GuzzleHttp\Client;
@ -34,7 +35,27 @@ class YouTube extends RESTfulService
}
/**
* Search for YouTube videos.
* Search for YouTube videos related to a song.
*
* @param Song $song
* @param string $pageToken
*
* @return object|false
*/
public function searchVideosRelatedToSong($song, $pageToken = '')
{
$q = $song->title;
// If the artist is worth noticing, include them into the search.
if (!$song->artist->isUnknown() && !$song->artist->isVarious()) {
$q .= ' '.$song->artist->name;
}
return $this->search($q, $pageToken);
}
/**
* Search for YouTube videos by a query string.
*
* @param string $q The query string
* @param string $pageToken YouTube page token (e.g. for next/previous page)
@ -48,7 +69,7 @@ class YouTube extends RESTfulService
return false;
}
$uri = sprintf('search?part=snippet&maxResults=%s&pageToken=%s&q=%s',
$uri = sprintf('search?part=snippet&type=video&maxResults=%s&pageToken=%s&q=%s',
$perPage,
urlencode($pageToken),
urlencode($q)

View file

@ -43,10 +43,11 @@
"rangetouch": "0.0.9",
"select": "^1.0.6",
"sinon": "^1.17.2",
"vue": "^2.0.0-beta.1",
"vue": "^2.0.0-beta.5",
"vue-hot-reload-api": "^1.3.2",
"vueify": "^9.1.0",
"vueify-insert-css": "^1.0.0"
"vueify-insert-css": "^1.0.0",
"youtube-player": "^3.0.4"
},
"scripts": {
"postinstall": "cross-env NODE_ENV=production && gulp --production",

View file

@ -8,6 +8,9 @@
:class="{ active: currentView === 'artistInfo' }">Artist</a>
<a @click.prevent="currentView = 'albumInfo'"
:class="{ active: currentView === 'albumInfo' }">Album</a>
<a @click.prevent="currentView = 'youtube'"
v-if="sharedState.useYouTube"
:class="{ active: currentView === 'youtube' }"><i class="fa fa-youtube-play"></i></a>
</div>
<div class="panes">
@ -24,6 +27,11 @@
ref="album-info"
v-show="currentView === 'albumInfo'">
</album-info>
<youtube v-if="sharedState.useYouTube"
:song="song" :youtube="song.youtube"
ref="youtube"
v-show="currentView === 'youtube'">
</youtube>
</div>
</div>
</section>
@ -35,21 +43,23 @@ import { invokeMap } from 'lodash';
import $ from 'jquery';
import { event } from '../../../utils';
import { songStore, preferenceStore as preferences } from '../../../stores';
import { sharedStore, songStore, preferenceStore as preferences } from '../../../stores';
import { songInfo } from '../../../services';
import lyrics from './lyrics.vue';
import artistInfo from './artist-info.vue';
import albumInfo from './album-info.vue';
import youtube from './youtube.vue';
export default {
name: 'main-wrapper--extra--index',
components: { lyrics, artistInfo, albumInfo },
components: { lyrics, artistInfo, albumInfo, youtube },
data() {
return {
song: songStore.stub,
state: preferences.state,
sharedState: sharedStore.state,
currentView: 'lyrics',
};
},
@ -102,7 +112,11 @@ export default {
}
},
'song:played': song => songInfo.fetch(this.song = song),
'song:played': song => {
songInfo.fetch(song).then(song => {
this.song = song;
});
},
'koel:teardown': () => this.resetState(),
});

View file

@ -0,0 +1,94 @@
<template>
<div id="youtube-wrapper">
<template v-if="videos && videos.length">
<a class="video" v-for="video in videos" href="#" @click.prevent="playYouTube(video.id.videoId)">
<div class="thumb">
<img :src="video.snippet.thumbnails.default.url" width="90">
</div>
<div class="meta">
<h3 class="title">{{ video.snippet.title }}</h3>
<p class="desc">{{ video.snippet.description }}</p>
</div>
</a>
<button @click="loadMore" v-if="!loading" class="more btn-blue">Load More</button>
</template>
<p class="nope" v-else>Play a song to retreive related YouTube videos.</p>
<p class="nope" v-show="loading">Loading</p>
</div>
</template>
<script>
import { event } from '../../../utils';
import { youtube as youtubeService } from '../../../services';
export default {
name: 'main-wrapper--extra--youtube',
// We explicitly use 'youtube' as a prop here to force reactivity.
props: ['song'],
data() {
return {
loading: false,
videos: [],
};
},
watch: {
song(val) {
this.videos = val.youtube ? val.youtube.items : [];
},
},
methods: {
playYouTube(id) {
youtubeService.play(id);
},
/**
* Load more videos.
*/
loadMore() {
this.loading = true;
youtubeService.searchVideosRelatedToSong(this.song, () => {
this.videos = this.song.youtube.items;
this.loading = false;
});
},
},
};
</script>
<style lang="sass" scoped>
#youtube-wrapper {
overflow-x: hidden;
.video {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #333;
.thumb {
margin-right: 10px;
}
.title {
font-size: 1.1rem;
margin-bottom: .4rem;
}
.desc {
font-size: .9rem;
}
&:hover {
.title {
color: #fff;
}
}
&:last-of-type {
margin-bottom: 16px;
}
}
}
</style>

View file

@ -13,6 +13,7 @@
<playlist v-show="view === 'playlist'"></playlist>
<favorites v-show="view === 'favorites'"></favorites>
<profile v-show="view === 'profile'"></profile>
<youtube-player v-show="view === 'youtubePlayer'"></youtube-player>
</section>
</template>
@ -32,9 +33,11 @@ import home from './home.vue';
import playlist from './playlist.vue';
import favorites from './favorites.vue';
import profile from './profile.vue';
import youtubePlayer from './youtube-player.vue';
export default {
components: { albums, album, artists, artist, songs, settings, users, home, queue, playlist, favorites, profile },
components: { albums, album, artists, artist, songs, settings,
users, home, queue, playlist, favorites, profile, youtubePlayer },
data() {
return {

View file

@ -0,0 +1,67 @@
<template>
<section id="youTubePlayer">
<h1 class="heading"><span>YouTube Video</span></h1>
<div id="player">
<p class="none">Your YouTube video will be played here.<br/>
You can start a video playback from the right sidebar. When a song is playing, that is.<br>
It might also be worth noting that videos volume, progress and such are controlled from within
the video itself, and not via Koels controls.</p>
</div>
</section>
</template>
<script>
import { event } from '../../../utils';
import { playback } from '../../../services';
import YouTubePlayer from 'youtube-player';
let player;
export default {
name: 'main-wrapper--main-content--youtube-player',
methods: {
/**
* Initialize the YouTube player. This should only be called once.
*/
initPlayer() {
if (!player) {
player = YouTubePlayer('player', {
width: '100%',
height: '100%',
});
player.on('stateChange', event => {
// Pause song playback when video is played
event.data === 1 && playback.pause();
});
}
},
},
created() {
event.on({
'youtube:play': id => {
this.initPlayer();
player.loadVideoById(id);
player.playVideo();
},
/**
* Stop video playback when a song is played/resumed.
*/
'song:played': () => player && player.pauseVideo(),
});
},
};
</script>
<style lang="sass" scoped>
@import "../../../../sass/partials/_vars.scss";
@import "../../../../sass/partials/_mixins.scss";
.none {
color: $color2ndText;
padding: 16px 24px;
}
</style>

View file

@ -24,6 +24,9 @@
<li>
<a class="artists" :class="[currentView == 'artists' ? 'active' : '']" href="/#!/artists">Artists</a>
</li>
<li v-if="sharedState.useYouTube">
<a class="youtube" :class="[currentView == 'youtubePlayer' ? 'active' : '']" href="/#!/youtube">YouTube Video</a>
</li>
</ul>
</section>
@ -225,6 +228,10 @@ export default {
content: "\f130";
}
&.youtube::before {
content: "\f16a";
}
&.settings::before {
content: "\f013";
}

View file

@ -79,6 +79,10 @@ export default {
playback.queueAndPlay(song);
}
},
'/youtube'() {
loadMainView('youtubePlayer');
},
},
init() {

View file

@ -3,3 +3,4 @@ export * from './download';
export * from './http';
export * from './ls';
export * from './playback';
export * from './youtube';

View file

@ -18,6 +18,7 @@ export const songInfo = {
song.lyrics = data.lyrics;
data.artist_info && artistInfo.merge(song.artist, data.artist_info);
data.album_info && albumInfo.merge(song.album, data.album_info);
song.youtube = data.youtube;
song.infoRetrieved = true;
resolve(song);
}, r => reject(r));

View file

@ -0,0 +1,35 @@
import { http, playback } from '.';
import { assign } from 'lodash';
import { event, loadMainView } from '../utils';
import router from '../router';
export const youtube = {
/**
* Search for YouTube videos related to a song.
*
* @param {Object} song
* @param {Function} cb
*/
searchVideosRelatedToSong(song, cb = null) {
if (!song.youtube) {
song.youtube = {};
}
const pageToken = song.youtube.nextPageToken || '';
http.get(`youtube/search/song/${song.id}?pageToken=${pageToken}`).then(data => {
song.youtube.nextPageToken = data.nextPageToken;
song.youtube.items.push(...data.items);
cb && cb();
});
},
/**
* Play a YouTube video.
*
* @param {string} id The video ID
*/
play(id) {
event.emit('youtube:play', id);
router.go('youtube');
},
};

View file

@ -17,6 +17,7 @@ export const sharedStore = {
currentUser: null,
playlists: [],
useLastfm: false,
useYouTube: false,
allowDownload: false,
currentVersion: '',
latestVersion: '',
@ -33,6 +34,9 @@ export const sharedStore = {
assign(this.state, data);
// Always disable YouTube integration on mobile.
this.state.useYouTube = this.state.useYouTube && !isMobile.phone;
// If this is a new user, initialize his preferences to be an empty object.
if (!this.state.currentUser.preferences) {
this.state.currentUser.preferences = {};