mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: adapt downloading to Plus
This commit is contained in:
parent
f04f940ffa
commit
473f1c11a1
26 changed files with 175 additions and 173 deletions
|
@ -25,7 +25,7 @@ class LicenseInstanceCast implements CastsAttributes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param LicenseInstance $value */
|
/** @param ?LicenseInstance $value */
|
||||||
public function set($model, string $key, $value, array $attributes): ?string
|
public function set($model, string $key, $value, array $attributes): ?string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -25,7 +25,7 @@ class LicenseMetaCast implements CastsAttributes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param LicenseMeta $value */
|
/** @param ?LicenseMeta $value */
|
||||||
public function set($model, string $key, $value, array $attributes): ?string
|
public function set($model, string $key, $value, array $attributes): ?string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ActivateLicenseCommand extends Command
|
||||||
|
|
||||||
$this->components->twoColumnDetail('Expires On', 'Never ever');
|
$this->components->twoColumnDetail('Expires On', 'Never ever');
|
||||||
|
|
||||||
$this->line('');
|
$this->newLine();
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ class CheckLicenseStatusCommand extends Command
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->components->twoColumnDetail('Expires On', 'Never ever');
|
$this->components->twoColumnDetail('Expires On', 'Never ever');
|
||||||
$this->line('');
|
$this->newLine();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LicenseStatus::STATUS_NO_LICENSE:
|
case LicenseStatus::STATUS_NO_LICENSE:
|
||||||
|
|
|
@ -27,7 +27,6 @@ class ScanCommand extends Command
|
||||||
protected $description = 'Scan for songs in the configured directory.';
|
protected $description = 'Scan for songs in the configured directory.';
|
||||||
|
|
||||||
private ?string $mediaPath;
|
private ?string $mediaPath;
|
||||||
private ?User $owner;
|
|
||||||
private ProgressBar $progressBar;
|
private ProgressBar $progressBar;
|
||||||
|
|
||||||
public function __construct(private MediaScanner $mediaScanner)
|
public function __construct(private MediaScanner $mediaScanner)
|
||||||
|
@ -50,15 +49,22 @@ class ScanCommand extends Command
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$this->owner = $this->getOwner();
|
|
||||||
$this->mediaPath = $this->getMediaPath();
|
$this->mediaPath = $this->getMediaPath();
|
||||||
|
|
||||||
|
$config = ScanConfiguration::make(
|
||||||
|
owner: $this->getOwner(),
|
||||||
|
// When scanning via CLI, the songs should be public by default, unless explicitly specified otherwise.
|
||||||
|
makePublic: !$this->option('private'),
|
||||||
|
ignores: collect($this->option('ignore'))->sort()->values()->all(),
|
||||||
|
force: $this->option('force')
|
||||||
|
);
|
||||||
|
|
||||||
$record = $this->argument('record');
|
$record = $this->argument('record');
|
||||||
|
|
||||||
if ($record) {
|
if ($record) {
|
||||||
$this->scanSingleRecord($record);
|
$this->scanSingleRecord($record, $config);
|
||||||
} else {
|
} else {
|
||||||
$this->scanMediaPath();
|
$this->scanMediaPath($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
|
@ -67,27 +73,14 @@ class ScanCommand extends Command
|
||||||
/**
|
/**
|
||||||
* Scan all files in the configured media path.
|
* Scan all files in the configured media path.
|
||||||
*/
|
*/
|
||||||
private function scanMediaPath(): void
|
private function scanMediaPath(ScanConfiguration $config): void
|
||||||
{
|
{
|
||||||
$this->components->info('Scanning ' . $this->mediaPath);
|
$this->components->info('Scanning ' . $this->mediaPath);
|
||||||
|
|
||||||
// The tags to ignore from scanning.
|
if ($config->ignores) {
|
||||||
// Notice that this is only meaningful for existing records.
|
$this->components->info('Ignoring tag(s): ' . implode(', ', $config->ignores));
|
||||||
// New records will have every applicable field scanned.
|
|
||||||
$ignores = collect($this->option('ignore'))->sort()->values()->all();
|
|
||||||
|
|
||||||
if ($ignores) {
|
|
||||||
$this->components->info('Ignoring tag(s): ' . implode(', ', $ignores));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = ScanConfiguration::make(
|
|
||||||
owner: $this->owner,
|
|
||||||
// When scanning via CLI, the songs should be public by default, unless explicitly specified otherwise.
|
|
||||||
makePublic: !$this->option('private'),
|
|
||||||
ignores: $ignores,
|
|
||||||
force: $this->option('force')
|
|
||||||
);
|
|
||||||
|
|
||||||
$results = $this->mediaScanner->scan($config);
|
$results = $this->mediaScanner->scan($config);
|
||||||
|
|
||||||
$this->newLine(2);
|
$this->newLine(2);
|
||||||
|
@ -110,9 +103,9 @@ class ScanCommand extends Command
|
||||||
*
|
*
|
||||||
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
|
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
|
||||||
*/
|
*/
|
||||||
private function scanSingleRecord(string $record): void
|
private function scanSingleRecord(string $record, ScanConfiguration $config): void
|
||||||
{
|
{
|
||||||
$this->mediaScanner->scanWatchRecord(new InotifyWatchRecord($record));
|
$this->mediaScanner->scanWatchRecord(new InotifyWatchRecord($record), $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onScanProgress(ScanResult $result): void
|
public function onScanProgress(ScanResult $result): void
|
||||||
|
|
|
@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method static string fromSong(Song $song)
|
* @method static string fromSong(Song $song)
|
||||||
|
* @see \App\Services\DownloadService
|
||||||
*/
|
*/
|
||||||
class Download extends Facade
|
class Download extends Facade
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Facade;
|
||||||
/**
|
/**
|
||||||
* @method static bool isPlus()
|
* @method static bool isPlus()
|
||||||
* @method static bool isCommunity()
|
* @method static bool isCommunity()
|
||||||
|
* @see \App\Services\LicenseService
|
||||||
*/
|
*/
|
||||||
class License extends Facade
|
class License extends Facade
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,12 +4,16 @@ namespace App\Http\Controllers\Download;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Album;
|
use App\Models\Album;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\SongRepository;
|
||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
|
||||||
class DownloadAlbumController extends Controller
|
class DownloadAlbumController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(Album $album, DownloadService $download)
|
/** @param User $user */
|
||||||
|
public function __invoke(Album $album, SongRepository $repository, DownloadService $download, Authenticatable $user)
|
||||||
{
|
{
|
||||||
return response()->download($download->from($album));
|
return response()->download($download->from($repository->getByAlbum($album, $user)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,20 @@ namespace App\Http\Controllers\Download;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Artist;
|
use App\Models\Artist;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\SongRepository;
|
||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
|
||||||
class DownloadArtistController extends Controller
|
class DownloadArtistController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(Artist $artist, DownloadService $download)
|
/** @param User $user */
|
||||||
{
|
public function __invoke(
|
||||||
return response()->download($download->from($artist));
|
Artist $artist,
|
||||||
|
SongRepository $repository,
|
||||||
|
DownloadService $download,
|
||||||
|
Authenticatable $user
|
||||||
|
) {
|
||||||
|
return response()->download($download->from($repository->getByArtist($artist, $user)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,15 @@ namespace App\Http\Controllers\Download;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Repositories\InteractionRepository;
|
use App\Repositories\SongRepository;
|
||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
|
||||||
class DownloadFavoritesController extends Controller
|
class DownloadFavoritesController extends Controller
|
||||||
{
|
{
|
||||||
/** @param User $user */
|
/** @param User $user */
|
||||||
public function __invoke(DownloadService $download, InteractionRepository $repository, Authenticatable $user)
|
public function __invoke(DownloadService $download, SongRepository $repository, Authenticatable $user)
|
||||||
{
|
{
|
||||||
return response()->download($download->from($repository->getUserFavorites($user)));
|
return response()->download($download->from($repository->getFavorites($user)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,30 @@ namespace App\Http\Controllers\Download;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Playlist;
|
use App\Models\Playlist;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\SongRepository;
|
||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
|
use App\Services\SmartPlaylistService;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
|
||||||
class DownloadPlaylistController extends Controller
|
class DownloadPlaylistController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(Playlist $playlist, DownloadService $download)
|
/** @param User $user */
|
||||||
{
|
public function __invoke(
|
||||||
$this->authorize('own', $playlist);
|
Playlist $playlist,
|
||||||
|
SongRepository $repository,
|
||||||
|
SmartPlaylistService $smartPlaylistService,
|
||||||
|
DownloadService $download,
|
||||||
|
Authenticatable $user
|
||||||
|
) {
|
||||||
|
$this->authorize('download', $playlist);
|
||||||
|
|
||||||
return response()->download($download->from($playlist));
|
return response()->download(
|
||||||
|
$download->from(
|
||||||
|
$playlist->is_smart
|
||||||
|
? $smartPlaylistService->getSongs($playlist, $user)
|
||||||
|
: $repository->getByStandardPlaylist($playlist, $user)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ class DownloadSongsController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(DownloadSongsRequest $request, DownloadService $download, SongRepository $repository)
|
public function __invoke(DownloadSongsRequest $request, DownloadService $download, SongRepository $repository)
|
||||||
{
|
{
|
||||||
|
$songs = $repository->getMany($request->songs);
|
||||||
|
$songs->each(fn ($song) => $this->authorize('download', $song));
|
||||||
|
|
||||||
return response()->download($download->from($repository->getMany($request->songs)));
|
return response()->download($download->from($repository->getMany($request->songs)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ class SongZipArchive
|
||||||
*/
|
*/
|
||||||
private array $fileNames = [];
|
private array $fileNames = [];
|
||||||
|
|
||||||
public function __construct(string $path = '')
|
public function __construct(?string $path = null)
|
||||||
{
|
{
|
||||||
$this->path = $path ?: self::generateRandomArchivePath();
|
$this->path = $path ?: self::generateRandomArchivePath();
|
||||||
|
|
||||||
|
|
|
@ -11,4 +11,9 @@ class PlaylistPolicy
|
||||||
{
|
{
|
||||||
return $playlist->user->is($user);
|
return $playlist->user->is($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function download(User $user, Playlist $playlist): bool
|
||||||
|
{
|
||||||
|
return $this->own($user, $playlist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,4 +27,13 @@ class SongPolicy
|
||||||
{
|
{
|
||||||
return (License::isCommunity() && $user->is_admin) || $song->owner_id === $user->id;
|
return (License::isCommunity() && $user->is_admin) || $song->owner_id === $user->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function download(User $user, Song $song): bool
|
||||||
|
{
|
||||||
|
if (!config('koel.download.allow')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return License::isCommunity() || $song->is_public || $song->owner_id === $user->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,12 @@
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Testing\TestResponse;
|
||||||
|
|
||||||
class MacroProvider extends ServiceProvider
|
class MacroProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@ -22,5 +25,18 @@ class MacroProvider extends ServiceProvider
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (app()->runningUnitTests()) {
|
||||||
|
UploadedFile::macro('fromFile', static function (string $path, ?string $name = null): UploadedFile {
|
||||||
|
return UploadedFile::fake()->createWithContent($name ?? basename($path), File::get($path));
|
||||||
|
});
|
||||||
|
|
||||||
|
TestResponse::macro('log', function (string $file = 'test-response.json'): TestResponse {
|
||||||
|
/** @var TestResponse $this */
|
||||||
|
File::put(storage_path('logs/' . $file), $this->getContent());
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ class AlbumRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Album> */
|
/** @return Collection|array<array-key, Album> */
|
||||||
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
|
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
|
||||||
$user ??= $this->auth->user();
|
$user ??= $this->auth->user();
|
||||||
|
|
||||||
$query = Album::query()
|
$query = Album::query()
|
||||||
|
|
|
@ -14,7 +14,7 @@ class ArtistRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Artist> */
|
/** @return Collection|array<array-key, Artist> */
|
||||||
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
|
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $user */
|
/** @var ?User $user */
|
||||||
$user ??= auth()->user();
|
$user ??= auth()->user();
|
||||||
|
|
||||||
$query = Artist::query()
|
$query = Artist::query()
|
||||||
|
|
|
@ -29,7 +29,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getRecentlyAdded(int $count = 10, ?User $scopedUser = null): Collection
|
public function getRecentlyAdded(int $count = 10, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -43,7 +43,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getMostPlayed(int $count = 7, ?User $scopedUser = null): Collection
|
public function getMostPlayed(int $count = 7, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -58,7 +58,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getRecentlyPlayed(int $count = 7, ?User $scopedUser = null): Collection
|
public function getRecentlyPlayed(int $count = 7, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -75,7 +75,7 @@ class SongRepository extends Repository
|
||||||
?User $scopedUser = null,
|
?User $scopedUser = null,
|
||||||
int $perPage = 50
|
int $perPage = 50
|
||||||
): Paginator {
|
): Paginator {
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -92,7 +92,7 @@ class SongRepository extends Repository
|
||||||
?User $scopedUser = null,
|
?User $scopedUser = null,
|
||||||
int $perPage = 50
|
int $perPage = 50
|
||||||
): Paginator {
|
): Paginator {
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -110,7 +110,7 @@ class SongRepository extends Repository
|
||||||
int $limit = self::DEFAULT_QUEUE_LIMIT,
|
int $limit = self::DEFAULT_QUEUE_LIMIT,
|
||||||
?User $scopedUser = null,
|
?User $scopedUser = null,
|
||||||
): Collection {
|
): Collection {
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -124,7 +124,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getFavorites(?User $scopedUser = null): Collection
|
public function getFavorites(?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -137,7 +137,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getByAlbum(Album $album, ?User $scopedUser = null): Collection
|
public function getByAlbum(Album $album, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -153,7 +153,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getByArtist(Artist $artist, ?User $scopedUser = null): Collection
|
public function getByArtist(Artist $artist, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -171,7 +171,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getByStandardPlaylist(Playlist $playlist, ?User $scopedUser = null): Collection
|
public function getByStandardPlaylist(Playlist $playlist, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -187,7 +187,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getRandom(int $limit, ?User $scopedUser = null): Collection
|
public function getRandom(int $limit, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -201,7 +201,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getMany(array $ids, bool $inThatOrder = false, ?User $scopedUser = null): Collection
|
public function getMany(array $ids, bool $inThatOrder = false, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
$songs = Song::query()
|
$songs = Song::query()
|
||||||
|
@ -215,7 +215,7 @@ class SongRepository extends Repository
|
||||||
|
|
||||||
public function getOne($id, ?User $scopedUser = null): Song
|
public function getOne($id, ?User $scopedUser = null): Song
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -226,7 +226,7 @@ class SongRepository extends Repository
|
||||||
|
|
||||||
public function findOne($id, ?User $scopedUser = null): ?Song
|
public function findOne($id, ?User $scopedUser = null): ?Song
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
@ -248,7 +248,7 @@ class SongRepository extends Repository
|
||||||
/** @return Collection|array<array-key, Song> */
|
/** @return Collection|array<array-key, Song> */
|
||||||
public function getRandomByGenre(string $genre, int $limit, ?User $scopedUser = null): Collection
|
public function getRandomByGenre(string $genre, int $limit, ?User $scopedUser = null): Collection
|
||||||
{
|
{
|
||||||
/** @var User $scopedUser */
|
/** @var ?User $scopedUser */
|
||||||
$scopedUser ??= $this->auth->user();
|
$scopedUser ??= $this->auth->user();
|
||||||
|
|
||||||
return Song::query()
|
return Song::query()
|
||||||
|
|
|
@ -2,14 +2,11 @@
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Album;
|
|
||||||
use App\Models\Artist;
|
|
||||||
use App\Models\Playlist;
|
|
||||||
use App\Models\Song;
|
use App\Models\Song;
|
||||||
use App\Models\SongZipArchive;
|
use App\Models\SongZipArchive;
|
||||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use InvalidArgumentException;
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
class DownloadService
|
class DownloadService
|
||||||
{
|
{
|
||||||
|
@ -17,57 +14,7 @@ class DownloadService
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function from(Collection $songs): string
|
||||||
* Generic method to generate a download archive from various source types.
|
|
||||||
*
|
|
||||||
* @return string Full path to the generated archive
|
|
||||||
*/
|
|
||||||
public function from(Playlist|Song|Album|Artist|Collection $downloadable): string
|
|
||||||
{
|
|
||||||
switch (get_class($downloadable)) {
|
|
||||||
case Song::class:
|
|
||||||
return $this->fromSong($downloadable);
|
|
||||||
|
|
||||||
case Collection::class:
|
|
||||||
case EloquentCollection::class:
|
|
||||||
return $this->fromMultipleSongs($downloadable);
|
|
||||||
|
|
||||||
case Album::class:
|
|
||||||
return $this->fromAlbum($downloadable);
|
|
||||||
|
|
||||||
case Artist::class:
|
|
||||||
return $this->fromArtist($downloadable);
|
|
||||||
|
|
||||||
case Playlist::class:
|
|
||||||
return $this->fromPlaylist($downloadable);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidArgumentException('Unsupported download type.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fromSong(Song $song): string
|
|
||||||
{
|
|
||||||
if ($song->s3_params) {
|
|
||||||
// The song is hosted on Amazon S3.
|
|
||||||
// We download it back to our local server first.
|
|
||||||
$url = $this->s3Service->getSongPublicUrl($song);
|
|
||||||
abort_unless((bool) $url, 404);
|
|
||||||
|
|
||||||
$localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . basename($song->s3_params['key']);
|
|
||||||
|
|
||||||
// The following function requires allow_url_fopen to be ON.
|
|
||||||
// We're just assuming that to be the case here.
|
|
||||||
copy($url, $localPath);
|
|
||||||
} else {
|
|
||||||
// The song is hosted locally. Make sure the file exists.
|
|
||||||
$localPath = $song->path;
|
|
||||||
abort_unless(file_exists($localPath), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $localPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fromMultipleSongs(Collection $songs): string
|
|
||||||
{
|
{
|
||||||
if ($songs->count() === 1) {
|
if ($songs->count() === 1) {
|
||||||
return $this->fromSong($songs->first());
|
return $this->fromSong($songs->first());
|
||||||
|
@ -79,24 +26,25 @@ class DownloadService
|
||||||
->getPath();
|
->getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fromPlaylist(Playlist $playlist): string
|
public function fromSong(Song $song): string
|
||||||
{
|
{
|
||||||
return $this->fromMultipleSongs($playlist->songs);
|
if ($song->s3_params) {
|
||||||
|
// The song is hosted on Amazon S3.
|
||||||
|
// We download it back to our local server first.
|
||||||
|
$url = $this->s3Service->getSongPublicUrl($song);
|
||||||
|
abort_unless((bool) $url, Response::HTTP_NOT_FOUND);
|
||||||
|
|
||||||
|
$localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . basename($song->s3_params['key']);
|
||||||
|
|
||||||
|
// The following function requires allow_url_fopen to be ON.
|
||||||
|
// We're just assuming that to be the case here.
|
||||||
|
copy($url, $localPath);
|
||||||
|
} else {
|
||||||
|
// The song is hosted locally. Make sure the file exists.
|
||||||
|
$localPath = $song->path;
|
||||||
|
abort_unless(File::exists($localPath), Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fromAlbum(Album $album): string
|
return $localPath;
|
||||||
{
|
|
||||||
return $this->fromMultipleSongs($album->songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fromArtist(Artist $artist): string
|
|
||||||
{
|
|
||||||
// We cater to the case where the artist is an "album artist," which means she has songs through albums as well.
|
|
||||||
$songs = $artist->albums->reduce(
|
|
||||||
static fn (Collection $songs, Album $album) => $songs->merge($album->songs),
|
|
||||||
$artist->songs
|
|
||||||
)->unique('id');
|
|
||||||
|
|
||||||
return $this->fromMultipleSongs($songs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class LicenseService
|
||||||
return Cache::get('license_status');
|
return Cache::get('license_status');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var License $license */
|
/** @var ?License $license */
|
||||||
$license = License::query()->latest()->first();
|
$license = License::query()->latest()->first();
|
||||||
|
|
||||||
if (!$license) {
|
if (!$license) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ Route::middleware('web')->group(static function (): void {
|
||||||
Route::middleware('audio.auth')->group(static function (): void {
|
Route::middleware('audio.auth')->group(static function (): void {
|
||||||
Route::get('play/{song}/{transcode?}/{bitrate?}', PlayController::class)->name('song.play');
|
Route::get('play/{song}/{transcode?}/{bitrate?}', PlayController::class)->name('song.play');
|
||||||
|
|
||||||
|
if (config('koel.download.allow')) {
|
||||||
Route::prefix('download')->group(static function (): void {
|
Route::prefix('download')->group(static function (): void {
|
||||||
Route::get('songs', DownloadSongsController::class);
|
Route::get('songs', DownloadSongsController::class);
|
||||||
Route::get('album/{album}', DownloadAlbumController::class);
|
Route::get('album/{album}', DownloadAlbumController::class);
|
||||||
|
@ -37,5 +38,6 @@ Route::middleware('web')->group(static function (): void {
|
||||||
Route::get('playlist/{playlist}', DownloadPlaylistController::class);
|
Route::get('playlist/{playlist}', DownloadPlaylistController::class);
|
||||||
Route::get('favorites', DownloadFavoritesController::class);
|
Route::get('favorites', DownloadFavoritesController::class);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,7 +19,7 @@ class UploadTest extends TestCase
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3');
|
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUnauthorizedPost(): void
|
public function testUnauthorizedPost(): void
|
||||||
|
|
|
@ -37,11 +37,10 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testScan(): void
|
public function testScan(): void
|
||||||
{
|
{
|
||||||
/** @var User $owner */
|
|
||||||
$owner = User::factory()->admin()->create();
|
|
||||||
|
|
||||||
$this->expectsEvents(MediaScanCompleted::class);
|
$this->expectsEvents(MediaScanCompleted::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||||
|
|
||||||
// Standard mp3 files under root path should be recognized
|
// Standard mp3 files under root path should be recognized
|
||||||
|
@ -94,10 +93,11 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testModifiedFileIsRescanned(): void
|
public function testModifiedFileIsRescanned(): void
|
||||||
{
|
{
|
||||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
|
||||||
|
|
||||||
$this->expectsEvents(MediaScanCompleted::class);
|
$this->expectsEvents(MediaScanCompleted::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
|
$config = ScanConfiguration::make(owner: $owner);
|
||||||
$this->scanner->scan($config);
|
$this->scanner->scan($config);
|
||||||
|
|
||||||
/** @var Song $song */
|
/** @var Song $song */
|
||||||
|
@ -111,10 +111,12 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testRescanWithoutForceDoesNotResetData(): void
|
public function testRescanWithoutForceDoesNotResetData(): void
|
||||||
{
|
{
|
||||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
|
||||||
|
|
||||||
$this->expectsEvents(MediaScanCompleted::class);
|
$this->expectsEvents(MediaScanCompleted::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
|
$config = ScanConfiguration::make(owner: $owner);
|
||||||
|
|
||||||
$this->scanner->scan($config);
|
$this->scanner->scan($config);
|
||||||
|
|
||||||
/** @var Song $song */
|
/** @var Song $song */
|
||||||
|
@ -134,11 +136,10 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testForceScanResetsData(): void
|
public function testForceScanResetsData(): void
|
||||||
{
|
{
|
||||||
/** @var User $owner */
|
|
||||||
$owner = User::factory()->admin()->create();
|
|
||||||
|
|
||||||
$this->expectsEvents(MediaScanCompleted::class);
|
$this->expectsEvents(MediaScanCompleted::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||||
|
|
||||||
/** @var Song $song */
|
/** @var Song $song */
|
||||||
|
@ -149,7 +150,9 @@ class MediaScannerTest extends TestCase
|
||||||
'lyrics' => 'Booom Wroooom',
|
'lyrics' => 'Booom Wroooom',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->scanner->scan(ScanConfiguration::make(owner: User::factory()->admin()->create(), force: true));
|
/** @var User $anotherOwner */
|
||||||
|
$anotherOwner = User::factory()->admin()->create();
|
||||||
|
$this->scanner->scan(ScanConfiguration::make(owner: $anotherOwner, force: true));
|
||||||
|
|
||||||
$song->refresh();
|
$song->refresh();
|
||||||
|
|
||||||
|
@ -161,11 +164,10 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testScanWithIgnoredTags(): void
|
public function testScanWithIgnoredTags(): void
|
||||||
{
|
{
|
||||||
/** @var User $owner */
|
|
||||||
$owner = User::factory()->admin()->create();
|
|
||||||
|
|
||||||
$this->expectsEvents(MediaScanCompleted::class);
|
$this->expectsEvents(MediaScanCompleted::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||||
|
|
||||||
/** @var Song $song */
|
/** @var Song $song */
|
||||||
|
@ -186,10 +188,10 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testScanAllTagsForNewFilesRegardlessOfIgnoredOption(): void
|
public function testScanAllTagsForNewFilesRegardlessOfIgnoredOption(): void
|
||||||
{
|
{
|
||||||
|
$this->expectsEvents(MediaScanCompleted::class);
|
||||||
|
|
||||||
/** @var User $owner */
|
/** @var User $owner */
|
||||||
$owner = User::factory()->admin()->create();
|
$owner = User::factory()->admin()->create();
|
||||||
|
|
||||||
$this->expectsEvents(MediaScanCompleted::class);
|
|
||||||
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
|
||||||
|
|
||||||
/** @var Song $song */
|
/** @var Song $song */
|
||||||
|
@ -214,11 +216,14 @@ class MediaScannerTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->expectsEvents(LibraryChanged::class);
|
$this->expectsEvents(LibraryChanged::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
|
|
||||||
$path = $this->path('/blank.mp3');
|
$path = $this->path('/blank.mp3');
|
||||||
|
|
||||||
$this->scanner->scanWatchRecord(
|
$this->scanner->scanWatchRecord(
|
||||||
new InotifyWatchRecord("CLOSE_WRITE,CLOSE $path"),
|
new InotifyWatchRecord("CLOSE_WRITE,CLOSE $path"),
|
||||||
ScanConfiguration::make(owner: User::factory()->admin()->create())
|
ScanConfiguration::make(owner: $owner)
|
||||||
);
|
);
|
||||||
|
|
||||||
self::assertDatabaseHas(Song::class, ['path' => $path]);
|
self::assertDatabaseHas(Song::class, ['path' => $path]);
|
||||||
|
@ -228,6 +233,9 @@ class MediaScannerTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->expectsEvents(LibraryChanged::class);
|
$this->expectsEvents(LibraryChanged::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
|
|
||||||
static::createSampleMediaSet();
|
static::createSampleMediaSet();
|
||||||
|
|
||||||
/** @var Song $song */
|
/** @var Song $song */
|
||||||
|
@ -235,7 +243,7 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
$this->scanner->scanWatchRecord(
|
$this->scanner->scanWatchRecord(
|
||||||
new InotifyWatchRecord("DELETE $song->path"),
|
new InotifyWatchRecord("DELETE $song->path"),
|
||||||
ScanConfiguration::make(owner: User::factory()->admin()->create())
|
ScanConfiguration::make(owner: $owner)
|
||||||
);
|
);
|
||||||
|
|
||||||
self::assertModelMissing($song);
|
self::assertModelMissing($song);
|
||||||
|
@ -243,10 +251,12 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testScanDeletedDirectoryViaWatch(): void
|
public function testScanDeletedDirectoryViaWatch(): void
|
||||||
{
|
{
|
||||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
|
||||||
|
|
||||||
$this->expectsEvents(LibraryChanged::class, MediaScanCompleted::class);
|
$this->expectsEvents(LibraryChanged::class, MediaScanCompleted::class);
|
||||||
|
|
||||||
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
|
$config = ScanConfiguration::make(owner: $owner);
|
||||||
|
|
||||||
$this->scanner->scan($config);
|
$this->scanner->scan($config);
|
||||||
$this->scanner->scanWatchRecord(new InotifyWatchRecord("MOVED_FROM,ISDIR $this->mediaPath/subdir"), $config);
|
$this->scanner->scanWatchRecord(new InotifyWatchRecord("MOVED_FROM,ISDIR $this->mediaPath/subdir"), $config);
|
||||||
|
|
||||||
|
@ -290,7 +300,9 @@ class MediaScannerTest extends TestCase
|
||||||
|
|
||||||
public function testOptionallyIgnoreHiddenFiles(): void
|
public function testOptionallyIgnoreHiddenFiles(): void
|
||||||
{
|
{
|
||||||
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
|
/** @var User $owner */
|
||||||
|
$owner = User::factory()->admin()->create();
|
||||||
|
$config = ScanConfiguration::make(owner: $owner);
|
||||||
|
|
||||||
config(['koel.ignore_dot_files' => false]);
|
config(['koel.ignore_dot_files' => false]);
|
||||||
$this->scanner->scan($config);
|
$this->scanner->scan($config);
|
||||||
|
|
|
@ -51,7 +51,7 @@ class UploadServiceTest extends TestCase
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$song = $this->service->handleUploadedFile(UploadedFile::fromFile(test_path('songs/full.mp3')), $user);
|
$song = $this->service->handleUploadedFile(UploadedFile::fromFile(test_path('songs/full.mp3')), $user); //@phpstan-ignore-line
|
||||||
|
|
||||||
self::assertSame($song->owner_id, $user->id);
|
self::assertSame($song->owner_id, $user->id);
|
||||||
self::assertSame(public_path("sandbox/media/__KOEL_UPLOADS_\${$user->id}__/full.mp3"), $song->path);
|
self::assertSame(public_path("sandbox/media/__KOEL_UPLOADS_\${$user->id}__/full.mp3"), $song->path);
|
||||||
|
|
|
@ -10,9 +10,6 @@ use App\Services\CommunityLicenseService;
|
||||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
use Illuminate\Http\UploadedFile;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use Illuminate\Testing\TestResponse;
|
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
use Tests\Traits\CreatesApplication;
|
use Tests\Traits\CreatesApplication;
|
||||||
use Tests\Traits\SandboxesTests;
|
use Tests\Traits\SandboxesTests;
|
||||||
|
@ -29,18 +26,6 @@ abstract class TestCase extends BaseTestCase
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
License::swap($this->app->make(CommunityLicenseService::class));
|
License::swap($this->app->make(CommunityLicenseService::class));
|
||||||
|
|
||||||
TestResponse::macro('log', function (string $file = 'test-response.json'): TestResponse {
|
|
||||||
/** @var TestResponse $this */
|
|
||||||
File::put(storage_path('logs/' . $file), $this->getContent());
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
});
|
|
||||||
|
|
||||||
UploadedFile::macro('fromFile', static function (string $path, ?string $name = null): UploadedFile {
|
|
||||||
return UploadedFile::fake()->createWithContent($name ?? basename($path), File::get($path));
|
|
||||||
});
|
|
||||||
|
|
||||||
self::createSandbox();
|
self::createSandbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue