Add iTunes integration

This commit is contained in:
An Phan 2016-12-11 21:08:30 +08:00
parent 27793adefd
commit 86ca8d40f6
No known key found for this signature in database
GPG key ID: 05536BB4BCDC02A2
19 changed files with 386 additions and 51 deletions

13
app/Facades/iTunes.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class iTunes extends Facade
{
protected static function getFacadeAccessor()
{
return 'iTunes';
}
}

View file

@ -8,6 +8,7 @@ use App\Models\Interaction;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use iTunes;
use Lastfm; use Lastfm;
use YouTube; use YouTube;
@ -36,6 +37,7 @@ class DataController extends Controller
'currentUser' => auth()->user(), 'currentUser' => auth()->user(),
'useLastfm' => Lastfm::used(), 'useLastfm' => Lastfm::used(),
'useYouTube' => YouTube::enabled(), 'useYouTube' => YouTube::enabled(),
'useiTunes' => iTunes::used(),
'allowDownload' => config('koel.download.allow'), 'allowDownload' => config('koel.download.allow'),
'cdnUrl' => app()->staticUrl(), 'cdnUrl' => app()->staticUrl(),
'currentVersion' => Application::KOEL_VERSION, 'currentVersion' => Application::KOEL_VERSION,

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Requests\API\ViewSongRequest;
use App\Models\Album;
use iTunes;
class iTunesController extends Controller
{
/**
* View a song on iTunes store.
*
* @param ViewSongRequest $request
* @param Album $album
*
* @return \Illuminate\Http\RedirectResponse
*/
public function viewSong(ViewSongRequest $request, Album $album)
{
$url = iTunes::getTrackUrl($request->q, $album->name, $album->artist->name);
if ($url) {
return redirect($url);
} else {
abort(404, "Koel can't find such a song on iTunes Store.");
}
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\API;
use Illuminate\Foundation\Http\FormRequest;
/**
* @property string q
*/
class ViewSongRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'q' => 'required',
];
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Providers;
use App\Services\iTunes;
use Illuminate\Support\ServiceProvider;
class iTunesServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
app()->singleton('iTunes', function () {
return new iTunes();
});
}
}

75
app/Services/iTunes.php Normal file
View file

@ -0,0 +1,75 @@
<?php
namespace App\Services;
use GuzzleHttp\Client;
class iTunes
{
/**
* @var Client
*/
protected $client;
/**
* @var string
*/
protected $endPoint = 'https://itunes.apple.com/search';
/**
* iTunes constructor.
*
* @param Client|null $client
*/
public function __construct(Client $client = null)
{
$this->client = $client ?: new Client();
}
/**
* Determines whether to use iTunes services.
*
* @return bool
*/
public function used()
{
return (bool) config('koel.itunes.enabled');
}
/**
* Search for a track on iTunes Store with the given information and get its URL.
*
* @param $term string The main query string (should be the track's name)
* @param $album string The album's name, if available
* @param $artist string The artist's name, if available
*
* @return string|false
*/
public function getTrackUrl($term, $album = '', $artist = '')
{
$params = [
'term' => $term.($album ? ' '.$album : '').($artist ? ' '.$artist : ''),
'media' => 'music',
'entity' => 'song',
'limit' => 1,
];
$response = (string) $this->client->get($this->endPoint, ['query' => $params])->getBody();
$response = json_decode($response);
if (!$response->resultCount) {
return false;
}
$trackUrl = $response->results[0]->trackViewUrl;
// Returns a string if the URL has parameters or NULL if not
if (parse_url($trackUrl, PHP_URL_QUERY)) {
$trackUrl .= '&at='.config('koel.itunes.affiliate_id');
} else {
$trackUrl .= '?at='.config('koel.itunes.affiliate_id');
}
return $trackUrl;
}
}

View file

@ -157,6 +157,7 @@ return [
App\Providers\YouTubeServiceProvider::class, App\Providers\YouTubeServiceProvider::class,
App\Providers\DownloadServiceProvider::class, App\Providers\DownloadServiceProvider::class,
App\Providers\BroadcastServiceProvider::class, App\Providers\BroadcastServiceProvider::class,
App\Providers\iTunesServiceProvider::class,
], ],
@ -214,6 +215,7 @@ return [
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class, 'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
'AWS' => Aws\Laravel\AwsFacade::class, 'AWS' => Aws\Laravel\AwsFacade::class,
'Sentry' => Sentry\SentryLaravel\SentryFacade::class, 'Sentry' => Sentry\SentryLaravel\SentryFacade::class,
'iTunes' => App\Facades\iTunes::class,
], ],

View file

@ -110,4 +110,9 @@ return [
*/ */
'ignore_dot_files' => env('IGNORE_DOT_FILES', true), 'ignore_dot_files' => env('IGNORE_DOT_FILES', true),
'itunes' => [
'enabled' => env('USE_ITUNES', true),
'affiliate_id' => '1000lsGu',
]
]; ];

