Upgrade to Larave 5.5 and PHP 7

This commit is contained in:
Phan An 2018-08-24 17:27:19 +02:00
parent f409c5bc47
commit 1dd5457084
98 changed files with 1298 additions and 1673 deletions

2
.gitignore vendored
View file

@ -72,3 +72,5 @@ Temporary Items
*.swp
*.swo
*~
/log

View file

@ -35,14 +35,9 @@ 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
* @param string $manifestFile
*
* @throws \InvalidArgumentException
*
* @return string
* @throws InvalidArgumentException
*/
public function rev($file, $manifestFile = null)
public function rev(string $file, string $manifestFile = null): string
{
static $manifest = null;
@ -67,10 +62,8 @@ class Application extends IlluminateApplication
* Otherwise, just use a full URL to the asset.
*
* @param string $name The additional resource name/path.
*
* @return string
*/
public function staticUrl($name = null)
public function staticUrl(?string $name = null): string
{
$cdnUrl = trim(config('koel.cdn.url'), '/ ');
@ -79,23 +72,16 @@ class Application extends IlluminateApplication
/**
* Get the latest version number of Koel from GitHub.
*
* @param Client $client
*
* @return string
*/
public function getLatestVersion(Client $client = null)
public function getLatestVersion(Client $client = null): string
{
return Cache::remember('latestKoelVersion', 1 * 24 * 60, function () use ($client) {
return Cache::remember('latestKoelVersion', 1 * 24 * 60, static function () use ($client) {
$client = $client ?: new Client();
try {
$v = json_decode(
$client->get('https://api.github.com/repos/phanan/koel/tags')
->getBody()
return json_decode(
$client->get('https://api.github.com/repos/phanan/koel/tags')->getBody()
)[0]->name;
return $v;
} catch (Exception $e) {
Log::error($e);

View file

@ -19,7 +19,7 @@ class GenerateJwtSecretCommand extends Command
$this->dotenvEditor = $dotenvEditor;
}
public function handle()
public function handle(): void
{
if (config('jwt.secret')) {
$this->comment('JWT secret exists -- skipping');

View file

@ -39,7 +39,7 @@ class InitCommand extends Command
$this->db = $db;
}
public function handle()
public function handle(): void
{
$this->comment('Attempting to install or upgrade Koel.');
$this->comment('Remember, you can always install/upgrade manually following the guide here:');
@ -68,7 +68,7 @@ class InitCommand extends Command
/**
* Prompt user for valid database credentials and set up the database.
*/
private function setUpDatabase()
private function setUpDatabase(): void
{
$config = [
'DB_CONNECTION' => '',
@ -117,12 +117,13 @@ class InitCommand extends Command
]);
}
private function setUpAdminAccount()
private function setUpAdminAccount(): void
{
$this->info("Let's create the admin account.");
$name = $this->ask('Your name');
$email = $this->ask('Your email address');
$passwordConfirmed = false;
$password = null;
while (!$passwordConfirmed) {
$password = $this->secret('Your desired password');
@ -143,7 +144,7 @@ class InitCommand extends Command
]);
}
private function maybeSetMediaPath()
private function maybeSetMediaPath(): void
{
if (!Setting::get('media_path')) {
return;
@ -168,7 +169,7 @@ class InitCommand extends Command
}
}
private function maybeGenerateAppKey()
private function maybeGenerateAppKey(): void
{
if (!config('app.key')) {
$this->info('Generating app key');
@ -178,7 +179,7 @@ class InitCommand extends Command
}
}
private function maybeGenerateJwtSecret()
private function maybeGenerateJwtSecret(): void
{
if (!config('jwt.secret')) {
$this->info('Generating JWT secret');
@ -188,7 +189,7 @@ class InitCommand extends Command
}
}
private function maybeSeedDatabase()
private function maybeSeedDatabase(): void
{
if (!User::count()) {
$this->setUpAdminAccount();
@ -199,7 +200,7 @@ class InitCommand extends Command
}
}
private function maybeSetUpDatabase()
private function maybeSetUpDatabase(): void
{
$dbSetUp = false;
@ -217,7 +218,7 @@ class InitCommand extends Command
}
}
private function migrateDatabase()
private function migrateDatabase(): void
{
$this->info('Migrating database');
$this->artisan->call('migrate', ['--force' => true]);
@ -226,7 +227,7 @@ class InitCommand extends Command
$this->mediaCacheService->clear();
}
private function compileFrontEndAssets()
private function compileFrontEndAssets(): void
{
$this->info('Compiling front-end stuff');
system('yarn install');

View file

@ -38,7 +38,7 @@ class SyncMediaCommand extends Command
/**
* @throws Exception
*/
public function handle()
public function handle(): void
{
if (!Setting::get('media_path')) {
$this->warn("Media path hasn't been configured. Let's set it up.");
@ -68,7 +68,7 @@ class SyncMediaCommand extends Command
*
* @throws Exception
*/
protected function syncAll()
protected function syncAll(): void
{
$this->info('Koel syncing started.'.PHP_EOL);
@ -101,19 +101,15 @@ class SyncMediaCommand extends Command
*
* @throws Exception
*/
public function syngle($record)
public function syngle(string $record): void
{
$this->mediaSyncService->syncByWatchRecord(new InotifyWatchRecord($record));
}
/**
* Log a song's sync status to console.
*
* @param string $path
* @param int $result
* @param string $reason
*/
public function logToConsole($path, $result, $reason = '')
public function logSyncStatusToConsole(string $path, int $result, string $reason = ''): void
{
$name = basename($path);
@ -138,20 +134,12 @@ class SyncMediaCommand extends Command
}
}
/**
* Create a progress bar.
*
* @param int $max Max steps
*/
public function createProgressBar($max)
public function createProgressBar(int $max): void
{
$this->progressBar = $this->getOutput()->createProgressBar($max);
}
/**
* Update the progress bar (advance by 1 step).
*/
public function updateProgressBar()
public function advanceProgressBar()
{
$this->progressBar->advance();
}

View file

@ -18,18 +18,15 @@ class AlbumInformationFetched extends Event
$this->information = $information;
}
/**
* @return Album
*/
public function getAlbum()
public function getAlbum(): Album
{
return $this->album;
}
/**
* @return array
* @return mixed[]
*/
public function getInformation()
public function getInformation(): array
{
return $this->information;
}

View file

@ -18,18 +18,15 @@ class ArtistInformationFetched
$this->information = $information;
}
/**
* @return Artist
*/
public function getArtist()
public function getArtist(): Artist
{
return $this->artist;
}
/**
* @return array
* @return mixed[]
*/
public function getInformation()
public function getInformation(): array
{
return $this->information;
}

View file

@ -10,26 +10,9 @@ class SongLikeToggled extends Event
{
use SerializesModels;
/**
* The interaction (like/unlike) in action.
*
* @var Interaction
*/
public $interaction;
/**
* The user who carries the action.
*
* @var User
*/
public $user;
/**
* Create a new event instance.
*
* @param Interaction $interaction
* @param User $user
*/
public function __construct(Interaction $interaction, User $user = null)
{
$this->interaction = $interaction;

View file

@ -10,26 +10,9 @@ class SongStartedPlaying extends Event
{
use SerializesModels;
/**
* The now playing song.
*
* @var Song
*/
public $song;
/**
* The user listening.
*
* @var User
*/
public $user;
/**
* Create a new event instance.
*
* @param Song $song
* @param User $user
*/
public function __construct(Song $song, User $user)
{
$this->song = $song;

View file

@ -28,15 +28,11 @@ class StreamerFactory
$this->transcodingService = $transcodingService;
}
/**
* @param Song $song
* @param bool|null $transcode
* @param int|null $bitRate
* @param int $startTime
*
* @return StreamerInterface
*/
public function createStreamer(Song $song, $transcode = null, $bitRate = null, $startTime = 0)
public function createStreamer(
Song $song,
?bool $transcode = null,
?int $bitRate = null,
float $startTime = 0): StreamerInterface
{
if ($song->s3_params) {
$this->objectStorageStreamer->setSong($song);

View file

@ -13,8 +13,6 @@ class AuthController extends Controller
/**
* Log a user in.
*
* @param UserLoginRequest $request
*
* @return JsonResponse
*/
public function login(UserLoginRequest $request)

View file

@ -36,8 +36,6 @@ class DataController extends Controller
/**
* Get a set of application data.
*
* @param Request $request
*
* @return JsonResponse
*/
public function index(Request $request)

View file

@ -3,16 +3,11 @@
namespace App\Http\Controllers\API\Download;
use App\Models\Album;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class AlbumController extends Controller
{
/**
* Download all songs in an album.
*
* @param Album $album
*
* @return BinaryFileResponse
*/
public function show(Album $album)
{

View file

@ -3,7 +3,6 @@
namespace App\Http\Controllers\API\Download;
use App\Models\Artist;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ArtistController extends Controller
{
@ -11,10 +10,6 @@ class ArtistController extends Controller
* Download all songs by an artist.
* Don't see why one would need this, really.
* Let's pray to God the user doesn't trigger this on Elvis.
*
* @param Artist $artist
*
* @return BinaryFileResponse
*/
public function show(Artist $artist)
{

View file

@ -5,7 +5,6 @@ namespace App\Http\Controllers\API\Download;
use App\Http\Requests\API\Download\Request;
use App\Services\DownloadService;
use App\Services\InteractionService;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class FavoritesController extends Controller
{
@ -19,10 +18,6 @@ class FavoritesController extends Controller
/**
* Download all songs in a playlist.
*
* @param Request $request
*
* @return BinaryFileResponse
*/
public function show(Request $request)
{

View file

@ -4,18 +4,13 @@ namespace App\Http\Controllers\API\Download;
use App\Models\Playlist;
use Illuminate\Auth\Access\AuthorizationException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class PlaylistController extends Controller
{
/**
* Download all songs in a playlist.
*
* @param Playlist $playlist
*
* @throws AuthorizationException
*
* @return BinaryFileResponse
*/
public function show(Playlist $playlist)
{

View file

@ -4,16 +4,11 @@ namespace App\Http\Controllers\API\Download;
use App\Http\Requests\API\Download\SongRequest;
use App\Models\Song;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class SongController extends Controller
{
/**
* Download a song or multiple songs.
*
* @param SongRequest $request
*
* @return BinaryFileResponse
*/
public function show(SongRequest $request)
{

View file

@ -3,16 +3,11 @@
namespace App\Http\Controllers\API\Interaction;
use App\Http\Requests\API\BatchInteractionRequest;
use Illuminate\Http\JsonResponse;
class BatchLikeController extends Controller
{
/**
* Like several songs at once as the currently authenticated user.
*
* @param BatchInteractionRequest $request
*
* @return JsonResponse
*/
public function store(BatchInteractionRequest $request)
{
@ -23,10 +18,6 @@ class BatchLikeController extends Controller
/**
* Unlike several songs at once as the currently authenticated user.
*
* @param BatchInteractionRequest $request
*
* @return JsonResponse
*/
public function destroy(BatchInteractionRequest $request)
{

View file

@ -10,8 +10,6 @@ class LikeController extends Controller
/**
* Like or unlike a song as the currently authenticated user.
*
* @param SongLikeRequest $request
*
* @return JsonResponse
*/
public function store(SongLikeRequest $request)

View file

@ -11,8 +11,6 @@ class PlayCountController extends Controller
/**
* Increase a song's play count as the currently authenticated user.
*
* @param StorePlayCountRequest $request
*
* @return JsonResponse
*/
public function store(StorePlayCountRequest $request)

View file

@ -51,8 +51,6 @@ class LastfmController extends Controller
/**
* Serve the callback request from Last.fm.
*
* @param LastfmCallbackRequest $request
*
* @return Response
*/
public function callback(LastfmCallbackRequest $request)

View file

@ -10,8 +10,6 @@ class AlbumController extends Controller
/**
* Get extra information about an album via Last.fm.
*
* @param Album $album
*
* @return JsonResponse
*/
public function show(Album $album)

View file

@ -10,8 +10,6 @@ class ArtistController extends Controller
/**
* Get extra information about an artist via Last.fm.
*
* @param Artist $artist
*
* @return JsonResponse
*/
public function show(Artist $artist)

View file

@ -14,15 +14,12 @@ class SongController extends Controller
public function __construct(MediaInformationService $mediaInformationService, YouTubeService $youTubeService)
{
parent::__construct($mediaInformationService);
$this->youTubeService = $youTubeService;
}
/**
* Get extra information about a song.
*
* @param Song $song
*
* @return JsonResponse
*/
public function show(Song $song)

View file

@ -27,8 +27,6 @@ class SongController extends Controller
/**
* Store a new song or update an existing one with data from AWS.
*
* @param PutSongRequest $request
*
* @return JsonResponse
*/
public function put(PutSongRequest $request)
@ -64,8 +62,6 @@ class SongController extends Controller
/**
* Remove a song whose info matches with data sent from AWS.
*
* @param RemoveSongRequest $request
*
* @throws Exception
*
* @return JsonResponse

View file

@ -25,8 +25,6 @@ class PlaylistController extends Controller
/**
* Create a new playlist.
*
* @param PlaylistStoreRequest $request
*
* @return JsonResponse
*/
public function store(PlaylistStoreRequest $request)
@ -42,9 +40,6 @@ class PlaylistController extends Controller
/**
* Rename a playlist.
*
* @param Request $request
* @param Playlist $playlist
*
* @throws AuthorizationException
*
* @return JsonResponse
@ -62,9 +57,6 @@ class PlaylistController extends Controller
* Sync a playlist with songs.
* Any songs that are not populated here will be removed from the playlist.
*
* @param PlaylistSyncRequest $request
* @param Playlist $playlist
*
* @throws AuthorizationException
*
* @return JsonResponse
@ -81,8 +73,6 @@ class PlaylistController extends Controller
/**
* Get a playlist's all songs.
*
* @param Playlist $playlist
*
* @throws AuthorizationException
*
* @return JsonResponse
@ -97,8 +87,6 @@ class PlaylistController extends Controller
/**
* Delete a playlist.
*
* @param Playlist $playlist
*
* @throws Exception
* @throws AuthorizationException
*

View file

@ -20,8 +20,6 @@ class ProfileController extends Controller
/**
* Get the current user's profile.
*
* @param Request $request
*
* @return JsonResponse
*/
public function show(Request $request)
@ -32,8 +30,6 @@ class ProfileController extends Controller
/**
* Update the current user's profile.
*
* @param ProfileUpdateRequest $request
*
* @throws RuntimeException
*
* @return JsonResponse

View file

@ -20,28 +20,22 @@ class ScrobbleController extends Controller
/**
* Create a Last.fm scrobble entry for a song.
*
* @param Request $request
* @param Song $song
* @param string $timestamp The UNIX timestamp when the song started playing.
*
* @return JsonResponse
*/
public function store(Request $request, Song $song, $timestamp)
public function store(Request $request, Song $song, string $timestamp)
{
if ($song->artist->is_unknown) {
return response()->json();
if (!$song->artist->is_unknown && $request->user()->connectedToLastfm()) {
$this->lastfmService->scrobble(
$song->artist->name,
$song->title,
(int) $timestamp,
$song->album->name === Album::UNKNOWN_NAME ? '' : $song->album->name,
$request->user()->lastfm_session_key
);
}
if (!$request->user()->connectedToLastfm()) {
return response()->json();
}
return response()->json($this->lastfmService->scrobble(
$song->artist->name,
$song->title,
$timestamp,
$song->album->name === Album::UNKNOWN_NAME ? '' : $song->album->name,
$request->user()->lastfm_session_key
));
return response()->json();
}
}

View file

@ -20,8 +20,6 @@ class SettingController extends Controller
/**
* Save the application settings.
*
* @param SettingRequest $request
*
* @throws Exception
*
* @return JsonResponse

View file

@ -27,8 +27,6 @@ class SongController extends Controller
*
* @link https://github.com/phanan/koel/wiki#streaming-music
*
* @param SongPlayRequest $request
* @param Song $song The song to stream.
* @param null|bool $transcode Whether to force transcoding the song.
* If this is omitted, by default Koel will transcode FLAC.
* @param null|int $bitRate The target bit rate to transcode, defaults to OUTPUT_BIT_RATE.
@ -36,7 +34,7 @@ class SongController extends Controller
*
* @return RedirectResponse|Redirector
*/
public function play(SongPlayRequest $request, Song $song, $transcode = null, $bitRate = null)
public function play(SongPlayRequest $request, Song $song, ?bool $transcode = null, ?int $bitRate = null)
{
return $this->streamerFactory
->createStreamer($song, $transcode, $bitRate, floatval($request->time))
@ -46,8 +44,6 @@ class SongController extends Controller
/**
* Update songs info.
*
* @param SongUpdateRequest $request
*
* @return JsonResponse
*/
public function update(SongUpdateRequest $request)

View file

@ -23,8 +23,6 @@ class UserController extends Controller
/**
* Create a new user.
*
* @param UserStoreRequest $request
*
* @throws RuntimeException
*
* @return JsonResponse
@ -41,9 +39,6 @@ class UserController extends Controller
/**
* Update a user.
*
* @param UserUpdateRequest $request
* @param User $user
*
* @throws RuntimeException
*
* @return JsonResponse
@ -62,8 +57,6 @@ class UserController extends Controller
/**
* Delete a user.
*
* @param User $user
*
* @throws Exception
* @throws AuthorizationException
*

View file

@ -19,9 +19,6 @@ class YouTubeController extends Controller
/**
* Search for YouTube videos related to a song (using its title and artist name).
*
* @param YouTubeSearchRequest $request
* @param Song $song
*
* @return JsonResponse
*/
public function searchVideosRelatedToSong(YouTubeSearchRequest $request, Song $song)

View file

@ -19,9 +19,6 @@ class iTunesController extends Controller
/**
* View a song on iTunes store.
*
* @param ViewSongOnITunesRequest $request
* @param Album $album
*
* @return RedirectResponse
*/
public function viewSong(ViewSongOnITunesRequest $request, Album $album)

View file

@ -4,35 +4,21 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
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)
public function handle(Request $request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax() || $request->route()->getName() === 'play') {

View file

@ -4,38 +4,25 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
class GetUserFromToken extends BaseMiddleware
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next)
{
if (!$token = $this->auth->setRequest($request)->getToken()) {
return $this->respond('tymon.jwt.absent', 'token_not_provided', 401);
}
try {
$user = $this->auth->authenticate($token);
} catch (TokenExpiredException $e) {
return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]);
} catch (JWTException $e) {
return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]);
}
$user = $this->auth->authenticate($token);
if (!$user) {
return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 401);
}
$this->events->fire('tymon.jwt.valid', $user);
$this->events->dispatch('tymon.jwt.valid', $user);
return $next($request);
}

View file

@ -12,11 +12,6 @@ use Illuminate\Http\Request;
class ObjectStorageAuthenticate
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)

View file

@ -4,35 +4,21 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
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)
public function handle(Request $request, Closure $next)
{
if ($this->auth->check()) {
return redirect('/♫');

View file

@ -11,11 +11,6 @@ use Illuminate\Http\Request;
class UseDifferentConfigIfE2E
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)

View file

@ -21,7 +21,7 @@ class JWTAuth extends BaseJWTAuth
/**
* {@inheritdoc}
*/
public function parseToken($method = 'bearer', $header = 'authorization', $query = 'jwt-token')
public function parseToken($method = 'bearer', $header = 'authorization', $query = 'jwt-token'): BaseJWTAuth
{
return parent::parseToken($method, $header, $query);
}

View file

@ -5,7 +5,6 @@ namespace App\Libraries\WatchRecord;
class InotifyWatchRecord extends WatchRecord implements WatchRecordInterface
{
/**
* InotifyWatchRecord constructor.
* {@inheritdoc}
*
* @param $string
@ -19,10 +18,8 @@ class InotifyWatchRecord extends WatchRecord implements WatchRecordInterface
/**
* Parse the inotifywait's output. The inotifywait command should be something like:
* $ inotifywait -rme move,close_write,delete --format "%e %w%f" $MEDIA_PATH.
*
* @param $string string The output string.
*/
public function parse($string)
public function parse(string $string): void
{
list($events, $this->path) = explode(' ', $string, 2);
$this->events = explode(',', $events);
@ -30,10 +27,8 @@ class InotifyWatchRecord extends WatchRecord implements WatchRecordInterface
/**
* Determine if the object has just been deleted or moved from our watched directory.
*
* @return bool
*/
public function isDeleted()
public function isDeleted(): bool
{
return $this->eventExists('DELETE') || $this->eventExists('MOVED_FROM');
}
@ -44,18 +39,13 @@ class InotifyWatchRecord extends WatchRecord implements WatchRecordInterface
* systems only support CREATE, but not CLOSE_WRITE and MOVED_TO.
* Additionally, a MOVED_TO (occurred after the object has been moved/renamed to another location
* **under our watched directory**) should be considered as "modified" also.
*
* @return bool
*/
public function isNewOrModified()
public function isNewOrModified(): bool
{
return $this->eventExists('CLOSE_WRITE') || $this->eventExists('CREATE') || $this->eventExists('MOVED_TO');
}
/**
* {@inheritdoc}
*/
public function isDirectory()
public function isDirectory(): bool
{
return $this->eventExists('ISDIR');
}

View file

@ -2,7 +2,7 @@
namespace App\Libraries\WatchRecord;
class WatchRecord
abstract class WatchRecord implements WatchRecordInterface
{
/**
* Array of the occurred events.
@ -28,55 +28,31 @@ class WatchRecord
protected $input;
/**
* WatchRecord constructor.
*
* @param $input string The output from a watcher command (which is an input for our script)
*/
public function __construct($input)
public function __construct(string $input)
{
$this->input = $input;
}
/**
* Determine if the object is a directory.
*
* @return bool
*/
public function isDirectory()
{
return true;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Determine if the object is a file.
*
* @return bool
*/
public function isFile()
public function isFile(): bool
{
return !$this->isDirectory();
}
/**
* Check if a given event name exists in the event array.
*
* @param $event string
*
* @return bool
*/
protected function eventExists($event)
protected function eventExists(string $event): bool
{
return in_array($event, $this->events, true);
}
public function getPath(): string
{
return $this->path;
}
public function __toString()
{
return $this->input;

View file

@ -4,15 +4,15 @@ namespace App\Libraries\WatchRecord;
interface WatchRecordInterface
{
public function parse($string);
public function parse(string $string);
public function getPath();
public function getPath(): string;
public function isDeleted();
public function isDeleted(): bool;
public function isNewOrModified();
public function isNewOrModified(): bool;
public function isDirectory();
public function isDirectory(): bool;
public function isFile();
public function isFile(): bool;
}

View file

@ -13,11 +13,7 @@ class ClearMediaCache
$this->mediaCacheService = $mediaCacheService;
}
/**
* Fired every time a LibraryChanged event is triggered.
* Clears the media cache.
*/
public function handle()
public function handle(): void
{
$this->mediaCacheService->clear();
}

View file

@ -14,14 +14,14 @@ class DownloadAlbumCover
$this->mediaMetadataService = $mediaMetadataService;
}
public function handle(AlbumInformationFetched $event)
public function handle(AlbumInformationFetched $event): void
{
$info = $event->getInformation();
$album = $event->getAlbum();
$image = array_get($info, 'image');
// If our current album has no cover, and Last.fm has one, why don't we steal it?
// If our current album has no cover, and Last.fm has one, steal it?
if (!$album->has_cover && is_string($image) && ini_get('allow_url_fopen')) {
$this->mediaMetadataService->downloadAlbumCover($album, $image);
}

View file

@ -14,14 +14,14 @@ class DownloadArtistImage
$this->mediaMetadataService = $mediaMetadataService;
}
public function handle(ArtistInformationFetched $event)
public function handle(ArtistInformationFetched $event): void
{
$info = $event->getInformation();
$artist = $event->getArtist();
$image = array_get($info, 'image');
// If our current album has no cover, and Last.fm has one, why don't we steal it?
// If our artist has no image, and Last.fm has one, we steal it?
if (!$artist->has_image && is_string($image) && ini_get('allow_url_fopen')) {
$this->mediaMetadataService->downloadArtistImage($artist, $image);
}

View file

@ -6,26 +6,16 @@ use Illuminate\Events\Dispatcher;
class JWTEventListener
{
/**
* Handle user login events.
*
* @param Dispatcher $event
*/
public function onValidUser($event)
public function onValidUser(Dispatcher $event)
{
auth()->setUser($event->user);
}
/**
* Register the listeners for the subscriber.
*
* @param Dispatcher $events
*/
public function subscribe($events)
public function subscribe(Dispatcher $events)
{
$events->listen(
'tymon.jwt.valid',
'App\Listeners\JWTEventListener@onValidUser'
JWTEventListener::class . '@onValidUser'
);
}
}

View file

@ -7,29 +7,14 @@ use App\Services\LastfmService;
class LoveTrackOnLastfm
{
/**
* The Last.fm service instance, which is DI'ed into our listener.
*
* @var LastfmService
*/
protected $lastfm;
/**
* Create the event listener.
*
* @param LastfmService $lastfm
*/
public function __construct(LastfmService $lastfm)
{
$this->lastfm = $lastfm;
}
/**
* Handle the event.
*
* @param SongLikeToggled $event
*/
public function handle(SongLikeToggled $event)
public function handle(SongLikeToggled $event): void
{
if (!$this->lastfm->enabled() ||
!($sessionKey = $event->user->lastfm_session_key) ||

View file

@ -15,9 +15,6 @@ class TidyLibrary
}
/**
* Fired every time a LibraryChanged event is triggered.
* Tidies up our lib.
*
* @throws Exception
*/
public function handle()

View file

@ -8,29 +8,14 @@ use App\Services\LastfmService;
class UpdateLastfmNowPlaying
{
/**
* The Last.fm service instance.
*
* @var LastfmService
*/
protected $lastfm;
/**
* Create the event listener.
*
* @param LastfmService $lastfm
*/
public function __construct(LastfmService $lastfm)
{
$this->lastfm = $lastfm;
}
/**
* Handle the event.
*
* @param SongStartedPlaying $event
*/
public function handle(SongStartedPlaying $event)
public function handle(SongStartedPlaying $event): void
{
if (!$this->lastfm->enabled() ||
!($sessionKey = $event->user->lastfm_session_key) ||

View file

@ -32,86 +32,49 @@ class Album extends Model
protected $casts = ['artist_id' => 'integer'];
protected $appends = ['is_compilation'];
/**
* An album belongs to an artist.
*
* @return BelongsTo
*/
public function artist()
public function artist(): BelongsTo
{
return $this->belongsTo(Artist::class);
}
/**
* An album can contain many songs.
*
* @return HasMany
*/
public function songs()
public function songs(): HasMany
{
return $this->hasMany(Song::class);
}
/**
* Indicate if the album is unknown.
*
* @return bool
*/
public function getIsUnknownAttribute()
public function getIsUnknownAttribute(): bool
{
return $this->id === self::UNKNOWN_ID;
}
/**
* Get an album using some provided information.
*
* @param Artist $artist
* @param string $name
* @param bool $isCompilation
*
* @return self
* If such is not found, a new album will be created using the information.
*/
public static function get(Artist $artist, $name, $isCompilation = false)
public static function get(Artist $artist, string $name, bool $isCompilation = false): self
{
// If this is a compilation album, its artist must be "Various Artists"
if ($isCompilation) {
$artist = Artist::getVariousArtist();
}
return self::firstOrCreate([
return static::firstOrCreate([
'artist_id' => $artist->id,
'name' => $name ?: self::UNKNOWN_NAME,
]);
}
/**
* Set the album cover.
*
* @param string $value
*/
public function setCoverAttribute($value)
public function setCoverAttribute(?string $value): void
{
$this->attributes['cover'] = $value ?: self::UNKNOWN_COVER;
}
/**
* Get the album cover.
*
* @param string $value
*
* @return string
*/
public function getCoverAttribute($value)
public function getCoverAttribute(?string $value): string
{
return app()->staticUrl('public/img/covers/'.($value ?: self::UNKNOWN_COVER));
}
/**
* Determine if the current album has a cover.
*
* @return bool
*/
public function getHasCoverAttribute()
public function getHasCoverAttribute(): bool
{
$cover = array_get($this->attributes, 'cover');
@ -129,22 +92,13 @@ class Album extends Model
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
*
* @param $value
*
* @return string
*/
public function getNameAttribute($value)
public function getNameAttribute(string $value): string
{
return html_entity_decode($value);
}
/**
* Determine if the album is a compilation.
*
* @return bool
*/
public function getIsCompilationAttribute()
public function getIsCompilationAttribute(): bool
{
return $this->artist_id === Artist::VARIOUS_ID;
}

View file

@ -28,15 +28,9 @@ class Artist extends Model
const VARIOUS_NAME = 'Various Artists';
protected $guarded = ['id'];
protected $hidden = ['created_at', 'updated_at'];
/**
* An artist can have many albums.
*
* @return HasMany
*/
public function albums()
public function albums(): HasMany
{
return $this->hasMany(Album::class);
}
@ -44,53 +38,32 @@ class Artist extends Model
/**
* An artist can have many songs.
* Unless he is Rick Astley.
*
* @return HasManyThrough
*/
public function songs()
public function songs(): HasManyThrough
{
return $this->hasManyThrough(Song::class, Album::class);
}
/**
* Indicate if the artist is unknown.
*
* @return bool
*/
public function getIsUnknownAttribute()
public function getIsUnknownAttribute(): bool
{
return $this->id === self::UNKNOWN_ID;
}
/**
* Indicate if the artist is the special "Various Artists".
*
* @return bool
*/
public function getIsVariousAttribute()
public function getIsVariousAttribute(): bool
{
return $this->id === self::VARIOUS_ID;
}
/**
* Get the "Various Artists" object.
*
* @return Artist
*/
public static function getVariousArtist()
public static function getVariousArtist(): self
{
return self::find(self::VARIOUS_ID);
return static::find(self::VARIOUS_ID);
}
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
*
* @param $value
*
* @return string
*/
public function getNameAttribute($value)
public function getNameAttribute(string $value): string
{
return html_entity_decode($value ?: self::UNKNOWN_NAME);
}
@ -98,12 +71,8 @@ class Artist extends Model
/**
* 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)
public static function get(string $name): self
{
// Remove the BOM from UTF-8/16/32, as it will mess up the database constraints.
if ($encoding = Util::detectUTFEncoding($name)) {
@ -112,22 +81,18 @@ class Artist extends Model
$name = trim($name) ?: self::UNKNOWN_NAME;
return self::firstOrCreate(compact('name'), compact('name'));
return static::firstOrCreate(compact('name'), compact('name'));
}
/**
* Turn the image name into its absolute URL.
*
* @param mixed $value
*
* @return string|null
*/
public function getImageAttribute($value)
public function getImageAttribute(?string $value): ?string
{
return $value ? app()->staticUrl("public/img/artists/$value") : null;
}
public function getHasImageAttribute()
public function getHasImageAttribute(): bool
{
$image = array_get($this->attributes, 'image');
@ -135,10 +100,6 @@ class Artist extends Model
return false;
}
if (!file_exists(public_path("public/img/artists/$image"))) {
return false;
}
return true;
return file_exists(public_path("public/img/artists/$image"));
}
}

View file

@ -31,15 +31,11 @@ class File
/**
* The file's path.
*
* @var string
*/
protected $path;
/**
* The getID3 object, for ID3 tag reading.
*
* @var getID3
*/
protected $getID3;
@ -105,10 +101,8 @@ class File
/**
* Get all applicable ID3 info from the file.
*
* @return array
*/
public function getInfo()
public function getInfo(): array
{
$info = $this->getID3->analyze($this->path);
@ -186,14 +180,14 @@ class File
/**
* Sync the song with all available media info against the database.
*
* @param array $tags The (selective) tags to sync (if the song exists)
* @param string[] $tags The (selective) tags to sync (if the song exists)
* @param bool $force Whether to force syncing, even if the file is unchanged
*
* @return bool|Song A Song object on success,
* true if file exists but is unmodified,
* or false on an error.
*/
public function sync($tags, $force = false)
public function sync(array $tags, bool $force = false)
{
// If the file is not new or changed and we're not forcing update, don't do anything.
if (!$this->isNewOrChanged() && !$force) {
@ -263,10 +257,9 @@ class File
/**
* Try to generate a cover for an album based on extracted data, or use the cover file under the directory.
*
* @param Album $album
* @param $coverData
* @param mixed[]|null $coverData
*/
private function generateAlbumCover(Album $album, $coverData)
private function generateAlbumCover(Album $album, ?array $coverData): void
{
// If the album has no cover, we try to get the cover image from existing tag data
if ($coverData) {
@ -286,66 +279,50 @@ class File
/**
* Determine if the file is new (its Song record can't be found in the database).
*
* @return bool
*/
public function isNew()
public function isNew(): bool
{
return !$this->song;
}
/**
* Determine if the file is changed (its Song record is found, but the timestamp is different).
*
* @return bool
*/
public function isChanged()
public function isChanged(): bool
{
return !$this->isNew() && $this->song->mtime !== $this->mtime;
}
/**
* Determine if the file is new or changed.
*
* @return bool
*/
public function isNewOrChanged()
public function isNewOrChanged(): bool
{
return $this->isNew() || $this->isChanged();
}
/**
* @return getID3
*/
public function getGetID3()
public function getGetID3(): getID3
{
return $this->getID3;
}
/**
* Get the last parsing error's text.
*
* @return string
*/
public function getSyncError()
public function getSyncError(): string
{
return $this->syncError;
}
/**
* @param getID3 $getID3
*
* @throws getid3_exception
*/
public function setGetID3(getID3 $getID3 = null)
public function setGetID3(?getID3 $getID3 = null): void
{
$this->getID3 = $getID3 ?: new getID3();
}
/**
* @return string
*/
public function getPath()
public function getPath(): string
{
return $this->path;
}
@ -356,10 +333,8 @@ class File
* We'll check if such a cover file is found, and use it if positive.
*
* @throws InvalidArgumentException
*
* @return string|false The cover file's full path, or false if none found
*/
private function getCoverFileUnderSameDirectory()
private function getCoverFileUnderSameDirectory(): ?string
{
// As directory scanning can be expensive, we cache and reuse the result.
return Cache::remember(md5($this->path.'_cover'), 24 * 60, function () {
@ -374,11 +349,11 @@ class File
)
);
$cover = $matches ? $matches[0] : false;
$cover = $matches ? $matches[0] : null;
// Even if a file is found, make sure it's a real image.
if ($cover && exif_imagetype($cover) === false) {
$cover = false;
$cover = null;
}
return $cover;
@ -387,17 +362,13 @@ class File
/**
* Get a unique hash from a file path.
*
* @param string $path
*
* @return string
*/
public static function getHash($path)
public static function getHash(string $path): string
{
return md5(config('app.key').$path);
}
private function setMediaMetadataService(MediaMetadataService $mediaMetadataService = null)
private function setMediaMetadataService(MediaMetadataService $mediaMetadataService = null): void
{
$this->mediaMetadataService = $mediaMetadataService ?: app(MediaMetadataService::class);
}

View file

@ -21,27 +21,15 @@ class Interaction extends Model
'liked' => 'boolean',
'play_count' => 'integer',
];
protected $guarded = ['id'];
protected $hidden = ['id', 'user_id', 'created_at', 'updated_at'];
/**
* An interaction belongs to a user.
*
* @return BelongsTo
*/
public function user()
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* An interaction is associated with a song.
*
* @return BelongsTo
*/
public function song()
public function song(): BelongsTo
{
return $this->belongsTo(Song::class);
}

View file

@ -18,29 +18,17 @@ class Playlist extends Model
use CanFilterByUser;
protected $hidden = ['user_id', 'created_at', 'updated_at'];
protected $guarded = ['id'];
protected $casts = [
'user_id' => 'int',
];
/**
* A playlist can have many songs.
*
* @return BelongsToMany
*/
public function songs()
public function songs(): BelongsToMany
{
return $this->belongsToMany(Song::class);
}
/**
* A playlist belongs to a user.
*
* @return BelongsTo
*/
public function user()
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

View file

@ -21,9 +21,9 @@ class Setting extends Model
*
* @param string $key
*
* @return mixed
* @return mixed|string
*/
public static function get($key)
public static function get(string $key)
{
if ($record = self::find($key)) {
return $record->value;
@ -39,7 +39,7 @@ class Setting extends Model
* in which case $value will be discarded.
* @param mixed $value
*/
public static function set($key, $value = null)
public static function set($key, $value = null): void
{
if (is_array($key)) {
foreach ($key as $k => $v) {
@ -58,7 +58,7 @@ class Setting extends Model
*
* @param mixed $value
*/
public function setValueAttribute($value)
public function setValueAttribute($value): void
{
$this->attributes['value'] = serialize($value);
}

View file

@ -59,53 +59,34 @@ class Song extends Model
*/
public $incrementing = false;
/**
* A song belongs to an artist.
*
* @return BelongsTo
*/
public function artist()
public function artist(): BelongsTo
{
return $this->belongsTo(Artist::class);
}
/**
* A song belongs to a album.
*
* @return BelongsTo
*/
public function album()
public function album(): BelongsTo
{
return $this->belongsTo(Album::class);
}
/**
* A song can belong to many playlists.
*
* @return BelongsToMany
*/
public function playlists()
public function playlists(): BelongsToMany
{
return $this->belongsToMany(Playlist::class);
}
/**
* Get a Song record using its path.
*
* @param string $path
*
* @return Song|null
*/
public static function byPath($path)
public static function byPath(string $path): ?self
{
return self::find(File::getHash($path));
return static::find(File::getHash($path));
}
/**
* Update song info.
*
* @param array $ids
* @param array $data The data array, with these supported fields:
* @param string[] $ids
* @param string[] $data The data array, with these supported fields:
* - title
* - artistName
* - albumName
@ -115,7 +96,7 @@ class Song extends Model
*
* @return array
*/
public static function updateInfo($ids, $data)
public static function updateInfo(array $ids, array $data): array
{
/*
* A collection of the updated songs.
@ -155,20 +136,14 @@ class Song extends Model
];
}
/**
* Update a single song's info.
*
* @param string $title
* @param string $albumName
* @param string $artistName
* @param string $lyrics
* @param int $track
* @param int $compilationState
*
* @return self
*/
public function updateSingle($title, $albumName, $artistName, $lyrics, $track, $compilationState)
{
public function updateSingle(
string $title,
string $albumName,
string $artistName,
string $lyrics,
int $track,
int $compilationState
): self {
if ($artistName === Artist::VARIOUS_NAME) {
// If the artist name is "Various Artists", it's a compilation song no matter what.
$compilationState = 1;
@ -211,13 +186,8 @@ class Song extends Model
/**
* Scope a query to only include songs in a given directory.
*
* @param Builder $query
* @param string $path Full path of the directory
*
* @return Builder
*/
public function scopeInDirectory($query, $path)
public function scopeInDirectory(Builder $query, string $path): Builder
{
// Make sure the path ends with a directory separator.
$path = rtrim(trim($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
@ -225,35 +195,12 @@ class Song extends Model
return $query->where('path', 'LIKE', "$path%");
}
/**
* Get all songs favored by a user.
*
* @param User $user
* @param bool $toArray
*
* @return Collection|array
*/
public static function getFavorites(User $user, $toArray = false)
{
/** @var Collection $songs */
$songs = Interaction::whereUserIdAndLike($user->id, true)
->with('song')
->get()
->pluck('song');
return $toArray ? $songs->toArray() : $songs;
}
/**
* Get the song's Object Storage url for streaming or downloading.
*
* @param AwsClient $s3
*
* @return string
*/
public function getObjectStoragePublicUrl(AwsClient $s3 = null)
public function getObjectStoragePublicUrl(AwsClient $s3 = null): string
{
return Cache::remember("OSUrl/{$this->id}", 60, function () use ($s3) {
return Cache::remember("OSUrl/{$this->id}", 60, static function () use ($s3) {
if (!$s3) {
$s3 = AWS::createClient('s3');
}
@ -275,10 +222,8 @@ class Song extends Model
/**
* Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane.
*
* @param string $value
*/
public function setTitleAttribute($value)
public function setTitleAttribute(string $value): void
{
$this->attributes['title'] = html_entity_decode($value);
}
@ -286,24 +231,16 @@ class Song extends Model
/**
* Some songs don't have a title.
* Fall back to the file name (without extension) for such.
*
* @param string $value
*
* @return string
*/
public function getTitleAttribute($value)
public function getTitleAttribute(?string $value): string
{
return $value ?: pathinfo($this->path, PATHINFO_FILENAME);
}
/**
* Prepare the lyrics for displaying.
*
* @param $value
*
* @return string
*/
public function getLyricsAttribute($value)
public function getLyricsAttribute(string $value): string
{
// We don't use nl2br() here, because the function actually preserves line breaks -
// it just _appends_ a "<br />" after each of them. This would cause our client
@ -314,12 +251,12 @@ class Song extends Model
/**
* Get the bucket and key name of an S3 object.
*
* @return bool|array
* @return string[]|null
*/
public function getS3ParamsAttribute()
public function getS3ParamsAttribute(): ?array
{
if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
return false;
return null;
}
list($bucket, $key) = explode('/', $matches[1], 2);
@ -329,8 +266,6 @@ class Song extends Model
/**
* Return the ID of the song when it's converted to string.
*
* @return string
*/
public function __toString()
{

View file

@ -59,12 +59,8 @@ class SongZipArchive
/**
* Add multiple songs into the archive.
*
* @param Collection $songs
*
* @return $this
*/
public function addSongs(Collection $songs)
public function addSongs(Collection $songs): self
{
$songs->each([$this, 'addSong']);
@ -73,12 +69,8 @@ class SongZipArchive
/**
* Add a single song into the archive.
*
* @param Song $song
*
* @return $this
*/
public function addSong(Song $song)
public function addSong(Song $song): self
{
try {
$path = Download::fromSong($song);
@ -107,10 +99,8 @@ class SongZipArchive
/**
* Finish (close) the archive.
*
* @return $this
*/
public function finish()
public function finish(): self
{
$this->archive->close();
@ -119,18 +109,13 @@ class SongZipArchive
/**
* Get the path to the archive.
*
* @return string
*/
public function getPath()
public function getPath(): string
{
return $this->path;
}
/**
* @return ZipArchive
*/
public function getArchive()
public function getArchive(): ZipArchive
{
return $this->archive;
}

View file

@ -16,67 +16,34 @@ class User extends Authenticatable
{
use Notifiable;
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The preferences that we don't want to show to the client.
*
* @var array
*/
protected $hiddenPreferences = ['lastfm_session_key'];
private const HIDDEN_PREFERENCES = ['lastfm_session_key'];
/**
* The attributes that are protected from mass assign.
*
* @var array
*/
protected $guarded = ['id'];
protected $casts = [
'id' => 'int',
'is_admin' => 'bool',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token', 'created_at', 'updated_at'];
/**
* A user can have many playlists.
*
* @return HasMany
*/
public function playlists()
public function playlists(): HasMany
{
return $this->hasMany(Playlist::class);
}
/**
* A user can make multiple interactions.
*
* @return HasMany
*/
public function interactions()
public function interactions(): HasMany
{
return $this->hasMany(Interaction::class);
}
/**
* Get a preference item of the current user.
*
* @param string $key
*
* @return string|null
* @return mixed|null
*/
public function getPreference($key)
public function getPreference(string $key)
{
// We can't use $this->preferences directly, since the data has been tampered
// by getPreferencesAttribute().
@ -84,12 +51,9 @@ class User extends Authenticatable
}
/**
* Save a user preference.
*
* @param string $key
* @param string $val
* @param mixed $val
*/
public function savePreference($key, $val)
public function savePreference(string $key, $val): void
{
$preferences = $this->preferences;
$preferences[$key] = $val;
@ -101,22 +65,15 @@ class User extends Authenticatable
/**
* An alias to savePreference().
*
* @see $this::savePreference
*
* @param $key
* @param $val
* @param mixed $val
* @see self::savePreference
*/
public function setPreference($key, $val)
public function setPreference(string $key, $val): void
{
return $this->savePreference($key, $val);
$this->savePreference($key, $val);
}
/**
* Delete a preference.
*
* @param string $key
*/
public function deletePreference($key)
public function deletePreference(string $key): void
{
$preferences = $this->preferences;
array_forget($preferences, $key);
@ -126,10 +83,8 @@ class User extends Authenticatable
/**
* Determine if the user is connected to Last.fm.
*
* @return bool
*/
public function connectedToLastfm()
public function connectedToLastfm(): bool
{
return (bool) $this->lastfm_session_key;
}
@ -139,7 +94,7 @@ class User extends Authenticatable
*
* @return string|null The key if found, or null if user isn't connected to Last.fm
*/
public function getLastfmSessionKeyAttribute()
public function getLastfmSessionKeyAttribute(): ?string
{
return $this->getPreference('lastfm_session_key');
}
@ -147,9 +102,9 @@ class User extends Authenticatable
/**
* User preferences are stored as a serialized associative array.
*
* @param array $value
* @param mixed[] $value
*/
public function setPreferencesAttribute($value)
public function setPreferencesAttribute(array $value): void
{
$this->attributes['preferences'] = serialize($value);
}
@ -157,16 +112,14 @@ class User extends Authenticatable
/**
* Unserialize the user preferences back to an array before returning.
*
* @param string $value
*
* @return array
* @return mixed[]
*/
public function getPreferencesAttribute($value)
public function getPreferencesAttribute(string $value): array
{
$preferences = unserialize($value) ?: [];
// Hide sensitive data from returned preferences.
foreach ($this->hiddenPreferences as $key) {
foreach (self::HIDDEN_PREFERENCES as $key) {
if (array_key_exists($key, $preferences)) {
$preferences[$key] = 'hidden';
}

View file

@ -7,7 +7,7 @@ use App\Models\User;
class PlaylistPolicy
{
public function owner(User $user, Playlist $playlist)
public function owner(User $user, Playlist $playlist): bool
{
return $user->id === $playlist->user_id;
}

View file

@ -6,7 +6,7 @@ use App\Models\User;
class UserPolicy
{
public function destroy(User $currentUser, User $userToDestroy)
public function destroy(User $currentUser, User $userToDestroy): bool
{
return $currentUser->is_admin && $currentUser->id !== $userToDestroy->id;
}

View file

@ -5,6 +5,7 @@ namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use InvalidArgumentException;
use SimpleXMLElement;
/**
* @method object get($uri, ...$args)
@ -20,8 +21,6 @@ abstract class ApiClient
/**
* The GuzzleHttp client to talk to the API.
*
* @var Client;
*/
protected $client;
@ -42,20 +41,20 @@ abstract class ApiClient
/**
* Make a request to the API.
*
* @param string $verb The HTTP verb
* @param string $uri The API URI (segment)
* @param bool $appendKey Whether to automatically append the API key into the URI.
* @param string $method The HTTP method
* @param string $uri The API URI (segment)
* @param bool $appendKey Whether to automatically append the API key into the URI.
* While it's usually the case, some services (like Last.fm) requires
* an "API signature" of the request. Appending an API key will break the request.
* @param array $params An array of parameters
* @param mixed[] $params An array of parameters
*
* @return object|string
* @return mixed|SimpleXMLElement|null
*/
public function request($verb, $uri, $appendKey = true, array $params = [])
public function request(string $method, string $uri, bool $appendKey = true, array $params = [])
{
try {
$body = (string) $this->getClient()
->$verb($this->buildUrl($uri, $appendKey), ['form_params' => $params])
->$method($this->buildUrl($uri, $appendKey), ['form_params' => $params])
->getBody();
if ($this->responseFormat === 'json') {
@ -68,21 +67,21 @@ abstract class ApiClient
return $body;
} catch (ClientException $e) {
return false;
return null;
}
}
/**
* Make an HTTP call to the external resource.
*
* @param string $method The HTTP method
* @param array $args An array of parameters
* @param string $method The HTTP method
* @param mixed[] $args An array of parameters
*
* @return mixed|null|SimpleXMLElement
* @throws InvalidArgumentException
*
* @return object
*/
public function __call($method, $args)
public function __call(string $method, array $args)
{
if (count($args) < 1) {
throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
@ -98,12 +97,9 @@ abstract class ApiClient
/**
* Turn a URI segment into a full API URL.
*
* @param string $uri
* @param bool $appendKey Whether to automatically append the API key into the URL.
*
* @return string
*/
public function buildUrl($uri, $appendKey = true)
public function buildUrl(string $uri, bool $appendKey = true): string
{
if (!starts_with($uri, ['http://', 'https://'])) {
if ($uri[0] !== '/') {
@ -124,17 +120,14 @@ abstract class ApiClient
return $uri;
}
/**
* @return Client
*/
public function getClient()
public function getClient(): Client
{
return $this->client;
}
abstract public function getKey();
abstract public function getKey(): ?string;
abstract public function getSecret();
abstract public function getSecret(): ?string;
abstract public function getEndpoint();
abstract public function getEndpoint(): string;
}

View file

@ -4,12 +4,7 @@ namespace App\Services;
interface ApiConsumerInterface
{
/** @return string */
public function getEndpoint();
/** @return string */
public function getKey();
/** @return string|null */
public function getSecret();
public function getEndpoint(): string;
public function getKey(): ?string;
public function getSecret(): ?string;
}

View file

@ -21,7 +21,7 @@ class DownloadService
*
* @return string Full path to the generated archive
*/
public function from($mixed)
public function from($mixed): string
{
switch (get_class($mixed)) {
case Song::class:
@ -39,14 +39,7 @@ class DownloadService
throw new InvalidArgumentException('Unsupported download type.');
}
/**
* Generate the downloadable path for a song.
*
* @param Song $song
*
* @return string
*/
public function fromSong(Song $song)
public function fromSong(Song $song): string
{
if ($s3Params = $song->s3_params) {
// The song is hosted on Amazon S3.
@ -69,14 +62,7 @@ class DownloadService
return $localPath;
}
/**
* Generate a downloadable path of multiple songs in zip format.
*
* @param Collection $songs
*
* @return string
*/
protected function fromMultipleSongs(Collection $songs)
protected function fromMultipleSongs(Collection $songs): string
{
if ($songs->count() === 1) {
return $this->fromSong($songs->first());
@ -88,32 +74,17 @@ class DownloadService
->getPath();
}
/**
* @param Playlist $playlist
*
* @return string
*/
protected function fromPlaylist(Playlist $playlist)
protected function fromPlaylist(Playlist $playlist): string
{
return $this->fromMultipleSongs($playlist->songs);
}
/**
* @param Album $album
*
* @return string
*/
protected function fromAlbum(Album $album)
protected function fromAlbum(Album $album): string
{
return $this->fromMultipleSongs($album->songs);
}
/**
* @param Artist $artist
*
* @return string
*/
protected function fromArtist(Artist $artist)
protected function fromArtist(Artist $artist): string
{
return $this->fromMultipleSongs($artist->songs);
}

View file

@ -19,17 +19,14 @@ class InteractionService
/**
* Increase the number of times a song is played by a user.
*
* @param string $songId
* @param User $user
*
* @return Interaction The affected Interaction object
*/
public function increasePlayCount($songId, User $user)
public function increasePlayCount(string $songId, User $user): Interaction
{
return tap($this->interaction->firstOrCreate([
'song_id' => $songId,
'user_id' => $user->id,
]), static function (Interaction $interaction) {
]), static function (Interaction $interaction): void {
if (!$interaction->exists) {
$interaction->liked = false;
}
@ -42,17 +39,14 @@ class InteractionService
/**
* Like or unlike a song on behalf of a user.
*
* @param string $songId
* @param User $user
*
* @return Interaction The affected Interaction object.
*/
public function toggleLike($songId, User $user)
public function toggleLike(string $songId, User $user): Interaction
{
return tap($this->interaction->firstOrCreate([
'song_id' => $songId,
'user_id' => $user->id,
]), static function (Interaction $interaction) {
]), static function (Interaction $interaction): void {
$interaction->liked = !$interaction->liked;
$interaction->save();
@ -63,18 +57,17 @@ class InteractionService
/**
* Like several songs at once as a user.
*
* @param array $songIds
* @param User $user
* @param string[] $songIds
*
* @return array The array of Interaction objects.
* @return Interaction[] The array of Interaction objects.
*/
public function batchLike(array $songIds, User $user)
public function batchLike(array $songIds, User $user): array
{
return collect($songIds)->map(function ($songId) use ($user) {
return collect($songIds)->map(function ($songId) use ($user): Interaction {
return tap($this->interaction->firstOrCreate([
'song_id' => $songId,
'user_id' => $user->id,
]), static function (Interaction $interaction) {
]), static function (Interaction $interaction): void {
if (!$interaction->exists) {
$interaction->play_count = 0;
}
@ -90,16 +83,15 @@ class InteractionService
/**
* Unlike several songs at once.
*
* @param array $songIds
* @param User $user
* @param string[] $songIds
*/
public function batchUnlike(array $songIds, User $user)
public function batchUnlike(array $songIds, User $user): void
{
$this->interaction
->whereIn('song_id', $songIds)
->where('user_id', $user->id)
->get()
->each(static function (Interaction $interaction) {
->each(static function (Interaction $interaction): void {
$interaction->liked = false;
$interaction->save();
@ -110,12 +102,8 @@ class InteractionService
/**
* Get all songs favorited by a user.
*
* @param User $user
*
* @return Collection
*/
public function getUserFavorites(User $user)
public function getUserFavorites(User $user): Collection
{
return $this->interaction->where([
'user_id' => $user->id,

View file

@ -23,20 +23,16 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
/**
* Determine if our application is using Last.fm.
*
* @return bool
*/
public function used()
public function used(): bool
{
return $this->getKey();
}
/**
* Determine if Last.fm integration is enabled.
*
* @return bool
*/
public function enabled()
public function enabled(): bool
{
return $this->getKey() && $this->getSecret();
}
@ -46,12 +42,12 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
*
* @param $name string Name of the artist
*
* @return array|false
* @return mixed[]|null
*/
public function getArtistInformation($name)
public function getArtistInformation(string $name): ?array
{
if (!$this->enabled()) {
return false;
return null;
}
$name = urlencode($name);
@ -70,32 +66,32 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
$response = json_decode(json_encode($response), true);
if (!$response || !$artist = array_get($response, 'artist')) {
return false;
return null;
}
return $this->buildArtistInformation($artist);
} catch (Exception $e) {
Log::error($e);
return false;
return null;
}
}
/**
* Build a Koel-usable array of artist information using the data from Last.fm.
*
* @param array $lastfmArtist
* @param mixed[] $artistData
*
* @return array
* @return mixed[]
*/
private function buildArtistInformation(array $lastfmArtist)
private function buildArtistInformation(array $artistData): array
{
return [
'url' => array_get($lastfmArtist, 'url'),
'image' => count($lastfmArtist['image']) > 3 ? $lastfmArtist['image'][3] : $lastfmArtist['image'][0],
'url' => array_get($artistData, 'url'),
'image' => count($artistData['image']) > 3 ? $artistData['image'][3] : $artistData['image'][0],
'bio' => [
'summary' => $this->formatText(array_get($lastfmArtist, 'bio.summary')),
'full' => $this->formatText(array_get($lastfmArtist, 'bio.content')),
'summary' => $this->formatText(array_get($artistData, 'bio.summary')),
'full' => $this->formatText(array_get($artistData, 'bio.content')),
],
];
}
@ -103,27 +99,24 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
/**
* Get information about an album.
*
* @param string $name Name of the album
* @param string $artistName Name of the artist
*
* @return array|false
* @return mixed[]|null
*/
public function getAlbumInformation($name, $artistName)
public function getAlbumInformation(string $albumName, string $artistName): ?array
{
if (!$this->enabled()) {
return false;
return null;
}
$name = urlencode($name);
$albumName = urlencode($albumName);
$artistName = urlencode($artistName);
try {
$cacheKey = md5("lastfm_album_{$name}_{$artistName}");
$cacheKey = md5("lastfm_album_{$albumName}_{$artistName}");
if ($response = cache($cacheKey)) {
$response = simplexml_load_string($response);
} else {
if ($response = $this->get("?method=album.getInfo&autocorrect=1&album=$name&artist=$artistName")) {
if ($response = $this->get("?method=album.getInfo&autocorrect=1&album=$albumName&artist=$artistName")) {
cache([$cacheKey => $response->asXML()], 24 * 60 * 7);
}
}
@ -131,32 +124,32 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
$response = json_decode(json_encode($response), true);
if (!$response || !$album = array_get($response, 'album')) {
return false;
return null;
}
return $this->buildAlbumInformation($album);
} catch (Exception $e) {
Log::error($e);
return false;
return null;
}
}
/**
* Build a Koel-usable array of album information using the data from Last.fm.
*
* @param array $lastfmAlbum
* @param mixed[] $albumData
*
* @return array
* @return mixed[]
*/
private function buildAlbumInformation(array $lastfmAlbum)
private function buildAlbumInformation(array $albumData): array
{
return [
'url' => array_get($lastfmAlbum, 'url'),
'image' => count($lastfmAlbum['image']) > 3 ? $lastfmAlbum['image'][3] : $lastfmAlbum['image'][0],
'url' => array_get($albumData, 'url'),
'image' => count($albumData['image']) > 3 ? $albumData['image'][3] : $albumData['image'][0],
'wiki' => [
'summary' => $this->formatText(array_get($lastfmAlbum, 'wiki.summary')),
'full' => $this->formatText(array_get($lastfmAlbum, 'wiki.content')),
'summary' => $this->formatText(array_get($albumData, 'wiki.summary')),
'full' => $this->formatText(array_get($albumData, 'wiki.content')),
],
'tracks' => array_map(function ($track) {
return [
@ -164,7 +157,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
'length' => (int) $track['duration'],
'url' => $track['url'],
];
}, array_get($lastfmAlbum, 'tracks.track', [])),
}, array_get($albumData, 'tracks.track', [])),
];
}
@ -174,10 +167,8 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
* @param string $token The token after successfully connecting to Last.fm
*
* @link http://www.last.fm/api/webauth#4
*
* @return string The token key
*/
public function getSessionKey($token)
public function getSessionKey(string $token): ?string
{
$query = $this->buildAuthCallParams([
'method' => 'auth.getSession',
@ -189,7 +180,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
} catch (Exception $e) {
Log::error($e);
return false;
return null;
}
}
@ -201,10 +192,8 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
* @param string|int $timestamp The UNIX timestamp
* @param string $album The album name
* @param string $sk The session key
*
* @return bool
*/
public function scrobble($artist, $track, $timestamp, $album, $sk)
public function scrobble(string $artist, string $track, int $timestamp, string $album, string $sk): void
{
$params = compact('artist', 'track', 'timestamp', 'sk');
@ -215,11 +204,9 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
$params['method'] = 'track.scrobble';
try {
return (bool) $this->post('/', $this->buildAuthCallParams($params), false);
$this->post('/', $this->buildAuthCallParams($params), false);
} catch (Exception $e) {
Log::error($e);
return false;
}
}
@ -230,20 +217,16 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
* @param string $artist The artist's name
* @param string $sk The session key
* @param bool $love Whether to love or unlove. Such cheesy terms... urrgggh
*
* @return bool
*/
public function toggleLoveTrack($track, $artist, $sk, $love = true)
public function toggleLoveTrack(string $track, string $artist, string $sk, ?bool $love = true): void
{
$params = compact('track', 'artist', 'sk');
$params['method'] = $love ? 'track.love' : 'track.unlove';
try {
return (bool) $this->post('/', $this->buildAuthCallParams($params), false);
$this->post('/', $this->buildAuthCallParams($params), false);
} catch (Exception $e) {
Log::error($e);
return false;
}
}
@ -255,10 +238,8 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
* @param string $album Name of the album
* @param int|float $duration Duration of the track, in seconds
* @param string $sk The session key
*
* @return bool
*/
public function updateNowPlaying($artist, $track, $album, $duration, $sk)
public function updateNowPlaying(string $artist, string $track, string $album, float $duration, string $sk): void
{
$params = compact('artist', 'track', 'duration', 'sk');
$params['method'] = 'track.updateNowPlaying';
@ -268,11 +249,9 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
}
try {
return (bool) $this->post('/', $this->buildAuthCallParams($params), false);
$this->post('/', $this->buildAuthCallParams($params), false);
} catch (Exception $e) {
Log::error($e);
return false;
}
}
@ -289,7 +268,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
*
* @return array|string
*/
public function buildAuthCallParams(array $params, $toString = false)
public function buildAuthCallParams(array $params, bool $toString = false)
{
$params['api_key'] = $this->getKey();
ksort($params);
@ -297,9 +276,11 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
// Generate the API signature.
// @link http://www.last.fm/api/webauth#6
$str = '';
foreach ($params as $name => $value) {
$str .= $name.$value;
}
$str .= $this->getSecret();
$params['api_sig'] = md5($str);
@ -317,12 +298,8 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
/**
* Correctly format a string returned by Last.fm.
*
* @param string $str
*
* @return string
*/
protected function formatText($str)
protected function formatText(string $str): string
{
if (!$str) {
return '';
@ -331,17 +308,17 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
return trim(str_replace('Read more on Last.fm', '', nl2br(strip_tags(html_entity_decode($str)))));
}
public function getKey()
public function getKey(): string
{
return config('koel.lastfm.key');
}
public function getEndpoint()
public function getEndpoint(): string
{
return config('koel.lastfm.endpoint');
}
public function getSecret()
public function getSecret(): string
{
return config('koel.lastfm.secret');
}

View file

@ -9,8 +9,9 @@ use Illuminate\Cache\Repository as Cache;
class MediaCacheService
{
private const CACHE_KEY = 'media_cache';
private $cache;
private $keyName = 'media_cache';
public function __construct(Cache $cache)
{
@ -21,15 +22,15 @@ class MediaCacheService
* Get media data.
* If caching is enabled, the data will be retrieved from the cache.
*
* @return array
* @return mixed[]
*/
public function get()
public function get(): array
{
if (!config('koel.cache_media')) {
return $this->query();
}
return $this->cache->rememberForever($this->keyName, function () {
return $this->cache->rememberForever(self::CACHE_KEY, function () {
return $this->query();
});
}
@ -37,9 +38,9 @@ class MediaCacheService
/**
* Query fresh data from the database.
*
* @return array
* @return mixed[]
*/
private function query()
private function query(): array
{
return [
'albums' => Album::orderBy('name')->get(),
@ -51,8 +52,8 @@ class MediaCacheService
/**
* Clear the media cache.
*/
public function clear()
public function clear(): void
{
$this->cache->forget($this->keyName);
$this->cache->forget(self::CACHE_KEY);
}
}

View file

@ -19,14 +19,12 @@ class MediaInformationService
/**
* Get extra information about an album from Last.fm.
*
* @param Album $album
*
* @return array|false The album info in an array format, or false on failure.
* @return array|null The album info in an array format, or null on failure.
*/
public function getAlbumInformation(Album $album)
public function getAlbumInformation(Album $album): ?array
{
if ($album->is_unknown) {
return false;
return null;
}
$info = $this->lastfmService->getAlbumInformation($album->name, $album->artist->name);
@ -45,14 +43,12 @@ class MediaInformationService
/**
* Get extra information about an artist from Last.fm.
*
* @param Artist $artist
*
* @return array|false The artist info in an array format, or false on failure.
* @return array|null The artist info in an array format, or null on failure.
*/
public function getArtistInformation(Artist $artist)
public function getArtistInformation(Artist $artist): ?array
{
if ($artist->is_unknown) {
return false;
return null;
}
$info = $this->lastfmService->getArtistInformation($artist->name);

View file

@ -11,11 +11,8 @@ class MediaMetadataService
{
/**
* Download a copy of the album cover.
*
* @param Album $album
* @param string $imageUrl
*/
public function downloadAlbumCover(Album $album, $imageUrl)
public function downloadAlbumCover(Album $album, string $imageUrl): void
{
$extension = explode('.', $imageUrl);
$this->writeAlbumCover($album, file_get_contents($imageUrl), last($extension));
@ -24,11 +21,10 @@ class MediaMetadataService
/**
* Copy a cover file from an existing image on the system.
*
* @param Album $album
* @param string $source The original image's full path.
* @param string $destination The destination path. Automatically generated if empty.
*/
public function copyAlbumCover(Album $album, $source, $destination = '')
public function copyAlbumCover(Album $album, string $source, string $destination = ''): void
{
$extension = pathinfo($source, PATHINFO_EXTENSION);
$destination = $destination ?: $this->generateAlbumCoverPath($extension);
@ -40,12 +36,9 @@ class MediaMetadataService
/**
* Write an album cover image file with binary data and update the Album with the new cover attribute.
*
* @param Album $album
* @param string $binaryData
* @param string $extension The file extension
* @param string $destination The destination path. Automatically generated if empty.
*/
public function writeAlbumCover(Album $album, $binaryData, $extension, $destination = '')
public function writeAlbumCover(Album $album, string $binaryData, string $extension, string $destination = ''): void
{
try {
$extension = trim(strtolower($extension), '. ');
@ -60,11 +53,8 @@ class MediaMetadataService
/**
* Download a copy of the artist image.
*
* @param Artist $artist
* @param string $imageUrl
*/
public function downloadArtistImage(Artist $artist, $imageUrl)
public function downloadArtistImage(Artist $artist, string $imageUrl): void
{
$extension = explode('.', $imageUrl);
$this->writeArtistImage($artist, file_get_contents($imageUrl), last($extension));
@ -73,13 +63,14 @@ class MediaMetadataService
/**
* Write an artist image file with binary data and update the Artist with the new image attribute.
*
* @param Artist $artist
* @param string $binaryData
* @param string $extension The file extension
* @param string $destination The destination path. Automatically generated if empty.
*/
public function writeArtistImage(Artist $artist, $binaryData, $extension, $destination = '')
{
public function writeArtistImage(
Artist $artist,
string $binaryData,
string $extension,
string $destination = ''
): void {
try {
$extension = trim(strtolower($extension), '. ');
$destination = $destination ?: $this->generateArtistImagePath($extension);
@ -95,10 +86,8 @@ class MediaMetadataService
* Generate a random path for an album cover image.
*
* @param string $extension The extension of the cover (without dot)
*
* @return string
*/
private function generateAlbumCoverPath($extension)
private function generateAlbumCoverPath($extension): string
{
return sprintf('%s/public/img/covers/%s.%s', app()->publicPath(), uniqid('', true), $extension);
}
@ -107,10 +96,8 @@ class MediaMetadataService
* Generate a random path for an artist image.
*
* @param string $extension The extension of the cover (without dot)
*
* @return string
*/
private function generateArtistImagePath($extension)
private function generateArtistImagePath($extension): string
{
return sprintf('%s/public/img/artists/%s.%s', app()->publicPath(), uniqid('', true), $extension);
}

View file

@ -14,6 +14,7 @@ use Exception;
use getID3;
use getid3_exception;
use Log;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
class MediaSyncService
@ -24,7 +25,7 @@ class MediaSyncService
*
* @var array
*/
protected $allTags = [
private const APPLICABLE_TAGS = [
'artist',
'album',
'title',
@ -54,8 +55,7 @@ class MediaSyncService
/**
* Sync the media. Oh sync the media.
*
* @param string|null $mediaPath
* @param array $tags The tags to sync.
* @param string[] $tags The tags to sync.
* Only taken into account for existing records.
* New records will have all tags synced in regardless.
* @param bool $force Whether to force syncing even unchanged files
@ -63,8 +63,12 @@ class MediaSyncService
*
* @throws Exception
*/
public function sync($mediaPath = null, $tags = [], $force = false, SyncMediaCommand $syncCommand = null)
{
public function sync(
?string $mediaPath = null,
array $tags = [],
bool $force = false,
SyncMediaCommand $syncCommand = null
): void {
if (!app()->runningInConsole()) {
set_time_limit(config('koel.sync.timeout'));
}
@ -102,8 +106,8 @@ class MediaSyncService
}
if ($syncCommand) {
$syncCommand->updateProgressBar();
$syncCommand->logToConsole($file->getPath(), $result, $file->getSyncError());
$syncCommand->advanceProgressBar();
$syncCommand->logSyncStatusToConsole($file->getPath(), $result, $file->getSyncError());
}
}
@ -123,9 +127,9 @@ class MediaSyncService
*
* @param string $path The directory's full path
*
* @return array An array of SplFileInfo objects
* @return SplFileInfo[]
*/
public function gatherFiles($path)
public function gatherFiles(string $path): array
{
return iterator_to_array(
Finder::create()
@ -141,11 +145,9 @@ class MediaSyncService
/**
* Sync media using a watch record.
*
* @param WatchRecordInterface $record The watch record.
*
* @throws Exception
*/
public function syncByWatchRecord(WatchRecordInterface $record)
public function syncByWatchRecord(WatchRecordInterface $record): void
{
Log::info("New watch record received: '$record'");
$record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
@ -154,11 +156,9 @@ class MediaSyncService
/**
* Sync a file's watch record.
*
* @param WatchRecordInterface $record
*
* @throws Exception
*/
private function syncFileRecord(WatchRecordInterface $record)
private function syncFileRecord(WatchRecordInterface $record): void
{
$path = $record->getPath();
Log::info("'$path' is a file.");
@ -188,11 +188,9 @@ class MediaSyncService
/**
* Sync a directory's watch record.
*
* @param WatchRecordInterface $record
*
* @throws getid3_exception
*/
private function syncDirectoryRecord(WatchRecordInterface $record)
private function syncDirectoryRecord(WatchRecordInterface $record): void
{
$path = $record->getPath();
Log::info("'$path' is a directory.");
@ -221,11 +219,11 @@ class MediaSyncService
* If the input array is empty or contains only invalid items, we use all tags.
* Otherwise, we only use the valid items in it.
*
* @param array $tags
* @param string[] $tags
*/
public function setTags($tags = [])
public function setTags(array $tags = []): void
{
$this->tags = array_intersect((array) $tags, $this->allTags) ?: $this->allTags;
$this->tags = array_intersect((array) $tags, self::APPLICABLE_TAGS) ?: self::APPLICABLE_TAGS;
// We always keep track of mtime.
if (!in_array('mtime', $this->tags, true)) {
@ -235,12 +233,8 @@ class MediaSyncService
/**
* Generate a unique hash for a file path.
*
* @param $path
*
* @return string
*/
public function getFileHash($path)
public function getFileHash(string $path): string
{
return File::getHash($path);
}
@ -250,7 +244,7 @@ class MediaSyncService
*
* @throws Exception
*/
public function tidy()
public function tidy(): void
{
$inUseAlbums = Song::select('album_id')
->groupBy('album_id')

View file

@ -8,7 +8,7 @@ class PHPStreamer extends Streamer implements DirectStreamerInterface
* Stream the current song using the most basic PHP method: readfile()
* Credits: DaveRandom @ http://stackoverflow.com/a/4451376/794641.
*/
public function stream()
public function stream(): void
{
// Get the 'Range' header if one was sent
if (array_key_exists('HTTP_RANGE', $_SERVER)) {

View file

@ -22,7 +22,7 @@ class Streamer
@error_reporting(0);
}
public function setSong(Song $song)
public function setSong(Song $song): void
{
$this->song = $song;

View file

@ -14,14 +14,14 @@ class TranscodingStreamer extends Streamer implements TranscodingStreamerInterfa
/**
* Time point to start transcoding from.
*
* @var int
* @var float
*/
private $startTime;
/**
* On-the-fly stream the current song while transcoding.
*/
public function stream()
public function stream(): void
{
$ffmpeg = config('koel.streaming.ffmpeg_path');
abort_unless(is_executable($ffmpeg), 500, 'Transcoding requires valid ffmpeg settings.');
@ -47,12 +47,12 @@ class TranscodingStreamer extends Streamer implements TranscodingStreamerInterfa
passthru("$ffmpeg ".implode($args, ' '));
}
public function setBitRate($bitRate)
public function setBitRate(int $bitRate): void
{
$this->bitRate = $bitRate;
}
public function setStartTime($startTime)
public function setStartTime(float $startTime): void
{
$this->startTime = $startTime;
}

View file

@ -4,7 +4,6 @@ namespace App\Services\Streamers;
interface TranscodingStreamerInterface extends StreamerInterface
{
public function setBitRate($bitRate);
public function setStartTime($startTime);
public function setBitRate(int $bitRate): void;
public function setStartTime(float $startTime): void;
}

View file

@ -9,7 +9,7 @@ class XAccelRedirectStreamer extends Streamer implements DirectStreamerInterface
/**
* Stream the current song using nginx's X-Accel-Redirect.
*/
public function stream()
public function stream(): void
{
$relativePath = str_replace(Setting::get('media_path'), '', $this->song->path);

View file

@ -7,7 +7,7 @@ class XSendFileStreamer extends Streamer implements DirectStreamerInterface
/**
* Stream the current song using Apache's x_sendfile module.
*/
public function stream()
public function stream(): void
{
header("X-Sendfile: {$this->song->path}");
header("Content-Type: {$this->contentType}");

View file

@ -6,14 +6,7 @@ use App\Models\Song;
class TranscodingService
{
/**
* Determine if a song should be transcoded.
*
* @param Song $song
*
* @return bool
*/
public function songShouldBeTranscoded(Song $song)
public function songShouldBeTranscoded(Song $song): bool
{
return ends_with(mime_content_type($song->path), 'flac');
}

View file

@ -15,12 +15,8 @@ class Util
/**
* Detects higher UTF encoded strings.
*
* @param string $str
*
* @return string|false
*/
public function detectUTFEncoding($str)
public function detectUTFEncoding(string $str): ?string
{
switch (substr($str, 0, 2)) {
case UTF16_BIG_ENDIAN_BOM:
@ -41,6 +37,6 @@ class Util
return 'UTF-32LE';
}
return false;
return null;
}
}

View file

@ -9,10 +9,8 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
{
/**
* Determine if our application is using YouTube.
*
* @return bool
*/
public function enabled()
public function enabled(): bool
{
return (bool) $this->getKey();
}
@ -20,12 +18,9 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
/**
* Search for YouTube videos related to a song.
*
* @param Song $song
* @param string $pageToken
*
* @return object|false
* @return mixed|null
*/
public function searchVideosRelatedToSong(Song $song, $pageToken = '')
public function searchVideosRelatedToSong(Song $song, string $pageToken = '')
{
$q = $song->title;
@ -44,12 +39,12 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
* @param string $pageToken YouTube page token (e.g. for next/previous page)
* @param int $perPage Number of results per page
*
* @return object|false
* @return mixed|null
*/
public function search($q, $pageToken = '', $perPage = 10)
public function search(string $q, string $pageToken = '', int $perPage = 10)
{
if (!$this->enabled()) {
return false;
return null;
}
$uri = sprintf('search?part=snippet&type=video&maxResults=%s&pageToken=%s&q=%s',
@ -63,20 +58,18 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
});
}
/** @return string */
public function getEndpoint()
public function getEndpoint(): string
{
return config('koel.youtube.endpoint');
}
/** @return string */
public function getKey()
public function getKey(): string
{
return config('koel.youtube.key');
}
/** @return string|null */
public function getSecret()
public function getSecret(): ?string
{
return null;
}
}

View file

@ -10,10 +10,8 @@ class iTunesService extends ApiClient implements ApiConsumerInterface
{
/**
* Determines whether to use iTunes services.
*
* @return bool
*/
public function used()
public function used(): bool
{
return (bool) config('koel.itunes.enabled');
}
@ -27,7 +25,7 @@ class iTunesService extends ApiClient implements ApiConsumerInterface
*
* @return string|false
*/
public function getTrackUrl($term, $album = '', $artist = '')
public function getTrackUrl(string $term, string $album = '', string $artist = ''): ?string
{
try {
return Cache::remember(md5("itunes_track_url_{$term}{$album}{$artist}"), 24 * 60 * 7,
@ -39,11 +37,12 @@ class iTunesService extends ApiClient implements ApiConsumerInterface
'limit' => 1,
];
$response = (string) $this->client->get($this->getEndpoint(), ['query' => $params])->getBody();
$response = json_decode($response);
$response = json_decode(
$this->getClient()->get($this->getEndpoint(), ['query' => $params])->getBody()
);
if (!$response->resultCount) {
return false;
return null;
}
$trackUrl = $response->results[0]->trackViewUrl;
@ -56,19 +55,21 @@ class iTunesService extends ApiClient implements ApiConsumerInterface
} catch (Exception $e) {
Log::error($e);
return false;
return null;
}
}
public function getKey()
public function getKey(): ?string
{
return null;
}
public function getSecret()
public function getSecret(): ?string
{
return null;
}
public function getEndpoint()
public function getEndpoint(): string
{
return config('koel.itunes.endpoint');
}

View file

@ -2,12 +2,14 @@
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
/**
* Indicate that a (Model) object collection can be filtered by the current authenticated user.
*/
trait CanFilterByUser
{
public function scopeByCurrentUser($query)
public function scopeByCurrentUser($query): Builder
{
return $query->whereUserId(auth()->user()->id);
}

View file

@ -15,45 +15,46 @@ trait SupportsDeleteWhereIDsNotIn
/**
* Deletes all records whose IDs are not in an array.
*
* @param array $ids The array of IDs.
* @param string[]|int[] $ids The array of IDs.
* @param string $key Name of the primary key.
*
* @throws \Exception
*
* @return mixed
* @throws Exception
*/
public static function deleteWhereIDsNotIn(array $ids, $key = 'id')
public static function deleteWhereIDsNotIn(array $ids, string $key = 'id'): void
{
// If the number of entries is lower than, or equals to 65535, just go ahead.
if (count($ids) <= 65535) {
return static::whereNotIn($key, $ids)->delete();
static::whereNotIn($key, $ids)->delete();
return;
}
// Otherwise, we get the actual IDs that should be deleted…
$allIDs = static::select($key)->get()->pluck($key)->all();
$whereInIDs = array_diff($allIDs, $ids);
// …and see if we can delete them instead.
if (count($whereInIDs) < 65535) {
return static::whereIn($key, $whereInIDs)->delete();
static::whereIn($key, $whereInIDs)->delete();
return;
}
// If that's not possible (i.e. this array has more than 65535 elements, too)
// then we'll delete chunk by chunk.
static::deleteByChunk($ids, $key);
return $whereInIDs;
}
/**
* Delete records chunk by chunk.
*
* @param array $ids The array of record IDs to delete
* @param string[]|int[] $ids The array of record IDs to delete
* @param string $key Name of the primary key
* @param int $chunkSize Size of each chunk. Defaults to 2^16-1 (65535)
*
* @throws \Exception
* @throws Exception
*/
public static function deleteByChunk(array $ids, $key = 'id', $chunkSize = 65535)
public static function deleteByChunk(array $ids, string $key = 'id', int $chunkSize = 65535): void
{
DB::beginTransaction();
@ -61,6 +62,7 @@ trait SupportsDeleteWhereIDsNotIn
foreach (array_chunk($ids, $chunkSize) as $chunk) {
static::whereIn($key, $chunk)->delete();
}
DB::commit();
} catch (Exception $e) {
DB::rollBack();

View file

@ -6,7 +6,7 @@
"type": "project",
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.4.*",
"laravel/framework": "5.5.*",
"james-heinrich/getid3": "^1.9",
"guzzlehttp/guzzle": "^6.1",
"tymon/jwt-auth": "^0.5.6",
@ -21,15 +21,16 @@
"ext-SimpleXML": "*"
},
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~5.7",
"symfony/css-selector": "2.8.*|3.0.*",
"phpunit/phpunit": "~6.0",
"symfony/css-selector": "~3.1",
"symfony/dom-crawler": "^3.2",
"facebook/webdriver": "^1.2",
"barryvdh/laravel-ide-helper": "^2.1",
"laravel/tinker": "^1.0",
"laravel/browser-kit-testing": "^1.0",
"laravel/browser-kit-testing": "^2.0",
"codeclimate/php-test-reporter": "^0.4.4",
"mikey179/vfsStream": "^1.6"
},
@ -49,6 +50,10 @@
]
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
],
"post-install-cmd": [
"php artisan clear-compiled",
"php artisan optimize",
@ -74,8 +79,6 @@
},
"config": {
"preferred-install": "dist",
"platform": {
"php": "5.6.31"
}
"optimize-autoloader": true
}
}

1411
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -5,12 +5,13 @@ namespace Tests\Feature;
use App\Events\SongLikeToggled;
use App\Models\Song;
use App\Models\User;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Exception;
class InteractionTest extends TestCase
{
use WithoutMiddleware;
/**
* @throws Exception
*/
public function setUp()
{
parent::setUp();
@ -45,7 +46,7 @@ class InteractionTest extends TestCase
/**
* @test
*
* @throws \Exception
* @throws Exception
*/
public function user_can_like_and_unlike_a_song()
{
@ -75,7 +76,7 @@ class InteractionTest extends TestCase
/**
* @test
*
* @throws \Exception
* @throws Exception
*/
public function user_can_like_and_unlike_songs_in_batch()
{

View file

@ -6,14 +6,11 @@ use App\Models\User;
use App\Services\LastfmService;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
use Tymon\JWTAuth\JWTAuth;
class LastfmTest extends TestCase
{
use WithoutMiddleware;
public function testGetSessionKey()
{
/** @var Client $client */
@ -21,15 +18,17 @@ class LastfmTest extends TestCase
'get' => new Response(200, [], file_get_contents(__DIR__.'../../blobs/lastfm/session-key.xml')),
]);
$this->assertEquals('foo', (new LastfmService($client))->getSessionKey('bar'));
self::assertEquals('foo', (new LastfmService($client))->getSessionKey('bar'));
}
public function testSetSessionKey()
{
$user = factory(User::class)->create();
$this->postAsUser('api/lastfm/session-key', ['key' => 'foo'], $user);
$this->postAsUser('api/lastfm/session-key', ['key' => 'foo'], $user)
->assertResponseOk();
$user = User::find($user->id);
$this->assertEquals('foo', $user->lastfm_session_key);
self::assertEquals('foo', $user->lastfm_session_key);
}
public function testConnectToLastfm()

View file

@ -3,6 +3,8 @@
namespace Tests\Feature\ObjectStorage;
use App\Events\LibraryChanged;
use App\Models\Song;
use Exception;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\Feature\TestCase;
@ -10,14 +12,16 @@ class S3Test extends TestCase
{
use WithoutMiddleware;
/**
* @throws Exception
*/
public function setUp()
{
parent::setUp();
$this->disableMiddlewareForAllTests();
}
/** @test */
public function a_song_can_be_added()
public function testStoringASong()
{
$this->post('api/os/s3/song', [
'bucket' => 'koel',
@ -33,18 +37,16 @@ class S3Test extends TestCase
])->seeInDatabase('songs', ['path' => 's3://koel/sample.mp3']);
}
/** @test */
public function a_song_can_be_removed()
/**
* @throws Exception
*/
public function testRemovingASong()
{
$this->expectsEvents(LibraryChanged::class);
$this->post('api/os/s3/song', [
'bucket' => 'koel',
'key' => 'sample.mp3',
'tags' => [
'lyrics' => '',
'duration' => 10,
],
])->seeInDatabase('songs', ['path' => 's3://koel/sample.mp3']);
factory(Song::class)->create([
'path' => 's3://koel/sample.mp3',
]);
$this->delete('api/os/s3/song', [
'bucket' => 'koel',

View file

@ -18,12 +18,9 @@ class PlaylistTest extends TestCase
$this->createSampleMediaSet();
}
/** @test */
public function user_can_create_a_playlist()
public function testCreatingPlaylist()
{
$user = factory(User::class)->create();
// Let's create a playlist with 3 songs
$songs = Song::orderBy('id')->take(3)->get();
$this->postAsUser('api/playlist', [

View file

@ -4,13 +4,10 @@ namespace Tests\Feature;
use App\Models\User;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery\MockInterface;
class ProfileTest extends TestCase
{
use WithoutMiddleware;
/** @var MockInterface */
private $hash;

View file

@ -3,15 +3,12 @@
namespace Tests\Feature;
use App\Models\Song;
use App\Models\User;
use App\Services\LastfmService;
use Exception;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
class ScrobbleTest extends TestCase
{
use WithoutMiddleware;
/**
* @throws Exception
*/
@ -21,13 +18,18 @@ class ScrobbleTest extends TestCase
$this->createSampleMediaSet();
$song = Song::first();
/** @var User $user */
$user = factory(User::class)->create();
$user->setPreference('lastfm_session_key', 'foo');
$ts = time();
m::mock(LastfmService::class, ['enabled' => true])
$this->mockIocDependency(LastfmService::class)
->shouldReceive('scrobble')
->with($song->album->artist->name, $song->title, $ts, $song->album->name, 'bar');
->with($song->album->artist->name, $song->title, $ts, $song->album->name, 'foo')
->once();
$this->post("/api/{$song->id}/scrobble/$ts");
$this->postAsUser("/api/{$song->id}/scrobble/$ts", [], $user)
->assertResponseOk();
}
}

View file

@ -5,13 +5,10 @@ namespace Tests\Feature;
use App\Models\Setting;
use App\Models\User;
use App\Services\MediaSyncService;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery\MockInterface;
class SettingTest extends TestCase
{
use WithoutMiddleware;
/** @var MockInterface */
private $mediaSyncService;
@ -26,8 +23,9 @@ class SettingTest extends TestCase
$this->mediaSyncService->shouldReceive('sync')->once();
$user = factory(User::class, 'admin')->create();
$this->postAsUser('/api/settings', ['media_path' => __DIR__], $user)->seeStatusCode(200);
file_put_contents('log', $this->postAsUser('/api/settings', ['media_path' => __DIR__], $user)
->response->content());
$this->assertEquals(__DIR__, Setting::get('media_path'));
self::assertEquals(__DIR__, Setting::get('media_path'));
}
}

View file

@ -8,12 +8,9 @@ use App\Models\Artist;
use App\Models\Song;
use App\Models\User;
use Exception;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class SongTest extends TestCase
{
use WithoutMiddleware;
/**
* @throws Exception
*/

View file

@ -7,8 +7,8 @@ use App\Models\Artist;
use App\Models\Song;
use App\Models\User;
use Exception;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use JWTAuth;
use Laravel\BrowserKitTesting\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase as BaseTestCase;
use Mockery;
use Tests\CreatesApplication;
@ -93,9 +93,9 @@ abstract class TestCase extends BaseTestCase
protected function tearDown()
{
$this->addToAssertionCount(
Mockery::getContainer()->mockery_getExpectationCount()
);
if ($container = Mockery::getContainer()) {
$this->addToAssertionCount($container->mockery_getExpectationCount());
}
Mockery::close();
parent::tearDown();

View file

@ -5,14 +5,12 @@ namespace Tests\Feature;
use App\Models\Song;
use App\Services\YouTubeService;
use Exception;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery;
use Mockery\MockInterface;
class YouTubeTest extends TestCase
{
use WithoutMiddleware;
/** @var YouTubeService|MockInterface */
/** @var MockInterface */
private $youTubeService;
public function setUp()
@ -25,15 +23,19 @@ class YouTubeTest extends TestCase
/**
* @throws Exception
*/
public function testSearchYouTubeVideos()
public function testSearchYouTubeVideos(): void
{
$this->createSampleMediaSet();
$song = Song::first();
$this->youTubeService
->shouldReceive('searchVideosRelatedToSong')
->with(Mockery::on(static function (Song $retrievedSong) use ($song) {
return $song->id === $retrievedSong->id;
}), 'foo')
->once();
$this->getAsUser("/api/youtube/search/song/{$song->id}");
$this->getAsUser("/api/youtube/search/song/{$song->id}?pageToken=foo")
->assertResponseOk();
}
}

View file

@ -55,9 +55,8 @@ class LastfmServiceTest extends TestCase
]);
$api = new LastfmService($client);
$result = $api->getArtistInformation($artist->name);
$this->assertFalse($result);
$this->assertNull($api->getArtistInformation($artist->name));
}
/**
@ -115,8 +114,7 @@ class LastfmServiceTest extends TestCase
]);
$api = new LastfmService($client);
$result = $api->getAlbumInformation($album->name, $album->artist->name);
$this->assertFalse($result);
$this->assertNull($api->getAlbumInformation($album->name, $album->artist->name));
}
}

View file

@ -50,13 +50,13 @@ class MediaCacheServiceTest extends TestCase
public function testGetIfCacheIsAvailable()
{
$this->cache->shouldReceive('rememberForever')->andReturn('dummy');
$this->cache->shouldReceive('rememberForever')->andReturn(['dummy']);
config(['koel.cache_media' => true]);
$data = $this->mediaCacheService->get();
$this->assertEquals('dummy', $data);
$this->assertEquals(['dummy'], $data);
}
public function testCacheDisabled()

View file

@ -19,12 +19,11 @@ abstract class TestCase extends BaseTestCase
protected function tearDown()
{
$this->addToAssertionCount(
Mockery::getContainer()->mockery_getExpectationCount()
);
if ($container = Mockery::getContainer()) {
$this->addToAssertionCount($container->mockery_getExpectationCount());
}
Mockery::close();
parent::tearDown();
}
}

View file

@ -6,16 +6,17 @@ use App\Services\ApiClient;
class ConcreteApiClient extends ApiClient
{
public function getKey()
public function getKey(): string
{
return 'bar';
}
public function getSecret()
public function getSecret(): string
{
return 'secret';
}
public function getEndpoint()
public function getEndpoint(): string
{
return 'http://foo.com';
}