mirror of
https://github.com/koel/koel
synced 2024-11-24 13:13:05 +00:00
Complete YouTube feature
This commit is contained in:
parent
b7a618fa8c
commit
e3ee03db24
15 changed files with 265 additions and 16 deletions
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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')));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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 video’s volume, progress and such are controlled from within
|
||||
the video itself, and not via Koel’s 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>
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -79,6 +79,10 @@ export default {
|
|||
playback.queueAndPlay(song);
|
||||
}
|
||||
},
|
||||
|
||||
'/youtube'() {
|
||||
loadMainView('youtubePlayer');
|
||||
},
|
||||
},
|
||||
|
||||
init() {
|
||||
|
|
|
@ -3,3 +3,4 @@ export * from './download';
|
|||
export * from './http';
|
||||
export * from './ls';
|
||||
export * from './playback';
|
||||
export * from './youtube';
|
||||
|
|
|
@ -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));
|
||||
|
|
35
resources/assets/js/services/youtube.js
Normal file
35
resources/assets/js/services/youtube.js
Normal 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');
|
||||
},
|
||||
};
|
|
@ -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 = {};
|
||||
|
|
Loading…
Reference in a new issue