mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
Finish structure and song download
This commit is contained in:
parent
8333f38d6d
commit
5185f3dc6b
16 changed files with 325 additions and 32 deletions
|
@ -62,6 +62,12 @@ FFMPEG_PATH=/usr/local/bin/ffmpeg
|
|||
# but slower streaming and more bandwidth.
|
||||
OUTPUT_BIT_RATE=128
|
||||
|
||||
# Whether to allow song downloading.
|
||||
# Note that if you're downloading more than one song, Koel will zip them up
|
||||
# using PHP's ZipArchive. So if the module isn't available in the current
|
||||
# environment, such a download will (silently) fail.
|
||||
ALLOW_DOWNLOAD=true
|
||||
|
||||
|
||||
# The variables below are Laravel-specific.
|
||||
# You can change them if you know what you're doing. Otherwise, just leave them as-is.
|
||||
|
|
13
app/Facades/Download.php
Normal file
13
app/Facades/Download.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Download extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'Download';
|
||||
}
|
||||
}
|
|
@ -3,9 +3,8 @@
|
|||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller as BaseController;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
}
|
||||
|
|
0
app/Http/Controllers/API/Download/AlbumController.php
Normal file
0
app/Http/Controllers/API/Download/AlbumController.php
Normal file
0
app/Http/Controllers/API/Download/ArtistController.php
Normal file
0
app/Http/Controllers/API/Download/ArtistController.php
Normal file
10
app/Http/Controllers/API/Download/Controller.php
Normal file
10
app/Http/Controllers/API/Download/Controller.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Download;
|
||||
|
||||
use App\Http\Controllers\API\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
|
||||
}
|
0
app/Http/Controllers/API/Download/PlaylistController.php
Normal file
0
app/Http/Controllers/API/Download/PlaylistController.php
Normal file
24
app/Http/Controllers/API/Download/SongController.php
Normal file
24
app/Http/Controllers/API/Download/SongController.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Download;
|
||||
|
||||
use App\Http\Requests\API\Download\SongRequest;
|
||||
use App\Models\Song;
|
||||
use Download;
|
||||
|
||||
class SongController extends Controller
|
||||
{
|
||||
/**
|
||||
* Download a song or multiple songs.
|
||||
*
|
||||
* @param SongRequest $request
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public function download(SongRequest $request)
|
||||
{
|
||||
$songs = Song::whereIn('id', $request->songs)->get();
|
||||
|
||||
return response()->download(Download::from($songs));
|
||||
}
|
||||
}
|
18
app/Http/Requests/API/Download/Request.php
Normal file
18
app/Http/Requests/API/Download/Request.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\API\Download;
|
||||
|
||||
use App\Http\Requests\API\Request as BaseRequest;
|
||||
|
||||
class Request extends BaseRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return env('ALLOW_DOWNLOAD', true);
|
||||
}
|
||||
}
|
18
app/Http/Requests/API/Download/SongRequest.php
Normal file
18
app/Http/Requests/API/Download/SongRequest.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\API\Download;
|
||||
|
||||
class SongRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'songs' => 'required|array',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -29,24 +29,32 @@ Route::group(['prefix' => 'api', 'namespace' => 'API'], function () {
|
|||
Route::get('{song}/info', 'SongController@show');
|
||||
Route::put('songs', 'SongController@update');
|
||||
|
||||
// Interaction routes
|
||||
Route::post('interaction/play', 'InteractionController@play');
|
||||
Route::post('interaction/like', 'InteractionController@like');
|
||||
Route::post('interaction/batch/like', 'InteractionController@batchLike');
|
||||
Route::post('interaction/batch/unlike', 'InteractionController@batchUnlike');
|
||||
|
||||
// Playlist routes
|
||||
Route::resource('playlist', 'PlaylistController');
|
||||
Route::put('playlist/{playlist}/sync', 'PlaylistController@sync')->where(['playlist' => '\d+']);
|
||||
|
||||
// User and user profile routes
|
||||
Route::resource('user', 'UserController', ['only' => ['store', 'update', 'destroy']]);
|
||||
Route::put('me', 'ProfileController@update');
|
||||
|
||||
// Last.fm-related routes
|
||||
Route::get('lastfm/connect', 'LastfmController@connect');
|
||||
Route::post('lastfm/session-key', 'LastfmController@setSessionKey');
|
||||
|
||||
Route::get('lastfm/callback', [
|
||||
'as' => 'lastfm.callback',
|
||||
'uses' => 'LastfmController@callback',
|
||||
]);
|
||||
Route::delete('lastfm/disconnect', 'LastfmController@disconnect');
|
||||
|
||||
// Download routes
|
||||
Route::group(['prefix' => 'download', 'namespace' => 'Download'], function () {
|
||||
Route::get('songs', 'SongController@download');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
31
app/Providers/DownloadServiceProvider.php
Normal file
31
app/Providers/DownloadServiceProvider.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\Download;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class DownloadServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
app()->singleton('Download', function () {
|
||||
return new Download();
|
||||
});
|
||||
}
|
||||
}
|
113
app/Services/Download.php
Normal file
113
app/Services/Download.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Song;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Playlist;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Log;
|
||||
|
||||
class Download
|
||||
{
|
||||
/**
|
||||
* Generic method to generate a download archive from various source types.
|
||||
*
|
||||
* @param Song|Collection<Song>|Album|Artist|Playlist $mixed
|
||||
*
|
||||
* @return string Full path to the generated archive
|
||||
*/
|
||||
public function from($mixed)
|
||||
{
|
||||
if (is_a($mixed, Song::class)) {
|
||||
return $this->fromSong($mixed);
|
||||
} elseif (is_a($mixed, Collection::class)) {
|
||||
return $this->fromMultipleSongs($mixed);
|
||||
} elseif (is_a($mixed, Album::class)) {
|
||||
return $this->fromAlbum($mixed);
|
||||
} elseif (is_a($mixed, Artist::class)) {
|
||||
return $this->fromArtist($mixed);
|
||||
} elseif (is_a($mixed, Playlist::class)) {
|
||||
return $this->fromPlaylist($mixed);
|
||||
} else {
|
||||
throw new Exception('Unsupport download type.');
|
||||
}
|
||||
}
|
||||
|
||||
protected function fromSong(Song $song)
|
||||
{
|
||||
// Maybe more interesting things can be added in the future.
|
||||
// For now, we simply return the song's path.
|
||||
return $song->path;
|
||||
}
|
||||
|
||||
protected function fromMultipleSongs(Collection $songs)
|
||||
{
|
||||
if ($songs->count() === 1) {
|
||||
return $this->fromSong($songs->first());
|
||||
}
|
||||
|
||||
if (!class_exists('\ZipArchive')) {
|
||||
throw new Exception('Downloading multiple files requires ZipArchive module.');
|
||||
}
|
||||
|
||||
// Start gathering the songs into a zip file.
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
// We use system's temp dir instead storage_path() here, so that the generated files
|
||||
// can be cleaned up automatically after server reboot.
|
||||
$filename = rtrim(sys_get_temp_dir(), '/').'/koel-download-'.uniqid().'.zip';
|
||||
if ($zip->open($filename, \ZipArchive::CREATE) !== true) {
|
||||
throw new Exception('Cannot create zip file.');
|
||||
}
|
||||
|
||||
$localNames = [
|
||||
// 'duplicated-name.mp3' => currentFileIndex
|
||||
];
|
||||
|
||||
$songs->each(function ($s) use ($zip, &$localNames) {
|
||||
try {
|
||||
// We add all files into the zip archive as a flat structure.
|
||||
// As a result, there can be duplicate file names.
|
||||
// The following several lines are to make sure each file name is unique.
|
||||
$name = basename($s->path);
|
||||
if (array_key_exists($name, $localNames)) {
|
||||
$localNames[$name]++;
|
||||
$parts = explode('.', $name);
|
||||
$ext = $parts[count($parts) - 1];
|
||||
$parts[count($parts) - 1] = $localNames[$name].".$ext";
|
||||
$name = implode('.', $parts);
|
||||
} else {
|
||||
$localNames[$name] = 1;
|
||||
}
|
||||
|
||||
$zip->addFile($s->path, $name);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
});
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
protected function fromPlaylist(Playlist $playlist)
|
||||
{
|
||||
return $this->fromMultipleSongs($playlist->songs);
|
||||
}
|
||||
|
||||
protected function fromAlbum(Album $album)
|
||||
{
|
||||
return $this->fromMultipleSongs($abum->songs);
|
||||
}
|
||||
|
||||
protected function fromArtist(Artist $artist)
|
||||
{
|
||||
$songs = $artist->with('albums', 'albums.songs')->get();
|
||||
|
||||
return $this->fromMultipleSongs($songs);
|
||||
}
|
||||
}
|
|
@ -152,6 +152,7 @@ return [
|
|||
App\Providers\MediaServiceProvider::class,
|
||||
App\Providers\UtilServiceProvider::class,
|
||||
App\Providers\LastfmServiceProvider::class,
|
||||
App\Providers\DownloadServiceProvider::class,
|
||||
|
||||
],
|
||||
|
||||
|
@ -168,40 +169,41 @@ return [
|
|||
|
||||
'aliases' => [
|
||||
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||
'Config' => Illuminate\Support\Facades\Config::class,
|
||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||
'DB' => Illuminate\Support\Facades\DB::class,
|
||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||
'Event' => Illuminate\Support\Facades\Event::class,
|
||||
'File' => Illuminate\Support\Facades\File::class,
|
||||
'Gate' => Illuminate\Support\Facades\Gate::class,
|
||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||
'Log' => Illuminate\Support\Facades\Log::class,
|
||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||
'Password' => Illuminate\Support\Facades\Password::class,
|
||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
'Redis' => Illuminate\Support\Facades\Redis::class,
|
||||
'Request' => Illuminate\Support\Facades\Request::class,
|
||||
'Response' => Illuminate\Support\Facades\Response::class,
|
||||
'Route' => Illuminate\Support\Facades\Route::class,
|
||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||
'Session' => Illuminate\Support\Facades\Session::class,
|
||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||
'Config' => Illuminate\Support\Facades\Config::class,
|
||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||
'DB' => Illuminate\Support\Facades\DB::class,
|
||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||
'Event' => Illuminate\Support\Facades\Event::class,
|
||||
'File' => Illuminate\Support\Facades\File::class,
|
||||
'Gate' => Illuminate\Support\Facades\Gate::class,
|
||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||
'Log' => Illuminate\Support\Facades\Log::class,
|
||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||
'Password' => Illuminate\Support\Facades\Password::class,
|
||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
'Redis' => Illuminate\Support\Facades\Redis::class,
|
||||
'Request' => Illuminate\Support\Facades\Request::class,
|
||||
'Response' => Illuminate\Support\Facades\Response::class,
|
||||
'Route' => Illuminate\Support\Facades\Route::class,
|
||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||
'Session' => Illuminate\Support\Facades\Session::class,
|
||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
'Media' => App\Facades\Media::class,
|
||||
'Util' => App\Facades\Util::class,
|
||||
'Lastfm' => App\Facades\Lastfm::class,
|
||||
'Download' => App\Facades\Download::class,
|
||||
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
|
||||
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li v-if="isAdmin" @click="openEditForm">Edit</li>
|
||||
<li @click="download">Download</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
@ -40,6 +41,7 @@
|
|||
import userStore from '../../stores/user';
|
||||
import playlistStore from '../../stores/playlist';
|
||||
import playback from '../../services/playback';
|
||||
import download from '../../services/download';
|
||||
|
||||
export default {
|
||||
props: ['songs'],
|
||||
|
@ -122,6 +124,11 @@
|
|||
|
||||
this.close();
|
||||
},
|
||||
|
||||
download() {
|
||||
download.fromSongs(this.songs);
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
44
resources/assets/js/services/download.js
Normal file
44
resources/assets/js/services/download.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import $ from 'jquery';
|
||||
import { map } from 'lodash';
|
||||
|
||||
import playlistStore from '../stores/playlist';
|
||||
import ls from './ls';
|
||||
|
||||
export default {
|
||||
fromSongs(songs) {
|
||||
const ids = map(songs, 'id');
|
||||
const params = $.param({ songs: ids });
|
||||
|
||||
return this.trigger(`songs?${params}`);
|
||||
},
|
||||
|
||||
fromAlbum(album) {
|
||||
|
||||
},
|
||||
|
||||
fromArtist(artist) {
|
||||
|
||||
},
|
||||
|
||||
fromPlaylist(playlist) {
|
||||
if (!playlistStore.getSongs(playlist).length) {
|
||||
console.warn('Empty playlist.');
|
||||
return;
|
||||
}
|
||||
|
||||
return this.trigger(`playlist/${playlist.id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a download link using a segment and trigger it.
|
||||
*
|
||||
* @param {string} uri The uri segment, corresponding to the song(s),
|
||||
* artist, playlist, or album.
|
||||
*/
|
||||
trigger(uri) {
|
||||
const sep = uri.indexOf('?') === -1 ? '?' : '&';
|
||||
const frameId = `downloader${Date.now()}`;
|
||||
$(`<iframe id="${frameId}" style="display:none"></iframe`).appendTo('body');
|
||||
document.getElementById(frameId).src = `/api/download/${uri}${sep}jwt-token=${ls.get('jwt-token')}`;
|
||||
},
|
||||
}
|
Loading…
Reference in a new issue