20
resources/assets/img/itunes.svg Executable file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="176px" height="177px" viewBox="0 0 176 177" enable-background="new 0 0 176 177" xml:space="preserve">
<g>
<g>
<path fill="#fff" d="M88,0.5c-48.602,0-88,39.399-88,88c0,48.601,39.398,88,88,88c48.601,0,88-39.399,88-88
C176,39.899,136.601,0.5,88,0.5z M88,169.108c-44.52,0-80.608-36.09-80.608-80.608C7.392,43.981,43.48,7.892,88,7.892
c44.519,0,80.608,36.089,80.608,80.608C168.608,133.019,132.519,169.108,88,169.108z M126.254,31.462
c-1.221-1.033-3.796-0.399-3.796-0.399L70.117,41.451c0,0-2.253,0.057-3.795,1.998c-1.083,1.363-0.999,3.796-0.999,3.796v62.129
c0,0,0.188,2.209-1.398,3.796c-1.843,1.842-6.479,2.14-10.588,3.196c-6.84,1.76-13.585,4.634-13.585,12.985
c0,5.86,2.965,13.186,13.186,13.186c12.313,0,18.578-6.789,18.578-15.982c0-5.952,0-7.392,0-7.392l0.2-47.346
c0,0-0.299-2.608,0.6-3.596c1.264-1.389,3.995-1.598,3.995-1.598l40.354-8.19c0,0,2.452-0.845,3.596,0
c1.154,0.853,0.999,3.396,0.999,3.396v36.159c0,0,0.219,2.379-0.999,3.596c-2.288,2.289-6.141,2.242-9.988,3.196
c-7.225,1.792-14.783,4.386-14.783,13.785c0,9.929,9.67,12.785,12.785,12.785c13.62,0,19.378-7.352,19.378-16.182V35.458
C127.652,35.458,127.622,32.62,126.254,31.462z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -38,7 +38,8 @@
</template> </template>
<script> <script>
import { playback } from '../../../services' import { sharedStore } from '../../../stores'
import { playback, ls } from '../../../services'
import trackListItem from '../../shared/track-list-item.vue' import trackListItem from '../../shared/track-list-item.vue'
export default { export default {
@ -47,7 +48,8 @@ export default {
data () { data () {
return { return {
showingFullWiki: false showingFullWiki: false,
useiTunes: sharedStore.state.useiTunes
} }
}, },
@ -64,6 +66,10 @@ export default {
showFull () { showFull () {
return this.mode === 'full' || this.showingFullWiki return this.mode === 'full' || this.showingFullWiki
},
iTunesUrl () {
return `/api/itunes/album/${this.album.id}&jwt-token=${ls.get('jwt-token')}`
} }
}, },

View file

@ -2,18 +2,33 @@
<li :class="{ available: correspondingSong }" :title="tooltip" @click="play"> <li :class="{ available: correspondingSong }" :title="tooltip" @click="play">
<span class="no">{{ index + 1 }}</span> <span class="no">{{ index + 1 }}</span>
<span class="title">{{ track.title }}</span> <span class="title">{{ track.title }}</span>
<a
:href="iTunesUrl"
v-if="useiTunes && !correspondingSong"
target="_blank"
class="view-on-itunes"
title="View on iTunes"
>
iTunes
</a>
<span class="length">{{ track.fmtLength }}</span> <span class="length">{{ track.fmtLength }}</span>
</li> </li>
</template> </template>
<script> <script>
import { songStore, queueStore } from '../../stores' import { songStore, queueStore, sharedStore } from '../../stores'
import { playback } from '../../services' import { ls, playback } from '../../services'
export default { export default {
name: 'shared--track-list-item', name: 'shared--track-list-item',
props: ['album', 'track', 'index'], props: ['album', 'track', 'index'],
data () {
return {
useiTunes: sharedStore.state.useiTunes
}
},
computed: { computed: {
correspondingSong () { correspondingSong () {
return songStore.guess(this.track.title, this.album) return songStore.guess(this.track.title, this.album)
@ -21,6 +36,10 @@ export default {
tooltip () { tooltip () {
return this.correspondingSong ? 'Click to play' : '' return this.correspondingSong ? 'Click to play' : ''
},
iTunesUrl () {
return `/api/itunes/song/${this.album.id}?q=${encodeURIComponent(this.track.title)}&jwt-token=${ls.get('jwt-token')}`
} }
}, },
@ -37,3 +56,25 @@ export default {
} }
} }
</script> </script>
<style lang="sass">
a.view-on-itunes {
display: inline-block;
border-radius: 3px;
font-size: .8rem;
padding: 0 5px;
color: #fff;
background: rgba(255, 255, 255, .1);
height: 20px;
line-height: 20px;
&:hover {
background: linear-gradient(27deg, #fe5c52 0%,#c74bd5 50%,#2daaff 100%);
color: #fff;
}
&:active {
box-shadow: inset 0px 5px 5px -5px #000;
}
}
</style>

View file

@ -5,7 +5,8 @@
@input="filter" @input="filter"
placeholder="Search" placeholder="Search"
v-model="q" v-model="q"
v-koel-focus="showing"> v-koel-focus
>
</div> </div>
</template> </template>

View file

@ -18,6 +18,7 @@ export const sharedStore = {
playlists: [], playlists: [],
useLastfm: false, useLastfm: false,
useYouTube: false, useYouTube: false,
useiTunes: true,
allowDownload: false, allowDownload: false,
currentVersion: '', currentVersion: '',
latestVersion: '', latestVersion: '',
@ -71,6 +72,8 @@ export const sharedStore = {
this.state.currentUser = null this.state.currentUser = null
this.state.playlists = [] this.state.playlists = []
this.state.useLastfm = false this.state.useLastfm = false
this.state.useYouTube = false
this.state.useiTunes = true
this.state.allowDownload = false this.state.allowDownload = false
this.state.currentVersion = '' this.state.currentVersion = ''
this.state.latestVersion = '' this.state.latestVersion = ''

View file

@ -0,0 +1,4 @@
@extends('errors.template')
@section('title', 'Not Found')
@section('details', $exception->getMessage())

View file

@ -1,47 +1,4 @@
<!DOCTYPE html> @extends('errors.template')
<html>
<head>
<title>Be right back.</title>
<link href="https://fonts.googleapis.com/css?family=Lato:100" rel="stylesheet" type="text/css"> @section('title', 'Service Unavailable')
@section('details', 'Koel will be right back.')
<style>
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">Be right back.</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>@yield('title')</title>
<style>
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
background: #181818;
display: table;
font-size: 24px;
font-family: system, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
color: #a0a0a0;
}
.container {
display: table-cell;
vertical-align: middle;
}
.content {
padding: 76px;
}
.title {
font-weight: 100;
font-size: 48px;
margin-bottom: 40px;
color: #fff;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">@yield('title')</div>
<div class="details">@yield('details')</div>
</div>
</div>
</body>
</html>

View file

@ -62,6 +62,11 @@ Route::group(['namespace' => 'API'], function () {
Route::get('album/{album}/info', 'AlbumController@getInfo'); Route::get('album/{album}/info', 'AlbumController@getInfo');
Route::get('artist/{artist}/info', 'ArtistController@getInfo'); Route::get('artist/{artist}/info', 'ArtistController@getInfo');
} }
// iTunes routes
if (iTunes::used()) {
Route::get('itunes/song/{album}', 'iTunesController@viewSong');
}
}); });
Route::group(['middleware' => 'os.auth', 'prefix' => 'os', 'namespace' => 'ObjectStorage'], function () { Route::group(['middleware' => 'os.auth', 'prefix' => 'os', 'namespace' => 'ObjectStorage'], function () {

View file

@ -0,0 +1,38 @@
{
"resultCount": 1,
"results": [
{
"wrapperType": "track",
"kind": "song",
"artistId": 163805,
"collectionId": 265611220,
"trackId": 265611396,
"artistName": "Skid Row",
"collectionName": "40 Seasons - The Best of Skid Row",
"trackName": "I Remember You",
"collectionCensoredName": "40 Seasons - The Best of Skid Row",
"trackCensoredName": "I Remember You",
"artistViewUrl": "https://itunes.apple.com/us/artist/skid-row/id163805?uo=4",
"collectionViewUrl": "https://itunes.apple.com/us/album/i-remember-you/id265611220?i=265611396&uo=4",
"trackViewUrl": "https://itunes.apple.com/us/album/i-remember-you/id265611220?i=265611396&uo=4",
"previewUrl": "http://audio.itunes.apple.com/apple-assets-us-std-000001/AudioPreview71/v4/fb/72/5d/fb725d9b-371f-1fe2-ba47-798f6058d4e0/mzaf_5805178457647444594.plus.aac.p.m4a",
"artworkUrl30": "http://is3.mzstatic.com/image/thumb/Music/v4/b3/5e/28/b35e28a2-2485-3124-721c-1948a9b99220/source/30x30bb.jpg",
"artworkUrl60": "http://is3.mzstatic.com/image/thumb/Music/v4/b3/5e/28/b35e28a2-2485-3124-721c-1948a9b99220/source/60x60bb.jpg",
"artworkUrl100": "http://is3.mzstatic.com/image/thumb/Music/v4/b3/5e/28/b35e28a2-2485-3124-721c-1948a9b99220/source/100x100bb.jpg",
"collectionPrice": 11.99,
"trackPrice": 1.29,
"releaseDate": "1998-11-03T08:00:00Z",
"collectionExplicitness": "notExplicit",
"trackExplicitness": "notExplicit",
"discCount": 1,
"discNumber": 1,
"trackCount": 16,
"trackNumber": 4,
"trackTimeMillis": 314027,
"country": "USA",
"currency": "USD",
"primaryGenreName": "Rock",
"isStreamable": true
}
]
}

25
tests/iTunesTest.php Normal file
View file

@ -0,0 +1,25 @@
<?php
use App\Services\iTunes;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
class iTunesTest extends TestCase
{
use WithoutMiddleware;
public function testGetTrackUrl()
{
$client = m::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'/blobs/itunes/track.json')),
]);
$api = new iTunes($client);
self::assertEquals(
'https://itunes.apple.com/us/album/i-remember-you/id265611220?i=265611396&uo=4&at=1000lsGu',
$api->getTrackUrl('Foo Bar')
);
}
}