diff --git a/api-docs/5.0.0/reference/api.yaml b/api-docs/5.0.0/reference/api.v5.yaml similarity index 95% rename from api-docs/5.0.0/reference/api.yaml rename to api-docs/5.0.0/reference/api.v5.yaml index ea6e0bef..854f48e7 100644 --- a/api-docs/5.0.0/reference/api.yaml +++ b/api-docs/5.0.0/reference/api.v5.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: Koel API version: 5.0.0 - description: 'The API for [Koel](http://koel.dev), the music streaming application that works.' + description: 'The API for [Koel](https://koel.dev), the music streaming application that works.' contact: name: An Phan url: 'https://phanan.net' @@ -11,7 +11,7 @@ info: name: MIT url: 'https://github.com/koel/koel/blob/master/LICENSE.md' servers: - - url: 'http://localhost:8000' + - url: 'https://koel.test' description: Local tags: - name: interaction @@ -852,8 +852,7 @@ paths: $ref: '#/components/schemas/Song' operationId: post-os-s3-song description: Create a new song or update an existing one with data sent from AWS - security: - - appKey: [] + security: [] requestBody: content: application/json: @@ -908,8 +907,7 @@ paths: '204': description: No Content description: Remove a song whose information matches the data sent from AWS S3 (`bucket` and `key`) - security: - - appKey: [] + security: [] requestBody: content: application/json: @@ -1103,6 +1101,84 @@ paths: description: Download a whole playlist. This is NOT an XmlHttpRequest. The response will be a download response of either one media file or a zip file containg multiple media files. security: - api-token: [] + /api/search: + get: + summary: 'Search for songs, albums, and artists' + tags: + - search + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + results: + type: object + required: + - songs + - artists + - albums + properties: + songs: + type: array + description: An array of max 6 best-matching songs' IDs + items: + type: string + artists: + type: array + description: An array of max 6 best-matching artists' IDs + items: + type: integer + albums: + type: array + description: An array of max 6 best-matching albums' IDs + items: + type: number + required: + - results + operationId: get-api-search + description: 'Seach for songs, albums, and artists, with a maximum of {count} results each.' + security: + - Bearer Token: [] + parameters: + - schema: + type: string + in: query + name: q + description: The keywords to search + required: true + - schema: + type: integer + minimum: 1 + default: 6 + in: query + name: count + description: 'The maximum number of results for songs, artists, and albums' + parameters: [] + /api/search/songs: + get: + summary: Search for songs + tags: + - search + responses: {} + operationId: get-api-search-songs + description: Get all songs that matches a search query. + security: + - Bearer Token: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + songs: + type: array + description: An array of matching songs' IDs + items: {} + required: + - songs components: schemas: User: @@ -1638,10 +1714,6 @@ components: Bearer Token: type: http scheme: bearer - appKey: - name: The applcation key (APP_KEY in .env) - type: apiKey - in: query api-token: name: The API token as a query parameter type: apiKey diff --git a/app/Console/Commands/ImportSearchableEntitiesCommand.php b/app/Console/Commands/ImportSearchableEntitiesCommand.php new file mode 100644 index 00000000..3436e20c --- /dev/null +++ b/app/Console/Commands/ImportSearchableEntitiesCommand.php @@ -0,0 +1,33 @@ +call('scout:import', ['model' => $entity]); + } + + return 0; + } +} diff --git a/app/Http/Controllers/API/Search/ExcerptSearchController.php b/app/Http/Controllers/API/Search/ExcerptSearchController.php new file mode 100644 index 00000000..d50e5beb --- /dev/null +++ b/app/Http/Controllers/API/Search/ExcerptSearchController.php @@ -0,0 +1,35 @@ +searchService = $searchService; + } + + public function index(Request $request) + { + if (!$request->get('q')) { + throw new InvalidArgumentException('A search query is required.'); + } + + $count = (int) $request->get('count', SearchService::DEFAULT_EXCERPT_RESULT_COUNT); + + if ($count < 0) { + throw new InvalidArgumentException('Invalid count parameter.'); + } + + return [ + 'results' => $this->searchService->excerptSearch($request->get('q'), $count), + ]; + } +} diff --git a/app/Http/Controllers/API/Search/SongSearchController.php b/app/Http/Controllers/API/Search/SongSearchController.php new file mode 100644 index 00000000..255c475d --- /dev/null +++ b/app/Http/Controllers/API/Search/SongSearchController.php @@ -0,0 +1,29 @@ +searchService = $searchService; + } + + public function index(Request $request) + { + if (!$request->get('q')) { + throw new InvalidArgumentException('A search query is required.'); + } + + return [ + 'songs' => $this->searchService->searchSongs($request->get('q')), + ]; + } +} diff --git a/app/Models/Album.php b/app/Models/Album.php index d5b60ca4..aba85bdf 100644 --- a/app/Models/Album.php +++ b/app/Models/Album.php @@ -9,22 +9,23 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Laravel\Scout\Searchable; /** - * @property string $cover The album cover's file name - * @property string|null $cover_path The absolute path to the cover file - * @property bool $has_cover If the album has a non-default cover image + * @property string $cover The album cover's file name + * @property string|null $cover_path The absolute path to the cover file + * @property bool $has_cover If the album has a non-default cover image * @property int $id - * @property string $name Name of the album + * @property string $name Name of the album * @property bool $is_compilation If the album is a compilation from multiple artists - * @property Artist $artist The album's artist + * @property Artist $artist The album's artist * @property int $artist_id * @property Collection $songs - * @property bool $is_unknown If the album is the Unknown Album + * @property bool $is_unknown If the album is the Unknown Album * @property string|null $thumbnail_name The file name of the album's thumbnail * @property string|null $thumbnail_path The full path to the thumbnail. * Notice that this doesn't guarantee the thumbnail exists. - * @property string|null $thumbnail The public URL to the album's thumbnail + * @property string|null $thumbnail The public URL to the album's thumbnail * * @method static self firstOrCreate(array $where, array $params = []) * @method static self|null find(int $id) @@ -35,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class Album extends Model { use HasFactory; + use Searchable; use SupportsDeleteWhereIDsNotIn; public const UNKNOWN_ID = 1; @@ -144,4 +146,19 @@ class Album extends Model { return $this->thumbnail_name ? album_cover_url($this->thumbnail_name) : null; } + + /** @return array */ + public function toSearchableArray(): array + { + $array = [ + 'id' => $this->id, + 'name' => $this->name, + ]; + + if (!$this->artist->is_unknown && !$this->artist->is_various) { + $array['artist'] = $this->artist->name; + } + + return $array; + } } diff --git a/app/Models/Artist.php b/app/Models/Artist.php index f4556a3d..aeac616a 100644 --- a/app/Models/Artist.php +++ b/app/Models/Artist.php @@ -10,15 +10,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Laravel\Scout\Searchable; /** - * @property int $id - * @property string $name - * @property string|null $image Public URL to the artist's image - * @property bool $is_unknown If the artist is Unknown Artist - * @property bool $is_various If the artist is Various Artist - * @property Collection $songs - * @property bool $has_image If the artist has a (non-default) image + * @property int $id + * @property string $name + * @property string|null $image Public URL to the artist's image + * @property bool $is_unknown If the artist is Unknown Artist + * @property bool $is_various If the artist is Various Artist + * @property Collection $songs + * @property bool $has_image If the artist has a (non-default) image * @property string|null $image_path Absolute path to the artist's image * * @method static self find(int $id) @@ -31,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Artist extends Model { use HasFactory; + use Searchable; use SupportsDeleteWhereIDsNotIn; public const UNKNOWN_ID = 1; @@ -122,4 +124,13 @@ class Artist extends Model return file_exists(artist_image_path($image)); } + + /** @return array */ + public function toSearchableArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + ]; + } } diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index 35289dce..62620f53 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -11,13 +11,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** - * @property int $user_id + * @property int $user_id * @property Collection $songs - * @property int $id - * @property array $rules - * @property bool $is_smart - * @property string $name - * @property user $user + * @property int $id + * @property array $rules + * @property bool $is_smart + * @property string $name + * @property user $user * * @method static Builder orderBy(string $field, string $order = 'asc') */ diff --git a/app/Models/Song.php b/app/Models/Song.php index 38027c45..98625da5 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; +use Laravel\Scout\Searchable; /** * @property string $path @@ -41,6 +42,7 @@ use Illuminate\Support\Collection; class Song extends Model { use HasFactory; + use Searchable; use SupportsDeleteWhereIDsNotIn; protected $guarded = []; @@ -241,6 +243,21 @@ class Song extends Model return compact('bucket', 'key'); } + /** @return array */ + public function toSearchableArray(): array + { + $array = [ + 'id' => $this->id, + 'title' => $this->title, + ]; + + if (!$this->artist->is_unknown && !$this->artist->is_various) { + $array['artist'] = $this->artist->name; + } + + return $array; + } + public function __toString(): string { return $this->id; diff --git a/app/Repositories/AbstractRepository.php b/app/Repositories/AbstractRepository.php index 8e59b97e..3dfad223 100644 --- a/app/Repositories/AbstractRepository.php +++ b/app/Repositories/AbstractRepository.php @@ -9,17 +9,19 @@ use Throwable; abstract class AbstractRepository implements RepositoryInterface { + /** @var string */ + private $modelClass; + /** @var Model */ protected $model; /** @var Guard */ protected $auth; - abstract public function getModelClass(): string; - - public function __construct() + public function __construct(?string $modelClass = null) { - $this->model = app($this->getModelClass()); + $this->modelClass = $modelClass ?: self::guessModelClass(); + $this->model = app($this->modelClass); // This instantiation may fail during a console command if e.g. APP_KEY is empty, // rendering the whole installation failing. @@ -29,6 +31,11 @@ abstract class AbstractRepository implements RepositoryInterface } } + private static function guessModelClass(): string + { + return preg_replace('/(.+)\\\\Repositories\\\\(.+)Repository$/m', '$1\Models\\\$2', static::class); + } + public function getOneById($id): ?Model { return $this->model->find($id); @@ -50,4 +57,9 @@ abstract class AbstractRepository implements RepositoryInterface { return $this->model->where(...$params)->first(); } + + public function getModelClass(): string + { + return $this->modelClass; + } } diff --git a/app/Repositories/AlbumRepository.php b/app/Repositories/AlbumRepository.php index 0e974e4a..dfd4dfce 100644 --- a/app/Repositories/AlbumRepository.php +++ b/app/Repositories/AlbumRepository.php @@ -2,15 +2,12 @@ namespace App\Repositories; -use App\Models\Album; use App\Models\Song; +use App\Repositories\Traits\Searchable; class AlbumRepository extends AbstractRepository { - public function getModelClass(): string - { - return Album::class; - } + use Searchable; /** @return array */ public function getNonEmptyAlbumIds(): array diff --git a/app/Repositories/ArtistRepository.php b/app/Repositories/ArtistRepository.php index c6b98256..6e74c3e7 100644 --- a/app/Repositories/ArtistRepository.php +++ b/app/Repositories/ArtistRepository.php @@ -2,15 +2,12 @@ namespace App\Repositories; -use App\Models\Artist; use App\Models\Song; +use App\Repositories\Traits\Searchable; class ArtistRepository extends AbstractRepository { - public function getModelClass(): string - { - return Artist::class; - } + use Searchable; /** @return array */ public function getNonEmptyArtistIds(): array diff --git a/app/Repositories/InteractionRepository.php b/app/Repositories/InteractionRepository.php index a230ce6b..ba1ec0d2 100644 --- a/app/Repositories/InteractionRepository.php +++ b/app/Repositories/InteractionRepository.php @@ -12,11 +12,6 @@ class InteractionRepository extends AbstractRepository { use ByCurrentUser; - public function getModelClass(): string - { - return Interaction::class; - } - /** @return Collection|array */ public function getUserFavorites(User $user): Collection { diff --git a/app/Repositories/PlaylistRepository.php b/app/Repositories/PlaylistRepository.php index 10dbdf3e..79f000c6 100644 --- a/app/Repositories/PlaylistRepository.php +++ b/app/Repositories/PlaylistRepository.php @@ -10,11 +10,6 @@ class PlaylistRepository extends AbstractRepository { use ByCurrentUser; - public function getModelClass(): string - { - return Playlist::class; - } - /** @return Collection|array */ public function getAllByCurrentUser(): Collection { diff --git a/app/Repositories/RepositoryInterface.php b/app/Repositories/RepositoryInterface.php index 591fd4dd..c0b1ec38 100644 --- a/app/Repositories/RepositoryInterface.php +++ b/app/Repositories/RepositoryInterface.php @@ -7,8 +7,6 @@ use Illuminate\Database\Eloquent\Model; interface RepositoryInterface { - public function getModelClass(): string; - public function getOneById($id): ?Model; /** @return Collection|array */ diff --git a/app/Repositories/SettingRepository.php b/app/Repositories/SettingRepository.php index 5d459673..2666b3ae 100644 --- a/app/Repositories/SettingRepository.php +++ b/app/Repositories/SettingRepository.php @@ -2,15 +2,8 @@ namespace App\Repositories; -use App\Models\Setting; - class SettingRepository extends AbstractRepository { - public function getModelClass(): string - { - return Setting::class; - } - /** @return array */ public function getAllAsKeyValueArray(): array { diff --git a/app/Repositories/SongRepository.php b/app/Repositories/SongRepository.php index d91a2c51..4bfd865c 100644 --- a/app/Repositories/SongRepository.php +++ b/app/Repositories/SongRepository.php @@ -3,10 +3,13 @@ namespace App\Repositories; use App\Models\Song; +use App\Repositories\Traits\Searchable; use App\Services\HelperService; class SongRepository extends AbstractRepository { + use Searchable; + private $helperService; public function __construct(HelperService $helperService) @@ -16,11 +19,6 @@ class SongRepository extends AbstractRepository $this->helperService = $helperService; } - public function getModelClass(): string - { - return Song::class; - } - public function getOneByPath(string $path): ?Song { return $this->getOneById($this->helperService->getFileHash($path)); diff --git a/app/Repositories/Traits/Searchable.php b/app/Repositories/Traits/Searchable.php new file mode 100644 index 00000000..6577726d --- /dev/null +++ b/app/Repositories/Traits/Searchable.php @@ -0,0 +1,13 @@ +getModelClass(), 'search'], $keywords); + } +} diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index dbcba735..f922cb43 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -2,12 +2,6 @@ namespace App\Repositories; -use App\Models\User; - class UserRepository extends AbstractRepository { - public function getModelClass(): string - { - return User::class; - } } diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php new file mode 100644 index 00000000..3e767b11 --- /dev/null +++ b/app/Services/SearchService.php @@ -0,0 +1,68 @@ +songRepository = $songRepository; + $this->albumRepository = $albumRepository; + $this->artistRepository = $artistRepository; + } + + /** @return array */ + public function excerptSearch(string $keywords, int $count): array + { + return [ + 'songs' => self::getTopResults($this->songRepository->search($keywords), $count) + ->map(static function (Song $song): string { + return $song->id; + }), + 'artists' => self::getTopResults($this->artistRepository->search($keywords), $count) + ->map(static function (Artist $artist): int { + return $artist->id; + }), + 'albums' => self::getTopResults($this->albumRepository->search($keywords), $count) + ->map(static function (Album $album): int { + return $album->id; + }), + ]; + } + + /** @return Collection|array */ + private static function getTopResults(Builder $query, int $count): Collection + { + return $query->take($count)->get(); + } + + /** @return Collection|array */ + public function searchSongs(string $keywords): Collection + { + return $this->songRepository + ->search($keywords) + ->get() + ->map(static function (Song $song): string { + return $song->id; + }); + } +} diff --git a/composer.json b/composer.json index 2bc743c5..141aacfc 100644 --- a/composer.json +++ b/composer.json @@ -26,9 +26,11 @@ "intervention/image": "^2.5", "laravel/sanctum": "^2.6", "doctrine/dbal": "^2.10", - "lstrojny/functional-php": "^1.14" + "lstrojny/functional-php": "^1.14", + "teamtnt/laravel-scout-tntsearch-driver": "^11.1" }, "require-dev": { + "facade/ignition": "^2.5", "mockery/mockery": "~1.0", "phpunit/phpunit": "^9.0", "laravel/tinker": "^2.0", diff --git a/composer.lock b/composer.lock index 3ca136c2..698f7571 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "becead0effd39fcac75e3efa67c6f3af", + "content-hash": "7b7d97f1f8c3818ebfed25d4e6ccf591", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.171.3", + "version": "3.171.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7469c8e7f8db5dec544937677d08d8fff4f2f4e9" + "reference": "848923bc9ce5f8c5ede2df7175b090e382bba699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7469c8e7f8db5dec544937677d08d8fff4f2f4e9", - "reference": "7469c8e7f8db5dec544937677d08d8fff4f2f4e9", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/848923bc9ce5f8c5ede2df7175b090e382bba699", + "reference": "848923bc9ce5f8c5ede2df7175b090e382bba699", "shasum": "" }, "require": { @@ -92,9 +92,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.171.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.171.5" }, - "time": "2020-12-21T19:14:02+00:00" + "time": "2020-12-22T21:56:27+00:00" }, { "name": "aws/aws-sdk-php-laravel", @@ -1379,16 +1379,16 @@ }, { "name": "laravel/framework", - "version": "v8.20.0", + "version": "v8.20.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "130168c9dcd399f6b42bccfe4882b88c81a4edfe" + "reference": "b5d8573ab16027867eaa1ac148893833434f9b02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/130168c9dcd399f6b42bccfe4882b88c81a4edfe", - "reference": "130168c9dcd399f6b42bccfe4882b88c81a4edfe", + "url": "https://api.github.com/repos/laravel/framework/zipball/b5d8573ab16027867eaa1ac148893833434f9b02", + "reference": "b5d8573ab16027867eaa1ac148893833434f9b02", "shasum": "" }, "require": { @@ -1542,7 +1542,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2020-12-22T17:01:04+00:00" + "time": "2020-12-22T21:21:19+00:00" }, { "name": "laravel/helpers", @@ -1664,6 +1664,75 @@ }, "time": "2020-11-24T17:31:19+00:00" }, + { + "name": "laravel/scout", + "version": "v8.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/scout.git", + "reference": "b8b8a35cc3ac82cbc07dfa83955492d2ef6e237b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/scout/zipball/b8b8a35cc3ac82cbc07dfa83955492d2ef6e237b", + "reference": "b8b8a35cc3ac82cbc07dfa83955492d2ef6e237b", + "shasum": "" + }, + "require": { + "illuminate/bus": "^6.0|^7.0|^8.0", + "illuminate/contracts": "^6.0|^7.0|^8.0", + "illuminate/database": "^6.0|^7.0|^8.0", + "illuminate/http": "^6.0|^7.0|^8.0", + "illuminate/pagination": "^6.0|^7.0|^8.0", + "illuminate/queue": "^6.0|^7.0|^8.0", + "illuminate/support": "^6.0|^7.0|^8.0", + "php": "^7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "suggest": { + "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^2.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Scout\\ScoutServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.", + "keywords": [ + "algolia", + "laravel", + "search" + ], + "support": { + "issues": "https://github.com/laravel/scout/issues", + "source": "https://github.com/laravel/scout" + }, + "time": "2020-12-22T17:29:20+00:00" + }, { "name": "league/commonmark", "version": "1.5.7", @@ -5482,6 +5551,154 @@ ], "time": "2020-12-16T17:02:19+00:00" }, + { + "name": "teamtnt/laravel-scout-tntsearch-driver", + "version": "v11.1.0", + "source": { + "type": "git", + "url": "https://github.com/teamtnt/laravel-scout-tntsearch-driver.git", + "reference": "a9c27a68dc2bd74fb354165633520de95708215d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/teamtnt/laravel-scout-tntsearch-driver/zipball/a9c27a68dc2bd74fb354165633520de95708215d", + "reference": "a9c27a68dc2bd74fb354165633520de95708215d", + "shasum": "" + }, + "require": { + "illuminate/bus": "~5.4|^6.0|^7.0|^8.0", + "illuminate/contracts": "~5.4|^6.0|^7.0|^8.0", + "illuminate/database": "~5.4|^6.0|^7.0|^8.0", + "illuminate/pagination": "~5.4|^6.0|^7.0|^8.0", + "illuminate/queue": "~5.4|^6.0|^7.0|^8.0", + "illuminate/support": "~5.4|^6.0|^7.0|^8.0", + "laravel/scout": "7.*|^8.0|^8.3", + "php": ">=7.1", + "teamtnt/tntsearch": "2.*" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "suggest": { + "teamtnt/tntsearch": "Required to use the TNTSearch engine." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "laravel": { + "providers": [ + "TeamTNT\\Scout\\TNTSearchScoutServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "TeamTNT\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "TNT Studio", + "email": "info@tntstudio.hr" + } + ], + "description": "Driver for Laravel Scout search package based on https://github.com/teamtnt/tntsearch", + "keywords": [ + "laravel", + "scout", + "search", + "tntsearch" + ], + "support": { + "issues": "https://github.com/teamtnt/laravel-scout-tntsearch-driver/issues", + "source": "https://github.com/teamtnt/laravel-scout-tntsearch-driver/tree/v11.1.0" + }, + "time": "2020-11-11T11:17:48+00:00" + }, + { + "name": "teamtnt/tntsearch", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/teamtnt/tntsearch.git", + "reference": "d9b2d764491c87f03ec214ed8dbc27336cf0c0e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/teamtnt/tntsearch/zipball/d9b2d764491c87f03ec214ed8dbc27336cf0c0e4", + "reference": "d9b2d764491c87f03ec214ed8dbc27336cf0c0e4", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-pdo_sqlite": "*", + "ext-sqlite3": "*", + "php": "~7.1|^8" + }, + "require-dev": { + "phpunit/phpunit": "7.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "TeamTNT\\TNTSearch\\": "src" + }, + "files": [ + "helper/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nenad Tičarić", + "email": "nticaric@gmail.com", + "homepage": "http://www.tntstudio.us", + "role": "Developer" + } + ], + "description": "A fully featured full text search engine written in PHP", + "homepage": "https://github.com/teamtnt/tntsearch", + "keywords": [ + "Fuzzy search", + "bm25", + "fulltext", + "geosearch", + "search", + "stemming", + "teamtnt", + "text classification", + "tntsearch" + ], + "support": { + "issues": "https://github.com/teamtnt/tntsearch/issues", + "source": "https://github.com/teamtnt/tntsearch/tree/v2.6.0" + }, + "funding": [ + { + "url": "https://ko-fi.com/nticaric", + "type": "ko_fi" + }, + { + "url": "https://opencollective.com/tntsearch", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/nticaric", + "type": "patreon" + } + ], + "time": "2020-12-21T09:11:54+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "2.2.3", @@ -6360,6 +6577,201 @@ ], "time": "2020-11-10T18:47:58+00:00" }, + { + "name": "facade/flare-client-php", + "version": "1.3.7", + "source": { + "type": "git", + "url": "https://github.com/facade/flare-client-php.git", + "reference": "fd688d3c06658f2b3b5f7bb19f051ee4ddf02492" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facade/flare-client-php/zipball/fd688d3c06658f2b3b5f7bb19f051ee4ddf02492", + "reference": "fd688d3c06658f2b3b5f7bb19f051ee4ddf02492", + "shasum": "" + }, + "require": { + "facade/ignition-contracts": "~1.0", + "illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0", + "php": "^7.1|^8.0", + "symfony/http-foundation": "^3.3|^4.1|^5.0", + "symfony/mime": "^3.4|^4.0|^5.1", + "symfony/var-dumper": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14", + "phpunit/phpunit": "^7.5.16", + "spatie/phpunit-snapshot-assertions": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Facade\\FlareClient\\": "src" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Send PHP errors to Flare", + "homepage": "https://github.com/facade/flare-client-php", + "keywords": [ + "exception", + "facade", + "flare", + "reporting" + ], + "support": { + "issues": "https://github.com/facade/flare-client-php/issues", + "source": "https://github.com/facade/flare-client-php/tree/1.3.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2020-10-21T16:02:39+00:00" + }, + { + "name": "facade/ignition", + "version": "2.5.3", + "source": { + "type": "git", + "url": "https://github.com/facade/ignition.git", + "reference": "d8dc4f90ed469f9f9313b976fb078c20585d5c99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facade/ignition/zipball/d8dc4f90ed469f9f9313b976fb078c20585d5c99", + "reference": "d8dc4f90ed469f9f9313b976fb078c20585d5c99", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "facade/flare-client-php": "^1.3.7", + "facade/ignition-contracts": "^1.0.2", + "filp/whoops": "^2.4", + "illuminate/support": "^7.0|^8.0", + "monolog/monolog": "^2.0", + "php": "^7.2.5|^8.0", + "symfony/console": "^5.0", + "symfony/var-dumper": "^5.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14", + "mockery/mockery": "^1.3", + "orchestra/testbench": "^5.0|^6.0", + "psalm/plugin-laravel": "^1.2" + }, + "suggest": { + "laravel/telescope": "^3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + }, + "laravel": { + "providers": [ + "Facade\\Ignition\\IgnitionServiceProvider" + ], + "aliases": { + "Flare": "Facade\\Ignition\\Facades\\Flare" + } + } + }, + "autoload": { + "psr-4": { + "Facade\\Ignition\\": "src" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A beautiful error page for Laravel applications.", + "homepage": "https://github.com/facade/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/facade/ignition/issues", + "source": "https://github.com/facade/ignition" + }, + "time": "2020-12-09T20:25:45+00:00" + }, + { + "name": "facade/ignition-contracts", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/facade/ignition-contracts.git", + "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v2.15.8", + "phpunit/phpunit": "^9.3.11", + "vimeo/psalm": "^3.17.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Facade\\IgnitionContracts\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://flareapp.io", + "role": "Developer" + } + ], + "description": "Solution contracts for Ignition", + "homepage": "https://github.com/facade/ignition-contracts", + "keywords": [ + "contracts", + "flare", + "ignition" + ], + "support": { + "issues": "https://github.com/facade/ignition-contracts/issues", + "source": "https://github.com/facade/ignition-contracts/tree/1.0.2" + }, + "time": "2020-10-16T08:27:54+00:00" + }, { "name": "fakerphp/faker", "version": "v1.13.0", @@ -6412,6 +6824,71 @@ }, "time": "2020-12-18T16:50:48+00:00" }, + { + "name": "filp/whoops", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/307fb34a5ab697461ec4c9db865b20ff2fd40771", + "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0", + "psr/log": "^1.0.1" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.9.1" + }, + "time": "2020-11-01T12:00:00+00:00" + }, { "name": "hamcrest/hamcrest-php", "version": "v2.0.1", diff --git a/config/app.php b/config/app.php index a57f3e54..4e564b1a 100644 --- a/config/app.php +++ b/config/app.php @@ -127,6 +127,9 @@ return [ Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class, Intervention\Image\ImageServiceProvider::class, + Laravel\Scout\ScoutServiceProvider::class, + TeamTNT\Scout\TNTSearchScoutServiceProvider::class, + /* * Application Service Providers... */ diff --git a/config/ignition.php b/config/ignition.php new file mode 100644 index 00000000..146b2e64 --- /dev/null +++ b/config/ignition.php @@ -0,0 +1,127 @@ + env('IGNITION_EDITOR', 'phpstorm'), + + /* + |-------------------------------------------------------------------------- + | Theme + |-------------------------------------------------------------------------- + | + | Here you may specify which theme Ignition should use. + | + | Supported: "light", "dark", "auto" + | + */ + + 'theme' => env('IGNITION_THEME', 'dark'), + + /* + |-------------------------------------------------------------------------- + | Sharing + |-------------------------------------------------------------------------- + | + | You can share local errors with colleagues or others around the world. + | Sharing is completely free and doesn't require an account on Flare. + | + | If necessary, you can completely disable sharing below. + | + */ + + 'enable_share_button' => env('IGNITION_SHARING_ENABLED', false), + + /* + |-------------------------------------------------------------------------- + | Register Ignition commands + |-------------------------------------------------------------------------- + | + | Ignition comes with an additional make command that lets you create + | new solution classes more easily. To keep your default Laravel + | installation clean, this command is not registered by default. + | + | You can enable the command registration below. + | + */ + 'register_commands' => env('REGISTER_IGNITION_COMMANDS', false), + + /* + |-------------------------------------------------------------------------- + | Ignored Solution Providers + |-------------------------------------------------------------------------- + | + | You may specify a list of solution providers (as fully qualified class + | names) that shouldn't be loaded. Ignition will ignore these classes + | and possible solutions provided by them will never be displayed. + | + */ + + 'ignored_solution_providers' => [ + MissingPackageSolutionProvider::class, + ], + + /* + |-------------------------------------------------------------------------- + | Runnable Solutions + |-------------------------------------------------------------------------- + | + | Some solutions that Ignition displays are runnable and can perform + | various tasks. Runnable solutions are enabled when your app has + | debug mode enabled. You may also fully disable this feature. + | + */ + + 'enable_runnable_solutions' => env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS', null), + + /* + |-------------------------------------------------------------------------- + | Remote Path Mapping + |-------------------------------------------------------------------------- + | + | If you are using a remote dev server, like Laravel Homestead, Docker, or + | even a remote VPS, it will be necessary to specify your path mapping. + | + | Leaving one, or both of these, empty or null will not trigger the remote + | URL changes and Ignition will treat your editor links as local files. + | + | "remote_sites_path" is an absolute base path for your sites or projects + | in Homestead, Vagrant, Docker, or another remote development server. + | + | Example value: "/home/vagrant/Code" + | + | "local_sites_path" is an absolute base path for your sites or projects + | on your local computer where your IDE or code editor is running on. + | + | Example values: "/Users//Code", "C:\Users\\Documents\Code" + | + */ + + 'remote_sites_path' => env('IGNITION_REMOTE_SITES_PATH', ''), + 'local_sites_path' => env('IGNITION_LOCAL_SITES_PATH', ''), + + /* + |-------------------------------------------------------------------------- + | Housekeeping Endpoint Prefix + |-------------------------------------------------------------------------- + | + | Ignition registers a couple of routes when it is enabled. Below you may + | specify a route prefix that will be used to host all internal links. + | + */ + 'housekeeping_endpoint_prefix' => '_ignition', + +]; diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 00000000..ea2561e5 --- /dev/null +++ b/config/scout.php @@ -0,0 +1,117 @@ + env('SCOUT_DRIVER', 'tntsearch'), + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + 'prefix' => env('SCOUT_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => env('SCOUT_QUEUE', false), + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => false, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => env('SCOUT_IDENTIFY', false), + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID', ''), + 'secret' => env('ALGOLIA_SECRET', ''), + ], + + 'tntsearch' => [ + 'storage' => storage_path('search-indexes'), + 'fuzziness' => env('TNTSEARCH_FUZZINESS', true), + 'fuzzy' => [ + 'prefix_length' => 2, + 'max_expansions' => 50, + 'distance' => 2, + ], + 'asYouType' => false, + 'searchBoolean' => env('TNTSEARCH_BOOLEAN', false), + ], +]; diff --git a/public/.gitignore b/public/.gitignore index 7cf3e5d7..2ef55e95 100644 --- a/public/.gitignore +++ b/public/.gitignore @@ -1,6 +1,7 @@ css fonts img +images js manifest.json manifest-remote.json diff --git a/public/mix-manifest.json b/public/mix-manifest.json deleted file mode 100644 index 42008646..00000000 --- a/public/mix-manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "/js/app.js": "/js/app.js", - "/css/app.css": "/css/app.css", - "/css/remote.css": "/css/remote.css", - "/js/remote/app.js": "/js/remote/app.js", - "/js/app.07a760363b90b9ec000a.hot-update.js": "/js/app.07a760363b90b9ec000a.hot-update.js", - "/js/remote/app.07a760363b90b9ec000a.hot-update.js": "/js/remote/app.07a760363b90b9ec000a.hot-update.js" -} diff --git a/resources/assets b/resources/assets index dbdcc595..c0dac561 160000 --- a/resources/assets +++ b/resources/assets @@ -1 +1 @@ -Subproject commit dbdcc595ba5f625c291ce6042a7e230d0273ded3 +Subproject commit c0dac561063ccf35a5faeddfc9d9629f594c1f2d diff --git a/routes/api.php b/routes/api.php index 8015abe9..e8ddcc72 100644 --- a/routes/api.php +++ b/routes/api.php @@ -75,6 +75,11 @@ Route::group(['namespace' => 'API'], static function (): void { Route::put('artist/{artist}/image', 'ArtistImageController@update'); Route::get('album/{album}/thumbnail', 'AlbumThumbnailController@get'); + + Route::group(['namespace' => 'Search', 'prefix' => 'search'], static function (): void { + Route::get('/', 'ExcerptSearchController@index'); + Route::get('songs', 'SongSearchController@index'); + }); }); Route::group([ diff --git a/storage/search-indexes/.gitignore b/storage/search-indexes/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/storage/search-indexes/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore