mirror of
https://github.com/koel/koel
synced 2024-11-24 05:03:05 +00:00
Add iTunes integration
This commit is contained in:
parent
27793adefd
commit
86ca8d40f6
19 changed files with 386 additions and 51 deletions
13
app/Facades/iTunes.php
Normal file
13
app/Facades/iTunes.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class iTunes extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'iTunes';
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use App\Models\Interaction;
|
|||
use App\Models\Playlist;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use iTunes;
|
||||
use Lastfm;
|
||||
use YouTube;
|
||||
|
||||
|
@ -36,6 +37,7 @@ class DataController extends Controller
|
|||
'currentUser' => auth()->user(),
|
||||
'useLastfm' => Lastfm::used(),
|
||||
'useYouTube' => YouTube::enabled(),
|
||||
'useiTunes' => iTunes::used(),
|
||||
'allowDownload' => config('koel.download.allow'),
|
||||
'cdnUrl' => app()->staticUrl(),
|
||||
'currentVersion' => Application::KOEL_VERSION,
|
||||
|
|
28
app/Http/Controllers/API/iTunesController.php
Normal file
28
app/Http/Controllers/API/iTunesController.php
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
33
app/Http/Requests/API/ViewSongRequest.php
Normal file
33
app/Http/Requests/API/ViewSongRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
31
app/Providers/iTunesServiceProvider.php
Normal file
31
app/Providers/iTunesServiceProvider.php
Normal 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
75
app/Services/iTunes.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -157,6 +157,7 @@ return [
|
|||
App\Providers\YouTubeServiceProvider::class,
|
||||
App\Providers\DownloadServiceProvider::class,
|
||||
App\Providers\BroadcastServiceProvider::class,
|
||||
App\Providers\iTunesServiceProvider::class,
|
||||
|
||||
],
|
||||
|
||||
|
@ -214,6 +215,7 @@ return [
|
|||
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
|
||||
'AWS' => Aws\Laravel\AwsFacade::class,
|
||||
'Sentry' => Sentry\SentryLaravel\SentryFacade::class,
|
||||
'iTunes' => App\Facades\iTunes::class,
|
||||
|
||||
],
|
||||
|
||||
|
|
|
@ -110,4 +110,9 @@ return [
|
|||
*/
|
||||
'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
20
resources/assets/img/itunes.svg
Executable 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 |
|
@ -38,7 +38,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { playback } from '../../../services'
|
||||
import { sharedStore } from '../../../stores'
|
||||
import { playback, ls } from '../../../services'
|
||||
import trackListItem from '../../shared/track-list-item.vue'
|
||||
|
||||
export default {
|
||||
|
@ -47,7 +48,8 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
showingFullWiki: false
|
||||
showingFullWiki: false,
|
||||
useiTunes: sharedStore.state.useiTunes
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -64,6 +66,10 @@ export default {
|
|||
|
||||
showFull () {
|
||||
return this.mode === 'full' || this.showingFullWiki
|
||||
},
|
||||
|
||||
iTunesUrl () {
|
||||
return `/api/itunes/album/${this.album.id}&jwt-token=${ls.get('jwt-token')}`
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -2,18 +2,33 @@
|
|||
<li :class="{ available: correspondingSong }" :title="tooltip" @click="play">
|
||||
<span class="no">{{ index + 1 }}</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>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { songStore, queueStore } from '../../stores'
|
||||
import { playback } from '../../services'
|
||||
import { songStore, queueStore, sharedStore } from '../../stores'
|
||||
import { ls, playback } from '../../services'
|
||||
|
||||
export default {
|
||||
name: 'shared--track-list-item',
|
||||
props: ['album', 'track', 'index'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
useiTunes: sharedStore.state.useiTunes
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
correspondingSong () {
|
||||
return songStore.guess(this.track.title, this.album)
|
||||
|
@ -21,6 +36,10 @@ export default {
|
|||
|
||||
tooltip () {
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
@input="filter"
|
||||
placeholder="Search"
|
||||
v-model="q"
|
||||
v-koel-focus="showing">
|
||||
v-koel-focus
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ export const sharedStore = {
|
|||
playlists: [],
|
||||
useLastfm: false,
|
||||
useYouTube: false,
|
||||
useiTunes: true,
|
||||
allowDownload: false,
|
||||
currentVersion: '',
|
||||
latestVersion: '',
|
||||
|
@ -71,6 +72,8 @@ export const sharedStore = {
|
|||
this.state.currentUser = null
|
||||
this.state.playlists = []
|
||||
this.state.useLastfm = false
|
||||
this.state.useYouTube = false
|
||||
this.state.useiTunes = true
|
||||
this.state.allowDownload = false
|
||||
this.state.currentVersion = ''
|
||||
this.state.latestVersion = ''
|
||||
|
|
4
resources/views/errors/404.blade.php
Normal file
4
resources/views/errors/404.blade.php
Normal file
|
@ -0,0 +1,4 @@
|
|||
@extends('errors.template')
|
||||
|
||||
@section('title', 'Not Found')
|
||||
@section('details', $exception->getMessage())
|
|
@ -1,47 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Be right back.</title>
|
||||
@extends('errors.template')
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Lato:100" rel="stylesheet" type="text/css">
|
||||
|
||||
<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>
|
||||
@section('title', 'Service Unavailable')
|
||||
@section('details', 'Koel will be right back.')
|
||||
|
|
46
resources/views/errors/template.blade.php
Normal file
46
resources/views/errors/template.blade.php
Normal 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>
|
|
@ -62,6 +62,11 @@ Route::group(['namespace' => 'API'], function () {
|
|||
Route::get('album/{album}/info', 'AlbumController@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 () {
|
||||
|
|
38
tests/blobs/itunes/track.json
Normal file
38
tests/blobs/itunes/track.json
Normal 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
25
tests/iTunesTest.php
Normal 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')
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue