mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
Upgrade to Larave 5.5 and PHP 7
This commit is contained in:
parent
f409c5bc47
commit
1dd5457084
98 changed files with 1298 additions and 1673 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -72,3 +72,5 @@ Temporary Items
|
|||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
/log
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -13,8 +13,6 @@ class AuthController extends Controller
|
|||
/**
|
||||
* Log a user in.
|
||||
*
|
||||
* @param UserLoginRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function login(UserLoginRequest $request)
|
||||
|
|
|
@ -36,8 +36,6 @@ class DataController extends Controller
|
|||
/**
|
||||
* Get a set of application data.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@ class SettingController extends Controller
|
|||
/**
|
||||
* Save the application settings.
|
||||
*
|
||||
* @param SettingRequest $request
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return JsonResponse
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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('/♫');
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -15,9 +15,6 @@ class TidyLibrary
|
|||
}
|
||||
|
||||
/**
|
||||
* Fired every time a LibraryChanged event is triggered.
|
||||
* Tidies up our lib.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle()
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -22,7 +22,7 @@ class Streamer
|
|||
@error_reporting(0);
|
||||
}
|
||||
|
||||
public function setSong(Song $song)
|
||||
public function setSong(Song $song): void
|
||||
{
|
||||
$this->song = $song;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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}");
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
1411
composer.lock
generated
File diff suppressed because it is too large
Load diff
2
storage/framework/cache/.gitignore
vendored
2
storage/framework/cache/.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
!.gitignore
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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', [
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue