This commit is contained in:
An Phan 2015-12-13 12:42:28 +08:00
parent 32f56ce24b
commit 0ee372882c
206 changed files with 15082 additions and 0 deletions

3
.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*.{js,sass,scss,json,coffee,vue}]
indent_style = space
indent_size = 4

29
.env.example Normal file
View file

@ -0,0 +1,29 @@
APP_ENV=local
APP_DEBUG=true
APP_KEY=SomeRandomString
# Username and passsword for the initial admin account
# This info will be populated into the database during `php artisan db:seed`
# After that, it can (and should) be removed from this .env file
ADMIN_EMAIL=
ADMIN_NAME=
ADMIN_PASSWORD=
# The maximum scan time, in seconds. Increase this if you have a huge library.
APP_MAX_SCAN_TIME=600
DB_HOST=localhost
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
* text=auto
*.css linguist-vendored
*.less linguist-vendored

75
.gitignore vendored Normal file
View file

@ -0,0 +1,75 @@
/vendor
/node_modules
Homestead.yaml
Homestead.json
.env
/public
/.idea
/_ide_helper.php
/config.testing
/config.local
db.mwb.bak
bower_components
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Sass ###
.sass-cache/
*.css.map

41
.htaccess Normal file
View file

@ -0,0 +1,41 @@
<IfModule mod_headers.c>
<FilesMatch "\.(eot|font.css|otf|ttc|ttf|woff)$">
Header set Access-Control-Allow-Origin "*"
</FilesMatch>
</IfModule>
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
RewriteEngine On
RewriteBase /
# Deny access to framework directories
RewriteRule ^(app/|bootstrap/|config/|database/|resources/|storage/tests|vendor/) - [R=404,L,NC]
# And dot files/folders (for example .env)
RedirectMatch 404 /\..*$
# Redirect Trailing Slashes...
RewriteRule ^(.*)/$ /$1 [L,R=301]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
<IfModule mod_deflate.c>
# Disable deflation for media files.
SetEnvIfNoCase Request_URI "^/api/play/" no-gzip dont-vary
</IfModule>
<IfModule mod_xsendfile.c>
# Set a MOD_X_SENDFILE_ENABLED env variable for PHP to use later.
<Files *.php>
XSendFile On
SetEnv MOD_X_SENDFILE_ENABLED 1
</Files>
</IfModule>

6
.jshintrc Normal file
View file

@ -0,0 +1,6 @@
{
"globals": { "$": false },
"globalstrict": false,
"devel": true,
"esnext": true
}

15
.travis.yml Normal file
View file

@ -0,0 +1,15 @@
language: php
php:
- 5.6
- 7.0
branches:
only:
- master
before_script:
- curl -s http://getcomposer.org/installer | php
- php composer.phar install
script: phpunit

35
app/Application.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace App;
use Illuminate\Foundation\Application as IlluminateApplication;
use InvalidArgumentException;
/**
* Extends \Illuminate\Foundation\Application to override some defaults.
*/
class Application extends IlluminateApplication
{
/**
* Loads a revision'ed asset file, making use of gulp-rev
* This is a copycat of L5's Elixir, but catered to our directory structure.
*
* @param string $file
*
* @return string
*/
public function rev($file)
{
static $manifest = null;
if (is_null($manifest)) {
$manifest = json_decode(file_get_contents($this->publicPath().'/build/rev-manifest.json'), true);
}
if (isset($manifest[$file])) {
return "/public/build/{$manifest[$file]}";
}
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
use Media;
class SyncMedia extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'koel:sync';
protected $ignored = 0;
protected $invalid = 0;
protected $synced = 0;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync songs found in configured directory against the database.';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!Setting::get('media_path')) {
$this->error("Media path hasn't been configured. Exiting.");
return;
}
$this->info('Koel syncing started. All we need now is just a little patience…');
Media::sync(null, $this);
$this->output->writeln("<info>Completed! {$this->synced} new or updated songs(s)</info>, "
."{$this->ignored} unchanged song(s), "
."and <comment>{$this->invalid} invalid file(s)</comment>.");
}
/**
* Log a song's sync status to console.
*/
public function logToConsole($path, $result)
{
$name = basename($path);
if ($result === true) {
if ($this->option('verbose')) {
$this->line("$name has no changes  ignoring");
}
++$this->ignored;
} elseif ($result === false) {
if ($this->option('verbose')) {
$this->error("$name is not a valid media file");
}
++$this->invalid;
} else {
if ($this->option('verbose')) {
$this->info("$name synced");
}
++$this->synced;
}
}
}

28
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,28 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
\App\Console\Commands\SyncMedia::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
}
}

8
app/Events/Event.php Normal file
View file

@ -0,0 +1,8 @@
<?php
namespace App\Events;
abstract class Event
{
//
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
HttpException::class,
ModelNotFoundException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $e
* @return void
*/
public function report(Exception $e)
{
return parent::report($e);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
}
return parent::render($request, $e);
}
}

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

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

View file

@ -0,0 +1,9 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller as BaseController;
abstract class Controller extends BaseController
{
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\API;
use App\Models\Artist;
use App\Models\Interaction;
use App\Models\Playlist;
use App\Models\Setting;
use App\Models\Song;
use App\Models\User;
class DataController extends Controller
{
/**
* Get a set of application data.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$playlists = Playlist::byCurrentUser()->orderBy('name')->with('songs')->get()->toArray();
// We don't need full song data, just ID's
foreach ($playlists as &$playlist) {
$playlist['songs'] = array_pluck($playlist['songs'], 'id');
}
return response()->json([
'artists' => Artist::orderBy('name')->with('albums', with('albums.songs'))->get(),
'settings' => Setting::all()->lists('value', 'key'),
'playlists' => $playlists,
'interactions' => Interaction::byCurrentUser()->get(),
'users' => auth()->user()->is_admin ? User::all() : [],
'user' => auth()->user(),
]);
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\API;
use App\Models\Interaction;
use App\Http\Requests\API\BatchInteractionRequest;
use Illuminate\Http\Request;
class InteractionController extends Controller
{
/**
* Increase a song's play count as the currently authenticated user.
*
* @param Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function play(Request $request)
{
return response()->json(Interaction::increasePlayCount($request->input('id')));
}
/**
* Like or unlike a song as the currently authenticated user.
*
* @param Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function like(Request $request)
{
return response()->json(Interaction::toggleLike($request->input('id')));
}
/**
* Like several songs at once as the currently authenticated user.
*
* @param BatchInteractionRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function batchLike(BatchInteractionRequest $request)
{
return response()->json(Interaction::batchLike((array) $request->input('ids')));
}
/**
* Unlike several songs at once as the currently authenticated user.
*
* @param BatchInteractionRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function batchUnlike(BatchInteractionRequest $request)
{
return response()->json(Interaction::batchUnlike((array) $request->input('ids')));
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\API;
use Illuminate\Http\Request;
use App\Models\Playlist;
use App\Http\Requests\API\PlaylistStoreRequest;
class PlaylistController extends Controller
{
/**
* Create a new playlist.
*
* @param PlaylistStoreRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(PlaylistStoreRequest $request)
{
$playlist = auth()->user()->playlists()->create($request->only('name'));
$playlist->songs()->sync($request->input('songs'));
$playlist->songs = $playlist->songs->fetch('id');
return response()->json($playlist);
}
/**
* Rename a playlist.
*
* @param \Illuminate\Http\Request $request
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function update(Request $request, $id)
{
$playlist = Playlist::findOrFail($id);
if ($playlist->user_id !== auth()->user()->id) {
abort(403);
}
$playlist->name = $request->input('name');
$playlist->save();
return response()->json($playlist);
}
/**
* Sync a playlist with songs.
* Any songs that are not populated here will be removed from the playlist.
*
* @param \Illuminate\Http\Request $request
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function sync(Request $request, $id)
{
$playlist = Playlist::findOrFail($id);
if ($playlist->user_id !== auth()->user()->id) {
abort(403);
}
$playlist->songs()->sync($request->input('songs'));
return response()->json();
}
/**
* Delete a playlist.
*
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
// This can't be put into a Request authorize(), due to Laravel(?)'s limitation.
if (Playlist::findOrFail($id)->user_id !== auth()->user()->id) {
abort(403);
}
Playlist::destroy($id);
return response()->json();
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\API;
use App\Facades\Media;
use App\Http\Requests\API\SettingRequest;
use App\Models\Setting;
class SettingController extends Controller
{
/**
* Save the application settings.
*
* @param SettingRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function save(SettingRequest $request)
{
// For right now there's only one setting to be saved
Setting::set('media_path', rtrim(trim($request->input('media_path')), '/'));
// In a next version we should opt for a "MediaPathChanged" event,
// but let's just do this async now.
Media::sync();
return response()->json();
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Streamers\XSendFileStreamer;
use App\Http\Streamers\PHPStreamer;
use App\Models\Song;
class SongController extends Controller
{
/**
* Play a song.
* As of current Koel supports two streamer: x_sendfile and native PHP readfile.
*
* @param $id
*/
public function play($id)
{
if (env('MOD_X_SENDFILE_ENABLED') ||
(function_exists('apache_get_modules') && in_array('mod_xsendfile', apache_get_modules()))) {
(new XSendFileStreamer($id))->stream();
return;
}
(new PHPStreamer($id))->stream();
// Exit here to avoid accidentally sending extra content at the end of the file.
exit;
}
/**
* Get the lyrics of a song.
*
* @param $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function getLyrics($id)
{
return response()->json(Song::findOrFail($id)->lyrics);
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\API;
use App\Models\User;
use App\Http\Requests\API\UserStoreRequest;
use App\Http\Requests\API\UserUpdateRequest;
use App\Http\Requests\API\ProfileUpdateRequest;
use Hash;
class UserController extends Controller
{
/**
* Create a new user.
*
* @param UserStoreRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(UserStoreRequest $request)
{
return response()->json(User::create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'password' => Hash::make($request->input('password')),
]));
}
/**
* Update a user.
*
* @param UserUpdateRequest $request
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function update(UserUpdateRequest $request, $id)
{
$data = $request->only('name', 'email');
if ($password = $request->input('password')) {
$data['password'] = Hash::make($password);
}
return response()->json(User::findOrFail($id)->update($data));
}
/**
* Delete a user.
*
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
if (!auth()->user()->is_admin || auth()->user()->id === $id) {
abort(403);
}
return response()->json(User::destroy($id));
}
/**
* Update the current user's profile.
*
* @param ProfileUpdateRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function updateProfile(ProfileUpdateRequest $request)
{
$data = $request->only('name', 'email');
if ($password = $request->input('password')) {
$data['password'] = Hash::make($password);
}
return response()->json(auth()->user()->update($data));
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class AuthController extends Controller
{
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
|
*/
protected $redirectPath = '/♫';
protected $loginPath = '/login';
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
/**
* Create a new authentication controller instance.
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'getLogout']);
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
*
* @return User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Create a new password controller instance.
*/
public function __construct()
{
$this->middleware('guest');
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
abstract class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

42
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* @var array
*/
protected $middleware = [
CheckForMaintenanceMode::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
];
/**
* The application's route middleware.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'guest' => RedirectIfAuthenticated::class,
];
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax() || $request->route()->getName() == 'play') {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('login');
}
}
return $next($request);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
class EncryptCookies extends BaseEncrypter
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class RedirectIfAuthenticated
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
return redirect('/♫');
}
return $next($request);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\API;
class BatchInteractionRequest extends Request
{
/**
* 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 [
'ids' => 'required|array',
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\API;
class PlaylistStoreRequest extends Request
{
/**
* 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 [
'name' => 'required',
'songs' => 'array',
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\API;
class ProfileUpdateRequest extends Request
{
/**
* 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 [
'name' => 'required',
'email' => 'required|email|unique:users,email,'.auth()->user()->id,
];
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Requests\API;
use App\Http\Requests\Request as BaseRequest;
class Request extends BaseRequest
{
//
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\API;
class SettingRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user()->is_admin;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'media_path' => 'string|required|valid_path',
];
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\API;
class UserStoreRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user()->is_admin;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required'
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\API;
class UserUpdateRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user()->is_admin;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'email' => 'required|email|unique:users,email,'.$this->route('user'),
];
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
abstract class Request extends FormRequest
{
//
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Streamers;
use App\Models\Song;
class BaseStreamer
{
/**
* @var Song|string
*/
protected $song;
/**
* @var string
*/
protected $contentType;
/**
* BaseStreamer constructor.
*
* @param $song Song|string A Song object, or its ID.
*/
public function __construct($song)
{
$this->song = $song instanceof Song ? $song : Song::findOrFail($song);
if (!file_exists($this->song->path)) {
abort(404);
}
// Hard code the content type instead of relying on PHP's fileinfo()
// or even Symfony's MIMETypeGuesser, since they appear to be wrong sometimes.
$this->contentType = 'audio/'.pathinfo($this->song->path, PATHINFO_EXTENSION);
// Turn off error reporting to make sure our stream isn't interfered.
@error_reporting(0);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Http\Streamers;
class PHPStreamer extends BaseStreamer implements StreamerInterface
{
public function __construct($id)
{
parent::__construct($id);
}
/**
* Stream the current song using the most basic PHP method: readfile()
* Credits: DaveRandom @ http://stackoverflow.com/a/4451376/794641.
*/
public function stream()
{
// Get the 'Range' header if one was sent
if (isset($_SERVER['HTTP_RANGE'])) {
// IIS/Some Apache versions
$range = $_SERVER['HTTP_RANGE'];
} elseif (function_exists('apache_request_headers') && $apache = apache_request_headers()) {
// Try Apache again
$headers = [];
foreach ($apache as $header => $val) {
$headers[strtolower($header)] = $val;
}
$range = isset($headers['range']) ? $headers['range'] : false;
} else {
// We can't get the header/there isn't one set
$range = false;
}
// Get the data range requested (if any)
$fileSize = filesize($this->song->path);
if ($range) {
$partial = true;
list($param, $range) = explode('=', $range);
if (strtolower(trim($param)) != 'bytes') {
// Bad request - range unit is not 'bytes'
abort(400);
}
$range = explode(',', $range);
$range = explode('-', $range[0]); // We only deal with the first requested range
if (count($range) != 2) {
// Bad request - 'bytes' parameter is not valid
abort(400);
}
if ($range[0] === '') {
// First number missing, return last $range[1] bytes
$end = $fileSize - 1;
$start = $end - intval($range[0]);
} elseif ($range[1] === '') {
// Second number missing, return from byte $range[0] to end
$start = intval($range[0]);
$end = $fileSize - 1;
} else {
// Both numbers present, return specific range
$start = intval($range[0]);
$end = intval($range[1]);
if ($end >= $fileSize || (!$start && (!$end || $end == ($fileSize - 1)))) {
// Invalid range/whole file specified, return whole file
$partial = false;
}
}
$length = $end - $start + 1;
} else {
// No range requested
$partial = false;
}
// Send standard headers
header("Content-Type: {$this->contentType}");
header("Content-Length: $fileSize");
header('Content-Disposition: attachment; filename="'.basename($this->song->path).'"');
header('Accept-Ranges: bytes');
// if requested, send extra headers and part of file...
if ($partial) {
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$fileSize");
if (!$fp = fopen($this->song->path, 'r')) {
// Error out if we can't read the file
abort(500);
}
if ($start) {
fseek($fp, $start);
}
while ($length) {
// Read in blocks of 8KB so we don't chew up memory on the server
$read = ($length > 8192) ? 8192 : $length;
$length -= $read;
print(fread($fp, $read));
}
fclose($fp);
} else {
// ...otherwise just send the whole file
readfile($this->song->path);
}
exit;
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Http\Streamers;
interface StreamerInterface
{
/**
* Stream the current song.
*/
public function stream();
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Http\Streamers;
class XSendFileStreamer extends BaseStreamer implements StreamerInterface
{
public function __construct($id)
{
parent::__construct($id);
}
/**
* Stream the current song using Apache's x_sendfile module.
*/
public function stream()
{
header("X-Sendfile: {$this->song->path}");
header("Content-Type: {$this->contentType}");
header('Content-Disposition: inline; filename="'.basename($this->song->path).'"');
exit;
}
}

38
app/Http/routes.php Normal file
View file

@ -0,0 +1,38 @@
<?php
get('login', 'Auth\AuthController@getLogin');
post('login', 'Auth\AuthController@postLogin');
get('logout', 'Auth\AuthController@getLogout');
get('/', function () {
return redirect('/♫');
});
get('♫', ['middleware' => 'auth', function () {
return view('index');
}]);
Route::group(['prefix' => 'api', 'middleware' => 'auth', 'namespace' => 'API'], function () {
get('/', function () {
// Just acting as a ping service.
});
get('data', 'DataController@index');
post('settings', 'SettingController@save');
get('{id}/play', 'SongController@play')->where('id', '[a-f0-9]{32}');
get('{id}/lyrics', 'SongController@getLyrics')->where('id', '[a-f0-9]{32}');
post('interaction/play', 'InteractionController@play');
post('interaction/like', 'InteractionController@like');
post('interaction/batch/like', 'InteractionController@batchLike');
post('interaction/batch/unlike', 'InteractionController@batchUnlike');
resource('playlist', 'PlaylistController');
put('playlist/{id}/sync', 'PlaylistController@sync')->where(['id' => '\d+']);
resource('user', 'UserController');
put('me', 'UserController@updateProfile');
});

21
app/Jobs/Job.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "onQueue" and "delay" queue helper methods.
|
*/
use Queueable;
}

0
app/Listeners/.gitkeep Normal file
View file

109
app/Models/Album.php Normal file
View file

@ -0,0 +1,109 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property string cover The path to the album's cover
* @property bool has_cover If the album has a cover image
* @property int id
*/
class Album extends Model
{
const UNKNOWN_ID = 1;
const UNKNOWN_NAME = 'Unknown Album';
const UNKNOWN_COVER = 'unknown-album.png';
protected $guarded = ['id'];
protected $hidden = ['created_at', 'updated_at'];
public function artist()
{
return $this->belongsTo(Artist::class);
}
public function songs()
{
return $this->hasMany(Song::class);
}
/**
* Get an album using some provided information.
*
* @param Artist $artist
* @param $name
*
* @return self
*/
public static function get(Artist $artist, $name)
{
// If an empty name is provided, turn it into our "Unknown Album"
$name = $name ?: self::UNKNOWN_NAME;
$album = self::firstOrCreate([
'artist_id' => $artist->id,
'name' => $name,
]);
return $album;
}
/**
* Generate a cover from provided data.
*
* @param array $cover The cover data in array format, extracted by getID3.
* For example:
* [
* 'data' => '<binary data>',
* 'image_mime' => 'image/png',
* 'image_width' => 512,
* 'image_height' => 512,
* 'imagetype' => 'PNG', // not always present
* 'picturetype' => 'Other',
* 'description' => '',
* 'datalength' => 7627,
* ]
*/
public function generateCover(array $cover)
{
$extension = explode('/', $cover['image_mime']);
$fileName = uniqid().'.'.strtolower($extension[1]);
$coverPath = app()->publicPath().'/img/covers/'.$fileName;
file_put_contents($coverPath, $cover['data']);
$this->update(['cover' => $fileName]);
}
public function setCoverAttribute($value)
{
$this->attributes['cover'] = $value ?: self::UNKNOWN_COVER;
}
public function getCoverAttribute($value)
{
return '/public/img/covers/'.($value ?: self::UNKNOWN_COVER);
}
/**
* Determine if the current album has a cover.
*
* @return bool
*/
public function getHasCoverAttribute()
{
return $this->cover !== $this->getCoverAttribute(null);
}
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
*
* @param $value
*/
public function setNameAttribute($value)
{
$this->attributes['name'] = html_entity_decode($value);
}
}

54
app/Models/Artist.php Normal file
View file

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int id The model ID
*/
class Artist extends Model
{
const UNKNOWN_ID = 1;
const UNKNOWN_NAME = 'Unknown Artist';
protected $guarded = ['id'];
protected $hidden = ['created_at', 'updated_at'];
public function albums()
{
return $this->hasMany(Album::class);
}
public function getNameAttribute($value)
{
return $value ?: self::UNKNOWN_NAME;
}
/**
* Get an Artist object from their name.
* If such is not found, a new artist will be created.
*
* @param string $name
*
* @return Artist
*/
public static function get($name)
{
$name = trim($name) ?: self::UNKNOWN_NAME;
return self::firstOrCreate(compact('name'), compact('name'));
}
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
*
* @param $value
*/
public function setNameAttribute($value)
{
$this->attributes['name'] = html_entity_decode($value);
}
}

132
app/Models/Interaction.php Normal file
View file

@ -0,0 +1,132 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\CanFilterByUser;
use DB;
/**
* @property bool liked
* @property int play_count
*/
class Interaction extends Model
{
use CanFilterByUser;
protected $casts = [
'liked' => 'boolean',
'play_count' => 'integer',
];
protected $guarded = ['id'];
protected $hidden = ['user_id', 'created_at', 'updated_at'];
public function user()
{
return $this->belongsTo(User::class);
}
public function song()
{
return $this->belongsTo(Song::class);
}
/**
* Increase the number of times a song is played by a user.
*
* @param string $songId
* @param int|null $userId
*
* @return Interaction
*/
public static function increasePlayCount($songId, $userId = null)
{
$interaction = self::firstOrCreate([
'song_id' => $songId,
'user_id' => $userId ?: auth()->user()->id,
]);
if (!$interaction->exists) {
$interaction->liked = false;
}
++$interaction->play_count;
$interaction->save();
return $interaction;
}
/**
* Like or unlike a song on behalf of a user.
*
* @param string $songId
* @param int|null $userId
*
* @return Interaction
*/
public static function toggleLike($songId, $userId = null)
{
$interaction = self::firstOrCreate([
'song_id' => $songId,
'user_id' => $userId ?: auth()->user()->id,
]);
if (!$interaction->exists) {
$interaction->play_count = 0;
}
$interaction->liked = !$interaction->liked;
$interaction->save();
return $interaction;
}
/**
* Like several songs at once.
*
* @param array $songIds
* @param int|null $userId
*
* @return array
*/
public static function batchLike(array $songIds, $userId = null)
{
$result = [];
foreach ($songIds as $songId) {
$interaction = self::firstOrCreate([
'song_id' => $songId,
'user_id' => $userId ?: auth()->user()->id,
]);
if (!$interaction->exists) {
$interaction->play_count = 0;
}
$interaction->liked = true;
$interaction->save();
$result[] = $interaction;
}
return $result;
}
/**
* Unlike several songs at once.
*
* @param array $songIds
* @param int|null $userId
*
* @return int
*/
public static function batchUnlike(array $songIds, $userId = null)
{
return DB::table('interactions')
->whereIn('song_id', $songIds)
->where('user_id', $userId ?: auth()->user()->id)
->update(['liked' => false]);
}
}

29
app/Models/Playlist.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\CanFilterByUser;
class Playlist extends Model
{
use CanFilterByUser;
protected $hidden = ['user_id', 'created_at', 'updated_at'];
protected $guarded = ['id'];
protected $casts = [
'user_id' => 'int',
];
public function songs()
{
return $this->belongsToMany(Song::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

69
app/Models/Setting.php Normal file
View file

@ -0,0 +1,69 @@
<?php
namespace App\Models;
use App\Facades\Media;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $primaryKey = 'key';
public $timestamps = false;
protected $guarded = [];
/**
* Get a setting value.
*
* @param string $key
*
* @return mixed
*/
public static function get($key)
{
if ($record = self::find($key)) {
return $record->value;
}
return '';
}
/**
* Set a setting (no pun) value.
*
* @param string|array $key The key of the setting, or an associative array of settings,
* in which case $value will be discarded.
* @param mixed $value
*
*/
public static function set($key, $value = null)
{
if (is_array($key)) {
foreach ($key as $k => $v) {
self::set($k, $v);
}
return;
}
self::updateOrCreate(compact('key'), compact('value'));
}
/**
* Serialize the setting value before saving into the database.
* This makes settings more flexible.
*
* @param mixed $value
*/
public function setValueAttribute($value)
{
$this->attributes['value'] = serialize($value);
}
public function getValueAttribute($value)
{
return unserialize($value);
}
}

68
app/Models/Song.php Normal file
View file

@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property string path
*/
class Song extends Model
{
protected $guarded = [];
/**
* Attributes to be hidden from JSON outputs.
* Here we specify to hide lyrics as well to save some bandwidth (actually, lots of it).
* Lyrics can then be queried on demand.
*
* @var array
*/
protected $hidden = ['lyrics', 'created_at', 'updated_at', 'path', 'mtime'];
public function album()
{
return $this->belongsTo(Album::class);
}
public function playlists()
{
return $this->belongsToMany(Playlist::class);
}
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
*
* @param $value
*/
public function setTitleAttribute($value)
{
$this->attributes['title'] = html_entity_decode($value);
}
/**
* Some songs don't have a title.
* Fall back to the file name (without extension) for such.
*
* @param $value
*
* @return string
*/
public function getTitleAttribute($value)
{
return $value ?: pathinfo($this->path, PATHINFO_FILENAME);
}
/**
* Prepare the lyrics for displaying.
*
* @param $value
*
* @return string
*/
public function getLyricsAttribute($value)
{
return nl2br($value);
}
}

53
app/Models/User.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are protected from mass assign.
*
* @var array
*/
protected $guarded = ['id'];
protected $casts = [
'is_admin' => 'bool',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token', 'created_at', 'updated_at' ];
public function playlists()
{
return $this->hasMany(Playlist::class);
}
public function interactions()
{
return $this->hasMany(Interaction::class);
}
}

0
app/Policies/.gitkeep Normal file
View file

View file

@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Add some custom validation rules
Validator::extend('valid_path', function($attribute, $value, $parameters, $validator) {
return (is_dir($value) && is_readable($value));
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Providers;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any application authentication / authorization services.
*
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
//
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Providers;
use App\Facades\Media;
use App\Models\Song;
use App\Models\Album;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\SomeEvent' => [
'App\Listeners\EventListener',
],
];
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
// Generate a unique hash for a song from its path to be the ID
Song::creating(function ($song) {
$song->id = Media::getHash($song->path);
});
// Remove the cover file if the album is deleted
Album::deleted(function ($album) {
if ($album->hasCover) {
@unlink(app()->publicPath().'/img/covers/'.$album->cover);
}
});
}
}

View file

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

View file

@ -0,0 +1,44 @@
<?php
namespace App\Providers;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to the controller routes in your routes file.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = 'App\Http\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Router $router)
{
//
parent::boot($router);
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function map(Router $router)
{
$router->group(['namespace' => $this->namespace], function ($router) {
require app_path('Http/routes.php');
});
}
}

231
app/Services/Media.php Normal file
View file

@ -0,0 +1,231 @@
<?php
namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Setting;
use App\Models\Song;
use getID3;
use getid3_lib;
use Exception;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Finder\Finder;
use App\Console\Commands\SyncMedia;
class Media
{
/**
* @var getID3
*/
protected $getID3;
protected $guarded = [];
public function __construct()
{
$this->setGetID3();
}
/**
* Sync the media. Oh sync the media.
*
* @param string|null $path
* @param SyncMedia $syncCommand The SyncMedia command object, to log to console if executed by artisan.
*/
public function sync($path = null, SyncMedia $syncCommand = null)
{
set_time_limit(env('APP_MAX_SCAN_TIME', 600));
$path = $path ?: Setting::get('media_path');
$results = [
'good' => [], // Updated or added files
'bad' => [], // Bad files
'ugly' => [], // Unmodified files
];
// For now we only care about mp3 and ogg files.
// Support for other formats (AAC?) may be added in the future.
$files = Finder::create()->files()->name('/\.(mp3|ogg)$/')->in($path);
foreach ($files as $file) {
$song = $this->syncFile($file);
if ($song === true) {
$results['ugly'][] = $file;
} elseif ($song === false) {
$results['bad'][] = $file;
} else {
$results['good'][] = $file;
}
if ($syncCommand) {
$syncCommand->logToConsole($file->getPathname(), $song);
}
}
// Delete non-existing songs.
$hashes = array_map(function ($f) {
return $this->getHash($f->getPathname());
}, array_merge($results['ugly'], $results['good']));
Song::whereNotIn('id', $hashes)->delete();
// Empty albums and artists should be gone as well.
$inUseAlbums = Song::select('album_id')->groupBy('album_id')->get()->lists('album_id');
$inUseAlbums[] = Album::UNKNOWN_ID;
Album::whereNotIn('id', $inUseAlbums)->delete();
$inUseArtists = Album::select('artist_id')->groupBy('artist_id')->get()->lists('artist_id');
$inUseArtists[] = Artist::UNKNOWN_ID;
Artist::whereNotIn('id', $inUseArtists)->delete();
}
/**
* Sync a song with all available media info against the database.
*
* @param SplFileInfo $file The SplFileInfo instance of the file.
*
* @return bool|Song A Song object on success,
* true if file existing but unmodified,
* or false on error.
*/
public function syncFile(SplFileInfo $file)
{
if (!$info = $this->getInfo($file)) {
return false;
}
if (!$this->isNewOrChanged($file)) {
return true;
}
$artist = Artist::get($info['artist']);
$album = Album::get($artist, $info['album']);
if ($info['cover'] && !$album->has_cover) {
try {
$album->generateCover($info['cover']);
} catch (Exception $e) {
Log::error($e);
}
}
$info['album_id'] = $album->id;
unset($info['artist']);
unset($info['album']);
unset($info['cover']);
$song = Song::updateOrCreate(['id' => $this->getHash($file->getPathname())], $info);
$song->save();
return $song;
}
/**
* Check if a media file is new or changed.
* A file is considered existing and unchanged only when:
* - its hash (ID) can be found in the database, and
* - its last modified time is the same with that of the comparing file.
*
* @param SplFileInfo $file
*
* @return bool
*/
protected function isNewOrChanged(SplFileInfo $file)
{
return !Song::whereIdAndMtime($this->getHash($file->getPathname()), $file->getMTime())->count();
}
/**
* Get ID3 info from a file.
*
* @param SplFileInfo $file
*
* @return array|null
*/
public function getInfo(SplFileInfo $file)
{
$info = $this->getID3->analyze($file->getPathname());
if (isset($info['error'])) {
return;
}
// Copy the available tags over to comment.
// This is a helper from getID3, though it doesn't really work well.
// We'll still prefer getting ID3v2 tags directly later.
// Read on.
getid3_lib::CopyTagsToComments($info);
$props = [
'artist' => '',
'album' => '',
'title' => '',
'length' => $info['playtime_seconds'],
'lyrics' => '',
'cover' => array_get($info, 'comments.picture', [null])[0],
'path' => $file->getPathname(),
'mtime' => $file->getMTime(),
];
if (!$comments = array_get($info, 'comments_html')) {
return $props;
}
// We prefer id3v2 tags over others.
if (!$artist = array_get($info, 'tags.id3v2.artist', [null])[0]) {
$artist = array_get($comments, 'artist', [''])[0];
}
if (!$album = array_get($info, 'tags.id3v2.album', [null])[0]) {
$album = array_get($comments, 'album', [''])[0];
}
if (!$title = array_get($info, 'tags.id3v2.title', [null])[0]) {
$title = array_get($comments, 'title', [''])[0];
}
if (!$lyrics = array_get($info, 'tags.id3v2.unsynchronised_lyric', [null])[0]) {
$lyrics = array_get($comments, 'unsynchronised_lyric', [''])[0];
}
$props['artist'] = trim($artist);
$props['album'] = trim($album);
$props['title'] = trim($title);
$props['lyrics'] = trim($lyrics);
return $props;
}
/**
* Generate a unique hash for a file path.
*
* @param $path
*
* @return string
*/
public function getHash($path)
{
return md5(config('app.key').$path);
}
/**
* @return getID3
*/
public function getGetID3()
{
return $this->getID3;
}
/**
* @param getID3 $getID3
*/
public function setGetID3($getID3 = null)
{
$this->getID3 = $getID3 ?: new getID3();
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Traits;
/**
* Indicate that a (Model) object collection can be filtered by the current authenticated user.
*
* @package App\Traits
*/
trait CanFilterByUser
{
public function scopeByCurrentUser($query)
{
return $query->whereUserId(auth()->user()->id);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

51
artisan Executable file
View file

@ -0,0 +1,51 @@
#!/usr/bin/env php
<?php
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/bootstrap/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running. We will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);

55
bootstrap/app.php Normal file
View file

@ -0,0 +1,55 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new App\Application(
realpath(__DIR__.'/../')
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

34
bootstrap/autoload.php Normal file
View file

@ -0,0 +1,34 @@
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Composer Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Include The Compiled Class File
|--------------------------------------------------------------------------
|
| To dramatically increase your application's performance, you may use a
| compiled class file which contains all of the classes commonly used
| by a request. The Artisan "optimize" is used to create this file.
|
*/
$compiledPath = __DIR__.'/cache/compiled.php';
if (file_exists($compiledPath)) {
require $compiledPath;
}

2
bootstrap/cache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

29
bower.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "koel",
"version": "0.0.1",
"authors": [
"An Phan <me@phanan.net>"
],
"moduleType": [
"es6"
],
"keywords": [
"koel",
"audio",
"streaming",
"mp3"
],
"license": "MIT",
"homepage": "http://koel.phanan.net",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"devDependencies": {
"plyr": "~1.3.6",
"fontawesome": "~4.5.0"
}
}

54
composer.json Normal file
View file

@ -0,0 +1,54 @@
{
"name": "phanan/koel",
"description": "Personal audio streaming service that works.",
"keywords": ["audio", "stream", "mp3"],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.1.*",
"james-heinrich/getid3": "^1.9"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~5.0",
"phpspec/phpspec": "~2.1",
"barryvdh/laravel-ide-helper": "^2.1",
"phanan/cascading-config": "~2.0"
},
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
"scripts": {
"post-install-cmd": [
"php artisan clear-compiled",
"php artisan optimize"
],
"pre-update-cmd": [
"php artisan clear-compiled"
],
"post-update-cmd": [
"php artisan optimize"
],
"post-root-package-install": [
"php -r \"copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate"
]
},
"config": {
"preferred-install": "dist"
}
}

3241
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

206
config/app.php Normal file
View file

@ -0,0 +1,206 @@
<?php
return [
'tagline' => 'Personal audio streaming service that works.',
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => 'http://localhost',
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY', 'SomeRandomString'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Logging Configuration
|--------------------------------------------------------------------------
|
| Here you may configure the log settings for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Settings: "single", "daily", "syslog", "errorlog"
|
*/
'log' => env('APP_LOG', 'single'),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Routing\ControllerServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
PhanAn\CascadingConfig\CascadingConfigServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\MediaServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'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,
'Bus' => Illuminate\Support\Facades\Bus::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,
'Input' => Illuminate\Support\Facades\Input::class,
'Inspiring' => Illuminate\Foundation\Inspiring::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,
'Media' => App\Facades\Media::class,
],
];

67
config/auth.php Normal file
View file

@ -0,0 +1,67 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Authentication Driver
|--------------------------------------------------------------------------
|
| This option controls the authentication driver that will be utilized.
| This driver manages the retrieval and authentication of the users
| attempting to get access to protected areas of your application.
|
| Supported: "database", "eloquent"
|
*/
'driver' => 'eloquent',
/*
|--------------------------------------------------------------------------
| Authentication Model
|--------------------------------------------------------------------------
|
| When using the "Eloquent" authentication driver, we need to know which
| Eloquent model should be used to retrieve your users. Of course, it
| is often just the "User" model but you may use whatever you like.
|
*/
'model' => App\Models\User::class,
/*
|--------------------------------------------------------------------------
| Authentication Table
|--------------------------------------------------------------------------
|
| When using the "Database" authentication driver, we need to know which
| table should be used to retrieve your users. We have chosen a basic
| default value but you may easily change it to any table you like.
|
*/
'table' => 'users',
/*
|--------------------------------------------------------------------------
| Password Reset Settings
|--------------------------------------------------------------------------
|
| Here you may set the options for resetting passwords including the view
| that is your password reset e-mail. You can also set the name of the
| table that maintains all of the reset tokens for your application.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'password' => [
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
],
];

52
config/broadcasting.php Normal file
View file

@ -0,0 +1,52 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
*/
'default' => env('BROADCAST_DRIVER', 'pusher'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_KEY'),
'secret' => env('PUSHER_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
//
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
],
];

79
config/cache.php Normal file
View file

@ -0,0 +1,79 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache connection that gets used while
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
*/
'default' => env('CACHE_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
*/
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache'),
],
'memcached' => [
'driver' => 'memcached',
'servers' => [
[
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing a RAM based store such as APC or Memcached, there might
| be other applications utilizing the same cache. So, we'll specify a
| value to get prefixed to all our keys so we can avoid collisions.
|
*/
'prefix' => 'laravel',
];

35
config/compile.php Normal file
View file

@ -0,0 +1,35 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Additional Compiled Classes
|--------------------------------------------------------------------------
|
| Here you may specify additional classes to include in the compiled file
| generated by the `artisan optimize` command. These should be classes
| that are included on basically every request into the application.
|
*/
'files' => [
//
],
/*
|--------------------------------------------------------------------------
| Compiled File Providers
|--------------------------------------------------------------------------
|
| Here you may list service providers which define a "compiles" function
| that returns additional files that should be compiled, providing an
| easy way to get common files from any packages you are utilizing.
|
*/
'providers' => [
//
],
];

126
config/database.php Normal file
View file

@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| PDO Fetch Style
|--------------------------------------------------------------------------
|
| By default, database results will be returned as instances of the PHP
| stdClass object; however, you may desire to retrieve records in an
| array format for simplicity. Here you can tweak the fetch style.
|
*/
'fetch' => PDO::FETCH_CLASS,
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => database_path('database.sqlite'),
'prefix' => '',
],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => false,
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'cluster' => false,
'default' => [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
],
],
];

85
config/filesystems.php Normal file
View file

@ -0,0 +1,85 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. A "local" driver, as well as a variety of cloud
| based drivers are available for your choosing. Just store away!
|
| Supported: "local", "ftp", "s3", "rackspace"
|
*/
'default' => 'local',
/*
|--------------------------------------------------------------------------
| Default Cloud Filesystem Disk
|--------------------------------------------------------------------------
|
| Many applications store files both locally and in the cloud. For this
| reason, you may specify a default "cloud" driver here. This driver
| will be bound as the Cloud disk implementation in the container.
|
*/
'cloud' => 's3',
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Here you may configure as many filesystem "disks" as you wish, and you
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'ftp' => [
'driver' => 'ftp',
'host' => 'ftp.example.com',
'username' => 'your-username',
'password' => 'your-password',
// Optional FTP Settings...
// 'port' => 21,
// 'root' => '',
// 'passive' => true,
// 'ssl' => true,
// 'timeout' => 30,
],
's3' => [
'driver' => 's3',
'key' => 'your-key',
'secret' => 'your-secret',
'region' => 'your-region',
'bucket' => 'your-bucket',
],
'rackspace' => [
'driver' => 'rackspace',
'username' => 'your-username',
'key' => 'your-key',
'container' => 'your-container',
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
'region' => 'IAD',
'url_type' => 'publicURL',
],
],
];

124
config/mail.php Normal file
View file

@ -0,0 +1,124 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Mail Driver
|--------------------------------------------------------------------------
|
| Laravel supports both SMTP and PHP's "mail" function as drivers for the
| sending of e-mail. You may specify which one you're using throughout
| your application here. By default, Laravel is setup for SMTP mail.
|
| Supported: "smtp", "mail", "sendmail", "mailgun", "mandrill", "ses", "log"
|
*/
'driver' => env('MAIL_DRIVER', 'smtp'),
/*
|--------------------------------------------------------------------------
| SMTP Host Address
|--------------------------------------------------------------------------
|
| Here you may provide the host address of the SMTP server used by your
| applications. A default option is provided that is compatible with
| the Mailgun mail service which will provide reliable deliveries.
|
*/
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
/*
|--------------------------------------------------------------------------
| SMTP Host Port
|--------------------------------------------------------------------------
|
| This is the SMTP port used by your application to deliver e-mails to
| users of the application. Like the host we have set this value to
| stay compatible with the Mailgun e-mail application by default.
|
*/
'port' => env('MAIL_PORT', 587),
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => ['address' => null, 'name' => null],
/*
|--------------------------------------------------------------------------
| E-Mail Encryption Protocol
|--------------------------------------------------------------------------
|
| Here you may specify the encryption protocol that should be used when
| the application send e-mail messages. A sensible default using the
| transport layer security protocol should provide great security.
|
*/
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
/*
|--------------------------------------------------------------------------
| SMTP Server Username
|--------------------------------------------------------------------------
|
| If your SMTP server requires a username for authentication, you should
| set it here. This will get used to authenticate with your server on
| connection. You may also set the "password" value below this one.
|
*/
'username' => env('MAIL_USERNAME'),
/*
|--------------------------------------------------------------------------
| SMTP Server Password
|--------------------------------------------------------------------------
|
| Here you may set the password required by your SMTP server to send out
| messages from your application. This will be given to the server on
| connection so that the application will be able to send messages.
|
*/
'password' => env('MAIL_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Sendmail System Path
|--------------------------------------------------------------------------
|
| When using the "sendmail" driver to send e-mails, we will need to know
| the path to where Sendmail lives on this server. A default path has
| been provided here, which will work well on most of your systems.
|
*/
'sendmail' => '/usr/sbin/sendmail -bs',
/*
|--------------------------------------------------------------------------
| Mail "Pretend"
|--------------------------------------------------------------------------
|
| When this option is enabled, e-mail will not actually be sent over the
| web and will instead be written to your application's logs files so
| you may inspect the message. This is great for local development.
|
*/
'pretend' => env('MAIL_PRETEND', false),
];

94
config/queue.php Normal file
View file

@ -0,0 +1,94 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Driver
|--------------------------------------------------------------------------
|
| The Laravel queue API supports a variety of back-ends via an unified
| API, giving you convenient access to each back-end using the same
| syntax for each one. Here you may set the default queue driver.
|
| Supported: "null", "sync", "database", "beanstalkd",
| "sqs", "iron", "redis"
|
*/
'default' => env('QUEUE_DRIVER', 'sync'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection information for each server that
| is used by your application. A default configuration has been added
| for each back-end shipped with Laravel. You are free to add more.
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'expire' => 60,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => 'localhost',
'queue' => 'default',
'ttr' => 60,
],
'sqs' => [
'driver' => 'sqs',
'key' => 'your-public-key',
'secret' => 'your-secret-key',
'queue' => 'your-queue-url',
'region' => 'us-east-1',
],
'iron' => [
'driver' => 'iron',
'host' => 'mq-aws-us-east-1.iron.io',
'token' => 'your-token',
'project' => 'your-project-id',
'queue' => 'your-queue-name',
'encrypt' => true,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'expire' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control which database and table are used to store the jobs that
| have failed. You may change them to any database / table you wish.
|
*/
'failed' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],
];

38
config/services.php Normal file
View file

@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Stripe, Mailgun, Mandrill, and others. This file provides a sane
| default location for this type of information, allowing packages
| to have a conventional place to find your various credentials.
|
*/
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],
'mandrill' => [
'secret' => env('MANDRILL_SECRET'),
],
'ses' => [
'key' => env('SES_KEY'),
'secret' => env('SES_SECRET'),
'region' => 'us-east-1',
],
'stripe' => [
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
];

153
config/session.php Normal file
View file

@ -0,0 +1,153 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option controls the default session "driver" that will be used on
| requests. By default, we will use the lightweight native driver but
| you may specify any of the other wonderful drivers provided here.
|
| Supported: "file", "cookie", "database", "apc",
| "memcached", "redis", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
'lifetime' => 30*24*60,
'expire_on_close' => false,
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it is stored. All encryption will be run
| automatically by Laravel and you can use the Session like normal.
|
*/
'encrypt' => true,
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When using the native session driver, we need a location where session
| files may be stored. A default has been set for you but a different
| location may be specified. This is only needed for file sessions.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => null,
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table we
| should use to manage the sessions. Of course, a sensible default is
| provided for you; however, you are free to change this as needed.
|
*/
'table' => 'sessions',
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the cookie used to identify a session
| instance by ID. The name specified here will get used every time a
| new session cookie is created by the framework for every driver.
|
*/
'cookie' => 'remember_me_before_the_war',
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application but you are free to change this when necessary.
|
*/
'path' => '/',
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| Here you may change the domain of the cookie used to identify a session
| in your application. This will determine which domains the cookie is
| available to in your application. A sensible default has been set.
|
*/
'domain' => null,
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you if it can not be done securely.
|
*/
'secure' => false,
];

33
config/view.php Normal file
View file

@ -0,0 +1,33 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| View Storage Paths
|--------------------------------------------------------------------------
|
| Most templating systems load templates from disk. Here you may specify
| an array of paths that should be checked for your views. Of course
| the usual Laravel view path has already been registered for you.
|
*/
'paths' => [
realpath(base_path('resources/views')),
],
/*
|--------------------------------------------------------------------------
| Compiled View Path
|--------------------------------------------------------------------------
|
| This option determines where all the compiled Blade templates will be
| stored for your application. Typically, this is within the storage
| directory. However, as usual, you are free to change this value.
|
*/
'compiled' => realpath(storage_path('framework/views')),
];

1
database/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.sqlite

View file

@ -0,0 +1,46 @@
<?php
$factory->define(App\Models\User::class, function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'is_admin' => false,
'remember_token' => str_random(10),
];
});
$factory->defineAs(App\Models\User::class, 'admin', function ($faker) use ($factory) {
$user = $factory->raw(App\Models\User::class);
return array_merge($user, ['is_admin' => true]);
});
$factory->define(App\Models\Artist::class, function ($faker) {
return [
'name' => $faker->name,
];
});
$factory->define(App\Models\Album::class, function ($faker) {
return [
'name' => $faker->sentence,
'cover' => md5(uniqid()).'.jpg',
];
});
$factory->define(App\Models\Song::class, function ($faker) {
return [
'title' => $faker->sentence,
'length' => $faker->randomFloat(2, 10, 500),
'lyrics' => $faker->paragraph(),
'path' => '/tmp/'.uniqid().'.mp3',
'mtime' => time(),
];
});
$factory->define(App\Models\Playlist::class, function ($faker) {
return [
'name' => $faker->name,
];
});

View file

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password', 60);
$table->boolean('is_admin')->default(false);
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePasswordResetsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token')->index();
$table->timestamp('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('password_resets');
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArtistsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('artists', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('artists');
}
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAlbumsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('albums', function (Blueprint $table) {
$table->increments('id');
$table->integer('artist_id')->unsigned();
$table->string('name');
$table->string('cover')->default('');
$table->timestamps();
$table->foreign('artist_id')->references('id')->on('artists')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('albums');
}
}

View file

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSongsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('songs', function (Blueprint $table) {
$table->string('id', 32);
$table->integer('album_id')->unsigned();
$table->string('title');
$table->float('length');
$table->text('lyrics');
$table->text('path');
$table->integer('mtime');
$table->timestamps();
$table->primary('id');
$table->foreign('album_id')->references('id')->on('albums');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('songs');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePlaylistsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('playlists', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->string('name');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('playlists');
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateInteractionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('interactions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->integer('user_id')->unsigned();
$table->string('song_id', 32);
$table->boolean('liked')->default(false);
$table->integer('play_count')->default(0);
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('song_id')->references('id')->on('songs')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('interactions');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePlaylistSongTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('playlist_song', function (Blueprint $table) {
$table->increments('id');
$table->integer('playlist_id')->unsigned();
$table->string('song_id', 32);
$table->foreign('playlist_id')->references('id')->on('playlists')->onDelete('cascade');
$table->foreign('song_id')->references('id')->on('songs')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('playlist_song');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSettingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('settings', function (Blueprint $table) {
$table->string('key');
$table->text('value');
$table->primary('key');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('settings');
}
}

0
database/seeds/.gitkeep Normal file
View file

View file

@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Album;
use App\Models\Artist;
class AlbumTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
Album::create([
'id' => Album::UNKNOWN_ID,
'artist_id' => Artist::UNKNOWN_ID,
'name' => Album::UNKNOWN_NAME,
'cover' => Album::UNKNOWN_COVER,
]);
}
}

View file

@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Artist;
class ArtistTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
Artist::create([
'id' => Artist::UNKNOWN_ID,
'name' => Artist::UNKNOWN_NAME,
]);
}
}

View file

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
Model::unguard();
// This must be run first, to check for dependencies
$this->call(UserTableSeeder::class);
$this->call(ArtistTableSeeder::class);
$this->call(AlbumTableSeeder::class);
$this->call(SettingTableSeeder::class);
Model::reguard();
}
}

View file

@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Setting;
class SettingTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Setting::set('media_path', '');
}
}

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\User;
class UserTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
if (!env('ADMIN_NAME') || !env('ADMIN_EMAIL') || !env('ADMIN_PASSWORD')) {
$this->command->error('Please fill in initial admin details in .env file first.');
abort(422);
}
User::create([
'name' => env('ADMIN_NAME'),
'email' => env('ADMIN_EMAIL'),
'password' => Hash::make(env('ADMIN_PASSWORD')),
'is_admin' => true,
]);
if (app()->environment() !== 'testing') {
$this->command->info('Admin user created. You can (and should) remove the auth details from .env now.');
}
}
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

23
gulpfile.js Normal file
View file

@ -0,0 +1,23 @@
require('events').EventEmitter.defaultMaxListeners = 30;
var elixir = require('laravel-elixir');
require('laravel-elixir-vueify');
elixir(function (mix) {
mix.browserify('main.js');
mix.sass('app.scss');
mix.copy('resources/assets/img', 'public/img')
.copy('bower_components/fontawesome/fonts', 'public/build/fonts');
mix.scripts([
'bower_components/plyr/dist/plyr.js'
], 'public/js/vendors.js', './')
.styles([
'resources/assets/css/**/*.css',
'bower_components/fontawesome/css/font-awesome.min.css',
'bower_components/plyr/dist/plyr.css'
], 'public/css/vendors.css', './');
mix.version(['css/vendors.css', 'css/app.css', 'js/vendors.js', 'js/main.js']);
});

58
index.php Normal file
View file

@ -0,0 +1,58 @@
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylorotwell@gmail.com>
*/
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels nice to relax.
|
*/
require __DIR__.'/bootstrap/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);

Some files were not shown because too many files have changed in this diff Show more