mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: supports multi-tenant
This commit is contained in:
parent
600a3c1dd6
commit
9e27a08960
45 changed files with 588 additions and 399 deletions
|
@ -2,8 +2,11 @@
|
|||
|
||||
namespace App\Builders;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Album;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class AlbumBuilder extends Builder
|
||||
{
|
||||
|
@ -11,4 +14,20 @@ class AlbumBuilder extends Builder
|
|||
{
|
||||
return $this->whereNot('albums.id', Album::UNKNOWN_ID);
|
||||
}
|
||||
|
||||
public function accessibleBy(User $user): static
|
||||
{
|
||||
if (License::isCommunity()) {
|
||||
// With the Community license, all albums are accessible by all users.
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->join('songs', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('albums.id', 'songs.album_id')
|
||||
->where(static function (JoinClause $query) use ($user): void {
|
||||
$query->where('songs.owner_id', $user->id)
|
||||
->orWhere('songs.is_public', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
namespace App\Builders;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class ArtistBuilder extends Builder
|
||||
{
|
||||
|
@ -11,4 +14,20 @@ class ArtistBuilder extends Builder
|
|||
{
|
||||
return $this->whereNotIn('artists.id', [Artist::UNKNOWN_ID, Artist::VARIOUS_ID]);
|
||||
}
|
||||
|
||||
public function accessibleBy(User $user): static
|
||||
{
|
||||
if (License::isCommunity()) {
|
||||
// With the Community license, all artists are accessible by all users.
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->join('songs', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('artists.id', 'songs.artist_id')
|
||||
->where(static function (JoinClause $query) use ($user): void {
|
||||
$query->where('songs.owner_id', $user->id)
|
||||
->orWhere('songs.is_public', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,36 @@
|
|||
|
||||
namespace App\Builders;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @method self logSql()
|
||||
*/
|
||||
class SongBuilder extends Builder
|
||||
{
|
||||
public const SORT_COLUMNS_NORMALIZE_MAP = [
|
||||
'title' => 'songs.title',
|
||||
'track' => 'songs.track',
|
||||
'length' => 'songs.length',
|
||||
'created_at' => 'songs.created_at',
|
||||
'disc' => 'songs.disc',
|
||||
'artist_name' => 'artists.name',
|
||||
'album_name' => 'albums.name',
|
||||
];
|
||||
|
||||
private const VALID_SORT_COLUMNS = [
|
||||
'songs.title',
|
||||
'songs.track',
|
||||
'songs.length',
|
||||
'songs.created_at',
|
||||
'artists.name',
|
||||
'albums.name',
|
||||
];
|
||||
|
||||
public function inDirectory(string $path): static
|
||||
{
|
||||
// Make sure the path ends with a directory separator.
|
||||
|
@ -16,12 +40,15 @@ class SongBuilder extends Builder
|
|||
return $this->where('path', 'LIKE', "$path%");
|
||||
}
|
||||
|
||||
public function withMeta(User $user): static
|
||||
public function withMetaFor(User $user, bool $requiresInteractions = false): static
|
||||
{
|
||||
$joinType = $requiresInteractions ? 'join' : 'leftJoin';
|
||||
|
||||
return $this
|
||||
->with('artist', 'album', 'album.artist')
|
||||
->leftJoin('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
|
||||
->$joinType('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')
|
||||
->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->join('albums', 'songs.album_id', '=', 'albums.id')
|
||||
->join('artists', 'songs.artist_id', '=', 'artists.id')
|
||||
|
@ -34,6 +61,53 @@ class SongBuilder extends Builder
|
|||
);
|
||||
}
|
||||
|
||||
public function accessibleBy(User $user, bool $withTableName = true): static
|
||||
{
|
||||
if (License::isCommunity()) {
|
||||
// In the Community Edition, all songs are accessible by all users.
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->where(static function (Builder $query) use ($user, $withTableName): void {
|
||||
$query->where(($withTableName ? 'songs.' : '') . 'is_public', true)
|
||||
->orWhere(($withTableName ? 'songs.' : '') . 'owner_id', $user->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function sort(string $column, string $direction): static
|
||||
{
|
||||
$column = self::normalizeSortColumn($column);
|
||||
|
||||
Assert::oneOf($column, self::VALID_SORT_COLUMNS);
|
||||
Assert::oneOf(strtolower($direction), ['asc', 'desc']);
|
||||
|
||||
$this->orderBy($column, $direction);
|
||||
|
||||
if ($column === 'artists.name') {
|
||||
$this->orderBy('albums.name')
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
->orderBy('songs.title');
|
||||
} elseif ($column === 'albums.name') {
|
||||
$this->orderBy('artists.name')
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
->orderBy('songs.title');
|
||||
} elseif ($column === 'track') {
|
||||
$this->orderBy('songs.disc')
|
||||
->orderBy('songs.track');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private static function normalizeSortColumn(string $column): string
|
||||
{
|
||||
return key_exists($column, self::SORT_COLUMNS_NORMALIZE_MAP)
|
||||
? self::SORT_COLUMNS_NORMALIZE_MAP[$column]
|
||||
: $column;
|
||||
}
|
||||
|
||||
public function hostedOnS3(): static
|
||||
{
|
||||
return $this->where('path', 'LIKE', 's3://%');
|
||||
|
|
17
app/Facades/License.php
Normal file
17
app/Facades/License.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static bool isPlus()
|
||||
* @method static bool isCommunity()
|
||||
*/
|
||||
class License extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'License';
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Interaction;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Repositories\InteractionRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class RecentlyPlayedController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private InteractionRepository $interactionRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(?int $count = null)
|
||||
{
|
||||
return response()->json($this->interactionRepository->getRecentlyPlayed($this->user, $count));
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ class UploadController extends Controller
|
|||
UploadRequest $request,
|
||||
Authenticatable $user
|
||||
) {
|
||||
$this->authorize('admin', User::class);
|
||||
$this->authorize('upload', User::class);
|
||||
|
||||
try {
|
||||
$song = $songRepository->getOne($uploadService->handleUploadedFile($request->file)->id);
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Album;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
public function __construct(private DownloadService $downloadService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(Album $album)
|
||||
{
|
||||
return response()->download($this->downloadService->from($album));
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artist;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class ArtistController extends Controller
|
||||
{
|
||||
public function __construct(private DownloadService $downloadService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(Artist $artist)
|
||||
{
|
||||
return response()->download($this->downloadService->from($artist));
|
||||
}
|
||||
}
|
15
app/Http/Controllers/Download/DownloadAlbumController.php
Normal file
15
app/Http/Controllers/Download/DownloadAlbumController.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Album;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class DownloadAlbumController extends Controller
|
||||
{
|
||||
public function __invoke(Album $album, DownloadService $download)
|
||||
{
|
||||
return response()->download($download->from($album));
|
||||
}
|
||||
}
|
15
app/Http/Controllers/Download/DownloadArtistController.php
Normal file
15
app/Http/Controllers/Download/DownloadArtistController.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artist;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class DownloadArtistController extends Controller
|
||||
{
|
||||
public function __invoke(Artist $artist, DownloadService $download)
|
||||
{
|
||||
return response()->download($download->from($artist));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Repositories\InteractionRepository;
|
||||
use App\Services\DownloadService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class DownloadFavoritesController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __invoke(DownloadService $download, InteractionRepository $repository, Authenticatable $user)
|
||||
{
|
||||
return response()->download($download->from($repository->getUserFavorites($user)));
|
||||
}
|
||||
}
|
17
app/Http/Controllers/Download/DownloadPlaylistController.php
Normal file
17
app/Http/Controllers/Download/DownloadPlaylistController.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Playlist;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class DownloadPlaylistController extends Controller
|
||||
{
|
||||
public function __invoke(Playlist $playlist, DownloadService $download)
|
||||
{
|
||||
$this->authorize('own', $playlist);
|
||||
|
||||
return response()->download($download->from($playlist));
|
||||
}
|
||||
}
|
16
app/Http/Controllers/Download/DownloadSongsController.php
Normal file
16
app/Http/Controllers/Download/DownloadSongsController.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Download\DownloadSongsRequest;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class DownloadSongsController extends Controller
|
||||
{
|
||||
public function __invoke(DownloadSongsRequest $request, DownloadService $download, SongRepository $repository)
|
||||
{
|
||||
return response()->download($download->from($repository->getMany($request->songs)));
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Repositories\InteractionRepository;
|
||||
use App\Services\DownloadService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class FavoritesController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private DownloadService $downloadService,
|
||||
private InteractionRepository $interactionRepository,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$songs = $this->interactionRepository->getUserFavorites($this->user);
|
||||
|
||||
return response()->download($this->downloadService->from($songs));
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Playlist;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class PlaylistController extends Controller
|
||||
{
|
||||
public function __construct(private DownloadService $downloadService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(Playlist $playlist)
|
||||
{
|
||||
$this->authorize('own', $playlist);
|
||||
|
||||
return response()->download($this->downloadService->from($playlist));
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Download\SongRequest;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
class SongController extends Controller
|
||||
{
|
||||
public function __construct(private DownloadService $downloadService, private SongRepository $songRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(SongRequest $request)
|
||||
{
|
||||
$songs = $this->songRepository->getMany($request->songs);
|
||||
|
||||
return response()->download($this->downloadService->from($songs));
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\API\ViewSongOnITunesRequest;
|
||||
use App\Models\Album;
|
||||
use App\Services\ITunesService;
|
||||
use App\Services\TokenManager;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ITunesController extends Controller
|
||||
{
|
||||
public function __construct(private ITunesService $iTunesService, private TokenManager $tokenManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function viewSong(ViewSongOnITunesRequest $request, Album $album)
|
||||
{
|
||||
abort_unless(
|
||||
(bool) $this->tokenManager->getUserFromPlainTextToken($request->api_token),
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
|
||||
$url = $this->iTunesService->getTrackUrl($request->q, $album->name, $album->artist->name);
|
||||
abort_unless((bool) $url, 404, "Koel can't find such a song on iTunes Store.");
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
|
@ -8,13 +8,16 @@ use App\Models\Song;
|
|||
|
||||
class PlayController extends Controller
|
||||
{
|
||||
public function __construct(private StreamerFactory $streamerFactory)
|
||||
{
|
||||
}
|
||||
public function __invoke(
|
||||
StreamerFactory $streamerFactory,
|
||||
SongPlayRequest $request,
|
||||
Song $song,
|
||||
?bool $transcode = null,
|
||||
?int $bitRate = null
|
||||
) {
|
||||
$this->authorize('play', $song);
|
||||
|
||||
public function show(SongPlayRequest $request, Song $song, ?bool $transcode = null, ?int $bitRate = null)
|
||||
{
|
||||
return $this->streamerFactory
|
||||
return $streamerFactory
|
||||
->createStreamer($song, $transcode, $bitRate, (float) $request->time)
|
||||
->stream();
|
||||
}
|
||||
|
|
29
app/Http/Controllers/ViewSongOnITunesController.php
Normal file
29
app/Http/Controllers/ViewSongOnITunesController.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\API\ViewSongOnITunesRequest;
|
||||
use App\Models\Album;
|
||||
use App\Services\ITunesService;
|
||||
use App\Services\TokenManager;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ViewSongOnITunesController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
ViewSongOnITunesRequest $request,
|
||||
ITunesService $iTunesService,
|
||||
TokenManager $tokenManager,
|
||||
Album $album
|
||||
) {
|
||||
abort_unless(
|
||||
(bool) $tokenManager->getUserFromPlainTextToken($request->api_token),
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
|
||||
$url = $iTunesService->getTrackUrl($request->q, $album->name, $album->artist->name);
|
||||
abort_unless((bool) $url, Response::HTTP_NOT_FOUND, "Koel can't find such a song on iTunes Store.");
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests\API;
|
||||
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Builders\SongBuilder;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,7 @@ class FetchSongsForQueueRequest extends Request
|
|||
'limit' => 'required|integer|min:1',
|
||||
'sort' => [
|
||||
'required_unless:order,rand',
|
||||
Rule::in(array_keys(SongRepository::SORT_COLUMNS_NORMALIZE_MAP)),
|
||||
Rule::in(array_keys(SongBuilder::SORT_COLUMNS_NORMALIZE_MAP)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace App\Http\Requests\Download;
|
|||
/**
|
||||
* @property array $songs
|
||||
*/
|
||||
class SongRequest extends Request
|
||||
class DownloadSongsRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
|
@ -35,6 +35,7 @@ class SongResource extends JsonResource
|
|||
'genre' => $this->song->genre,
|
||||
'year' => $this->song->year,
|
||||
'created_at' => $this->song->created_at,
|
||||
'owner_id' => $this->song->owner_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use Laravel\Scout\Searchable;
|
|||
* @property string $path
|
||||
* @property string $title
|
||||
* @property Album $album
|
||||
* @property User $uploader
|
||||
* @property Artist $artist
|
||||
* @property Artist $album_artist
|
||||
* @property float $length
|
||||
|
@ -33,6 +34,8 @@ use Laravel\Scout\Searchable;
|
|||
* @property ?int $play_count The number of times the song has been played by the current user (dynamically calculated)
|
||||
* @property Carbon $created_at
|
||||
* @property array<mixed> $s3_params
|
||||
* @property int $owner_id
|
||||
* @property bool $is_public
|
||||
*/
|
||||
class Song extends Model
|
||||
{
|
||||
|
@ -52,6 +55,7 @@ class Song extends Model
|
|||
'mtime' => 'int',
|
||||
'track' => 'int',
|
||||
'disc' => 'int',
|
||||
'is_public' => 'bool',
|
||||
];
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
@ -66,6 +70,11 @@ class Song extends Model
|
|||
return parent::query();
|
||||
}
|
||||
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function newEloquentBuilder($query): SongBuilder
|
||||
{
|
||||
return new SongBuilder($query);
|
||||
|
@ -104,6 +113,11 @@ class Song extends Model
|
|||
);
|
||||
}
|
||||
|
||||
public function accessibleBy(User $user): bool
|
||||
{
|
||||
return $this->is_public || $this->owner_id === $user->id;
|
||||
}
|
||||
|
||||
protected function lyrics(): Attribute
|
||||
{
|
||||
$normalizer = static function (?string $value): string {
|
||||
|
|
15
app/Policies/SongPolicy.php
Normal file
15
app/Policies/SongPolicy.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
|
||||
class SongPolicy
|
||||
{
|
||||
public function play(User $user, Song $song): bool
|
||||
{
|
||||
return License::isCommunity() || $song->is_public || $song->owner_id === $user->id;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\User;
|
||||
|
||||
class UserPolicy
|
||||
|
@ -15,4 +16,11 @@ class UserPolicy
|
|||
{
|
||||
return $currentUser->is_admin && $currentUser->isNot($userToDestroy);
|
||||
}
|
||||
|
||||
public function upload(User $currentUser): bool
|
||||
{
|
||||
// For Community Edition, only admins can upload songs.
|
||||
// For Plus Edition, any user can upload songs (to their own library).
|
||||
return License::isCommunity() ? $currentUser->is_admin : true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistFolder;
|
||||
use App\Models\User;
|
||||
use App\Policies\PlaylistFolderPolicy;
|
||||
use App\Policies\PlaylistPolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use App\Services\TokenManager;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
|
@ -16,12 +11,6 @@ use Illuminate\Validation\Rules\Password;
|
|||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $policies = [
|
||||
Playlist::class => PlaylistPolicy::class,
|
||||
User::class => UserPolicy::class,
|
||||
PlaylistFolder::class => PlaylistFolderPolicy::class,
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
|
14
app/Providers/LicenseServiceProvider.php
Normal file
14
app/Providers/LicenseServiceProvider.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\LicenseService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class LicenseServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
app()->singleton('License', static fn (): LicenseService => app(LicenseService::class));
|
||||
}
|
||||
}
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class MacroProvider extends ServiceProvider
|
||||
|
@ -13,5 +15,12 @@ class MacroProvider extends ServiceProvider
|
|||
/** @var Collection $this */
|
||||
return $this->sortBy(static fn ($item) => array_search($item->$key, $orderBy, true));
|
||||
});
|
||||
|
||||
Builder::macro('logSql', function (): Builder {
|
||||
/** @var Builder $this */
|
||||
Log::info($this->toSql());
|
||||
|
||||
return $this;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,59 +2,91 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Traits\Searchable;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AlbumRepository extends Repository
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
/** @return Collection|array<array-key, Album> */
|
||||
public function getRecentlyAdded(int $count = 6): Collection
|
||||
public function getRecentlyAdded(int $count = 6, ?User $user = null): Collection
|
||||
{
|
||||
return Album::query()
|
||||
->isStandard()
|
||||
->latest('created_at')
|
||||
->accessibleBy($user ?? $this->auth->user())
|
||||
->groupBy('albums.id')
|
||||
->distinct()
|
||||
->latest('albums.created_at')
|
||||
->limit($count)
|
||||
->get();
|
||||
->get('albums.*');
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Album> */
|
||||
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user ??= $this->auth->user();
|
||||
|
||||
return Album::query()
|
||||
->leftJoin('songs', 'albums.id', 'songs.album_id')
|
||||
->leftJoin('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('songs.id', 'interactions.song_id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
$query = Album::query()
|
||||
->isStandard()
|
||||
->accessibleBy($user);
|
||||
|
||||
if (License::isCommunity()) {
|
||||
// if the license is Plus, accessibleBy() would have already joined the songs table
|
||||
// and we don't want to join it twice
|
||||
$query->leftJoin('songs', 'albums.id', 'songs.album_id');
|
||||
}
|
||||
|
||||
return $query->join('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('songs.id', 'interactions.song_id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->groupBy('albums.id')
|
||||
->distinct()
|
||||
->orderByDesc('play_count')
|
||||
->limit($count)
|
||||
->get('albums.*');
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Album> */
|
||||
public function getByArtist(Artist $artist): Collection
|
||||
public function getMany(array $ids, bool $inThatOrder = false, ?User $user = null): Collection
|
||||
{
|
||||
return Album::query()
|
||||
->where('artist_id', $artist->id)
|
||||
->orWhereIn('id', $artist->songs()->pluck('album_id'))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
$albums = Album::query()
|
||||
->isStandard()
|
||||
->accessibleBy($user ?? auth()->user())
|
||||
->whereIn('albums.id', $ids)
|
||||
->groupBy('albums.id')
|
||||
->distinct()
|
||||
->get('albums.*');
|
||||
|
||||
return $inThatOrder ? $albums->orderByArray($ids) : $albums;
|
||||
}
|
||||
|
||||
public function paginate(): Paginator
|
||||
/** @return Collection|array<array-key, Album> */
|
||||
public function getByArtist(Artist $artist, ?User $user = null): Collection
|
||||
{
|
||||
return Album::query()
|
||||
->accessibleBy($user ?? $this->auth->user())
|
||||
->where('albums.artist_id', $artist->id)
|
||||
->orWhereIn('albums.id', $artist->songs()->pluck('album_id'))
|
||||
->orderBy('albums.name')
|
||||
->groupBy('albums.id')
|
||||
->distinct()
|
||||
->get('albums.*');
|
||||
}
|
||||
|
||||
public function paginate(?User $user = null): Paginator
|
||||
{
|
||||
return Album::query()
|
||||
->accessibleBy($user ?? $this->auth->user())
|
||||
->isStandard()
|
||||
->orderBy('name')
|
||||
->orderBy('albums.name')
|
||||
->groupBy('albums.id')
|
||||
->distinct()
|
||||
->select('albums.*')
|
||||
->simplePaginate(21);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,27 +2,34 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Traits\Searchable;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class ArtistRepository extends Repository
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
/** @return Collection|array<array-key, Artist> */
|
||||
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user ??= auth()->user();
|
||||
|
||||
return Artist::query()
|
||||
->leftJoin('songs', 'artists.id', '=', 'songs.artist_id')
|
||||
->leftJoin('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
$query = Artist::query()
|
||||
->isStandard()
|
||||
->accessibleBy($user);
|
||||
|
||||
if (License::isCommunity()) {
|
||||
// if the license is Plus, accessibleBy() would have already joined the songs table
|
||||
// and we don't want to join it twice
|
||||
$query->leftJoin('songs', 'artists.id', 'songs.artist_id');
|
||||
}
|
||||
|
||||
return $query->join('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->groupBy([
|
||||
'artists.id',
|
||||
'play_count',
|
||||
|
@ -31,28 +38,35 @@ class ArtistRepository extends Repository
|
|||
'artists.created_at',
|
||||
'artists.updated_at',
|
||||
])
|
||||
->isStandard()
|
||||
->distinct()
|
||||
->orderByDesc('play_count')
|
||||
->limit($count)
|
||||
->get('artists.*');
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Artist> */
|
||||
public function getMany(array $ids, bool $inThatOrder = false): Collection
|
||||
public function getMany(array $ids, bool $inThatOrder = false, ?User $user = null): Collection
|
||||
{
|
||||
$artists = Artist::query()
|
||||
->isStandard()
|
||||
->whereIn('id', $ids)
|
||||
->get();
|
||||
->accessibleBy($user ?? auth()->user())
|
||||
->whereIn('artists.id', $ids)
|
||||
->groupBy('artists.id')
|
||||
->distinct()
|
||||
->get('artists.*');
|
||||
|
||||
return $inThatOrder ? $artists->orderByArray($ids) : $artists;
|
||||
}
|
||||
|
||||
public function paginate(): Paginator
|
||||
public function paginate(?User $user = null): Paginator
|
||||
{
|
||||
return Artist::query()
|
||||
->isStandard()
|
||||
->orderBy('name')
|
||||
->accessibleBy($user ?? auth()->user())
|
||||
->groupBy('artists.id')
|
||||
->distinct()
|
||||
->orderBy('artists.name')
|
||||
->select('artists.*')
|
||||
->simplePaginate(21);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Values\Genre;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
@ -10,9 +11,10 @@ use Illuminate\Support\Facades\DB;
|
|||
class GenreRepository
|
||||
{
|
||||
/** @return Collection|array<array-key, Genre> */
|
||||
public function getAll(): Collection
|
||||
public function getAll(?User $scopedUser = null): Collection
|
||||
{
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser ?? auth()->user())
|
||||
->select('genre', DB::raw('COUNT(id) AS song_count'), DB::raw('SUM(length) AS length'))
|
||||
->groupBy('genre')
|
||||
->orderBy('genre')
|
||||
|
@ -24,10 +26,11 @@ class GenreRepository
|
|||
));
|
||||
}
|
||||
|
||||
public function getOne(string $name): ?Genre
|
||||
public function getOne(string $name, ?User $scopedUser = null): ?Genre
|
||||
{
|
||||
/** @var object|null $record */
|
||||
$record = Song::query()
|
||||
->accessibleBy($scopedUser ?? auth()->user())
|
||||
->select('genre', DB::raw('COUNT(id) AS song_count'), DB::raw('SUM(length) AS length'))
|
||||
->groupBy('genre')
|
||||
->where('genre', $name === Genre::NO_GENRE ? '' : $name)
|
||||
|
|
|
@ -4,19 +4,14 @@ namespace App\Repositories;
|
|||
|
||||
use App\Models\Interaction;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Traits\ByCurrentUser;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InteractionRepository extends Repository
|
||||
{
|
||||
use ByCurrentUser;
|
||||
|
||||
/** @return Collection|array<Interaction> */
|
||||
public function getUserFavorites(User $user): Collection
|
||||
{
|
||||
return $this->model
|
||||
->newQuery()
|
||||
return Interaction::query()
|
||||
->where([
|
||||
'user_id' => $user->id,
|
||||
'liked' => true,
|
||||
|
@ -24,17 +19,4 @@ class InteractionRepository extends Repository
|
|||
->with('song')
|
||||
->pluck('song');
|
||||
}
|
||||
|
||||
/** @return array<Interaction> */
|
||||
public function getRecentlyPlayed(User $user, ?int $count = null): array
|
||||
{
|
||||
return $this->model
|
||||
->newQuery()
|
||||
->where('user_id', $user->id)
|
||||
->where('play_count', '>', 0)
|
||||
->latest('last_played_at')
|
||||
->when($count, static fn (Builder $query, int $count) => $query->take($count))
|
||||
->pluck('song_id')
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Repositories\Traits\ByCurrentUser;
|
||||
|
||||
class PlaylistFolderRepository extends Repository
|
||||
{
|
||||
use ByCurrentUser;
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Playlist;
|
||||
use App\Repositories\Traits\ByCurrentUser;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PlaylistRepository extends Repository
|
||||
{
|
||||
use ByCurrentUser;
|
||||
|
||||
/** @return Collection|array<Playlist> */
|
||||
public function getAllByCurrentUser(): Collection
|
||||
{
|
||||
return $this->byCurrentUser()->orderBy('name')->get();
|
||||
}
|
||||
}
|
|
@ -7,36 +7,12 @@ use App\Models\Artist;
|
|||
use App\Models\Playlist;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Traits\Searchable;
|
||||
use App\Values\Genre;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class SongRepository extends Repository
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
public const SORT_COLUMNS_NORMALIZE_MAP = [
|
||||
'title' => 'songs.title',
|
||||
'track' => 'songs.track',
|
||||
'length' => 'songs.length',
|
||||
'created_at' => 'songs.created_at',
|
||||
'disc' => 'songs.disc',
|
||||
'artist_name' => 'artists.name',
|
||||
'album_name' => 'albums.name',
|
||||
];
|
||||
|
||||
private const VALID_SORT_COLUMNS = [
|
||||
'songs.title',
|
||||
'songs.track',
|
||||
'songs.length',
|
||||
'songs.created_at',
|
||||
'artists.name',
|
||||
'albums.name',
|
||||
];
|
||||
|
||||
private const DEFAULT_QUEUE_LIMIT = 500;
|
||||
|
||||
public function getOneByPath(string $path): ?Song
|
||||
|
@ -53,14 +29,26 @@ class SongRepository extends Repository
|
|||
/** @return Collection|array<array-key, Song> */
|
||||
public function getRecentlyAdded(int $count = 10, ?User $scopedUser = null): Collection
|
||||
{
|
||||
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->latest()->limit($count)->get();
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->latest()
|
||||
->limit($count)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Song> */
|
||||
public function getMostPlayed(int $count = 7, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser, requiresInteractions: true)
|
||||
->where('interactions.play_count', '>', 0)
|
||||
->orderByDesc('interactions.play_count')
|
||||
->limit($count)
|
||||
|
@ -70,8 +58,12 @@ class SongRepository extends Repository
|
|||
/** @return Collection|array<array-key, Song> */
|
||||
public function getRecentlyPlayed(int $count = 7, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser, requiresInteractions: true)
|
||||
->orderByDesc('interactions.last_played_at')
|
||||
->limit($count)
|
||||
->get();
|
||||
|
@ -83,11 +75,13 @@ class SongRepository extends Repository
|
|||
?User $scopedUser = null,
|
||||
int $perPage = 50
|
||||
): Paginator {
|
||||
return self::applySort(
|
||||
Song::query()->withMeta($scopedUser ?? $this->auth->user()),
|
||||
$sortColumn,
|
||||
$sortDirection
|
||||
)
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->sort($sortColumn, $sortDirection)
|
||||
->simplePaginate($perPage);
|
||||
}
|
||||
|
||||
|
@ -98,13 +92,14 @@ class SongRepository extends Repository
|
|||
?User $scopedUser = null,
|
||||
int $perPage = 50
|
||||
): Paginator {
|
||||
return self::applySort(
|
||||
Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->where('genre', $genre),
|
||||
$sortColumn,
|
||||
$sortDirection
|
||||
)
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->where('genre', $genre)
|
||||
->sort($sortColumn, $sortDirection)
|
||||
->simplePaginate($perPage);
|
||||
}
|
||||
|
||||
|
@ -115,26 +110,39 @@ class SongRepository extends Repository
|
|||
int $limit = self::DEFAULT_QUEUE_LIMIT,
|
||||
?User $scopedUser = null,
|
||||
): Collection {
|
||||
return self::applySort(
|
||||
Song::query()->withMeta($scopedUser ?? $this->auth->user()),
|
||||
$sortColumn,
|
||||
$sortDirection
|
||||
)
|
||||
->limit($limit)
|
||||
->get();
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->sort($sortColumn, $sortDirection)
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Song> */
|
||||
public function getFavorites(?User $scopedUser = null): Collection
|
||||
{
|
||||
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->where('interactions.liked', true)->get();
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->where('interactions.liked', true)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Song> */
|
||||
public function getByAlbum(Album $album, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->where('album_id', $album->id)
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
|
@ -145,8 +153,12 @@ class SongRepository extends Repository
|
|||
/** @return Collection|array<array-key, Song> */
|
||||
public function getByArtist(Artist $artist, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->where('songs.artist_id', $artist->id)
|
||||
->orWhere('albums.artist_id', $artist->id)
|
||||
->orderBy('albums.name')
|
||||
|
@ -159,8 +171,12 @@ class SongRepository extends Repository
|
|||
/** @return Collection|array<array-key, Song> */
|
||||
public function getByStandardPlaylist(Playlist $playlist, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->leftJoin('playlist_song', 'songs.id', '=', 'playlist_song.song_id')
|
||||
->leftJoin('playlists', 'playlists.id', '=', 'playlist_song.playlist_id')
|
||||
->where('playlists.id', $playlist->id)
|
||||
|
@ -171,14 +187,26 @@ class SongRepository extends Repository
|
|||
/** @return Collection|array<array-key, Song> */
|
||||
public function getRandom(int $limit, ?User $scopedUser = null): Collection
|
||||
{
|
||||
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->inRandomOrder()->limit($limit)->get();
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->inRandomOrder()
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Song> */
|
||||
public function getMany(array $ids, bool $inThatOrder = false, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
$songs = Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->whereIn('songs.id', $ids)
|
||||
->get();
|
||||
|
||||
|
@ -187,63 +215,45 @@ class SongRepository extends Repository
|
|||
|
||||
public function getOne($id, ?User $scopedUser = null): Song
|
||||
{
|
||||
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->findOrFail($id);
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
public function findOne($id, ?User $scopedUser = null): ?Song
|
||||
{
|
||||
return Song::query()->withMeta($scopedUser ?? $this->auth->user())->find($id);
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser ?? $this->auth->user())
|
||||
->find($id);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
public function count(?User $scopedUser = null): int
|
||||
{
|
||||
return Song::query()->count();
|
||||
return Song::query()->accessibleBy($scopedUser ?? auth()->user())->count();
|
||||
}
|
||||
|
||||
public function getTotalLength(): float
|
||||
public function getTotalLength(?User $scopedUser = null): float
|
||||
{
|
||||
return Song::query()->sum('length');
|
||||
}
|
||||
|
||||
private static function normalizeSortColumn(string $column): string
|
||||
{
|
||||
return key_exists($column, self::SORT_COLUMNS_NORMALIZE_MAP)
|
||||
? self::SORT_COLUMNS_NORMALIZE_MAP[$column]
|
||||
: $column;
|
||||
}
|
||||
|
||||
private static function applySort(Builder $query, string $column, string $direction): Builder
|
||||
{
|
||||
$column = self::normalizeSortColumn($column);
|
||||
|
||||
Assert::oneOf($column, self::VALID_SORT_COLUMNS);
|
||||
Assert::oneOf(strtolower($direction), ['asc', 'desc']);
|
||||
|
||||
$query->orderBy($column, $direction);
|
||||
|
||||
if ($column === 'artists.name') {
|
||||
$query->orderBy('albums.name')
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
->orderBy('songs.title');
|
||||
} elseif ($column === 'albums.name') {
|
||||
$query->orderBy('artists.name')
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
->orderBy('songs.title');
|
||||
} elseif ($column === 'track') {
|
||||
$query->orderBy('song.disc')
|
||||
->orderBy('songs.track');
|
||||
}
|
||||
|
||||
return $query;
|
||||
return Song::query()->accessibleBy($scopedUser ?? auth()->user())->sum('length');
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Song> */
|
||||
public function getRandomByGenre(string $genre, int $limit, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->withMeta($scopedUser ?? $this->auth->user())
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->where('genre', $genre === Genre::NO_GENRE ? '' : $genre)
|
||||
->limit($limit)
|
||||
->inRandomOrder()
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repositories\Traits;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait ByCurrentUser
|
||||
{
|
||||
private function byCurrentUser(): Builder
|
||||
{
|
||||
return $this->model->whereUserId($this->auth->id());
|
||||
}
|
||||
|
||||
/** @return Collection|array<Model> */
|
||||
public function getAllByCurrentUser(): Collection
|
||||
{
|
||||
return $this->byCurrentUser()->get();
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repositories\Traits;
|
||||
|
||||
use Laravel\Scout\Builder;
|
||||
|
||||
trait Searchable
|
||||
{
|
||||
public function search(string $keywords): Builder
|
||||
{
|
||||
return forward_static_call([$this->getModelClass(), 'search'], $keywords);
|
||||
}
|
||||
}
|
11
app/Services/CommunityLicenseService.php
Normal file
11
app/Services/CommunityLicenseService.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class CommunityLicenseService extends LicenseService
|
||||
{
|
||||
public function isPlus(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
17
app/Services/LicenseService.php
Normal file
17
app/Services/LicenseService.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class LicenseService
|
||||
{
|
||||
public function isPlus(): bool
|
||||
{
|
||||
// @todo Implement checking for Plus license
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isCommunity(): bool
|
||||
{
|
||||
return !$this->isPlus();
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ class SearchService
|
|||
): Collection {
|
||||
return Song::search($keywords)
|
||||
->query(static function (SongBuilder $builder) use ($scopedUser, $limit): void {
|
||||
$builder->withMeta($scopedUser ?? auth()->user())->limit($limit);
|
||||
$builder->withMetaFor($scopedUser ?? auth()->user())->limit($limit);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class SmartPlaylistService
|
|||
{
|
||||
throw_unless($playlist->is_smart, NonSmartPlaylistException::create($playlist));
|
||||
|
||||
$query = Song::query()->withMeta($user ?? $playlist->user);
|
||||
$query = Song::query()->withMetaFor($user ?? $playlist->user);
|
||||
|
||||
$playlist->rule_groups->each(static function (RuleGroup $group, int $index) use ($query): void {
|
||||
$clause = $index === 0 ? 'where' : 'orWhere';
|
||||
|
|
|
@ -146,6 +146,7 @@ return [
|
|||
App\Providers\StreamerServiceProvider::class,
|
||||
App\Providers\ObjectStorageServiceProvider::class,
|
||||
App\Providers\MacroProvider::class,
|
||||
App\Providers\LicenseServiceProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -199,5 +200,6 @@ return [
|
|||
'YouTube' => App\Facades\YouTube::class,
|
||||
'Download' => App\Facades\Download::class,
|
||||
'ITunes' => App\Facades\ITunes::class,
|
||||
'License' => App\Facades\License::class,
|
||||
],
|
||||
];
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('songs', static function (Blueprint $table): void {
|
||||
$table->unsignedInteger('owner_id')->nullable();
|
||||
$table->foreign('owner_id')->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete();
|
||||
$table->boolean('is_public')->default(false)->index();
|
||||
});
|
||||
|
||||
Schema::table('songs', static function (Blueprint $table): void {
|
||||
/** @var ?User $firstAdmin */
|
||||
$firstAdmin = User::query()->where('is_admin', true)->oldest()->first();
|
||||
|
||||
if ($firstAdmin === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure all existing songs are accessible by all users and assuming the first admin "owns" them
|
||||
Song::query()->update([
|
||||
'is_public' => true,
|
||||
'owner_id' => $firstAdmin->id,
|
||||
]);
|
||||
|
||||
$table->unsignedInteger('owner_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,14 +1,14 @@
|
|||
<?php
|
||||
|
||||
use App\Facades\ITunes;
|
||||
use App\Http\Controllers\Download\AlbumController as AlbumDownloadController;
|
||||
use App\Http\Controllers\Download\ArtistController as ArtistDownloadController;
|
||||
use App\Http\Controllers\Download\FavoritesController as FavoritesDownloadController;
|
||||
use App\Http\Controllers\Download\PlaylistController as PlaylistDownloadController;
|
||||
use App\Http\Controllers\Download\SongController as SongDownloadController;
|
||||
use App\Http\Controllers\ITunesController;
|
||||
use App\Http\Controllers\Download\DownloadAlbumController;
|
||||
use App\Http\Controllers\Download\DownloadArtistController;
|
||||
use App\Http\Controllers\Download\DownloadFavoritesController;
|
||||
use App\Http\Controllers\Download\DownloadPlaylistController;
|
||||
use App\Http\Controllers\Download\DownloadSongsController;
|
||||
use App\Http\Controllers\LastfmController;
|
||||
use App\Http\Controllers\PlayController;
|
||||
use App\Http\Controllers\ViewSongOnITunesController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('web')->group(static function (): void {
|
||||
|
@ -23,19 +23,19 @@ Route::middleware('web')->group(static function (): void {
|
|||
});
|
||||
|
||||
if (ITunes::used()) {
|
||||
Route::get('itunes/song/{album}', [ITunesController::class, 'viewSong'])->name('iTunes.viewSong');
|
||||
Route::get('itunes/song/{album}', ViewSongOnITunesController::class)->name('iTunes.viewSong');
|
||||
}
|
||||
});
|
||||
|
||||
Route::middleware('audio.auth')->group(static function (): void {
|
||||
Route::get('play/{song}/{transcode?}/{bitrate?}', [PlayController::class, 'show'])->name('song.play');
|
||||
Route::get('play/{song}/{transcode?}/{bitrate?}', PlayController::class)->name('song.play');
|
||||
|
||||
Route::prefix('download')->group(static function (): void {
|
||||
Route::get('songs', [SongDownloadController::class, 'show']);
|
||||
Route::get('album/{album}', [AlbumDownloadController::class, 'show']);
|
||||
Route::get('artist/{artist}', [ArtistDownloadController::class, 'show']);
|
||||
Route::get('playlist/{playlist}', [PlaylistDownloadController::class, 'show']);
|
||||
Route::get('favorites', [FavoritesDownloadController::class, 'show']);
|
||||
Route::get('songs', DownloadSongsController::class);
|
||||
Route::get('album/{album}', DownloadAlbumController::class);
|
||||
Route::get('artist/{artist}', DownloadArtistController::class);
|
||||
Route::get('playlist/{playlist}', DownloadPlaylistController::class);
|
||||
Route::get('favorites', DownloadFavoritesController::class);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace Tests;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use App\Services\CommunityLicenseService;
|
||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
@ -24,6 +26,8 @@ abstract class TestCase extends BaseTestCase
|
|||
{
|
||||
parent::setUp();
|
||||
|
||||
License::swap($this->app->make(CommunityLicenseService::class));
|
||||
|
||||
TestResponse::macro('log', function (string $file = 'test-response.json'): TestResponse {
|
||||
/** @var TestResponse $this */
|
||||
file_put_contents(storage_path('logs/' . $file), $this->getContent());
|
||||
|
|
Loading…
Reference in a new issue