Merge pull request #1269 from koel/search

This commit is contained in:
Phan An 2020-12-25 13:30:54 +01:00 committed by GitHub
commit b901a8cbb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1096 additions and 96 deletions

View file

@ -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

View file

@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
use Illuminate\Console\Command;
class ImportSearchableEntitiesCommand extends Command
{
private const SEARCHABLE_ENTITIES = [
Song::class,
Album::class,
Artist::class,
];
protected $signature = 'koel:search:import';
protected $description = 'Import all searchable entities with Scout';
public function handle(): int
{
foreach (self::SEARCHABLE_ENTITIES as $entity) {
if (!class_exists($entity)) {
continue;
}
$this->call('scout:import', ['model' => $entity]);
}
return 0;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\API\Search;
use App\Http\Controllers\API\Controller;
use App\Services\SearchService;
use Illuminate\Http\Request;
use InvalidArgumentException;
class ExcerptSearchController extends Controller
{
private $searchService;
public function __construct(SearchService $searchService)
{
$this->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),
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\API\Search;
use App\Http\Controllers\API\Controller;
use App\Services\SearchService;
use Illuminate\Http\Request;
use InvalidArgumentException;
class SongSearchController extends Controller
{
private $searchService;
public function __construct(SearchService $searchService)
{
$this->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')),
];
}
}

View file

@ -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<mixed> */
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;
}
}

View file

@ -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<mixed> */
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}

View file

@ -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')
*/

View file

@ -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<mixed> */
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;

View file

@ -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;
}
}

View file

@ -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<int> */
public function getNonEmptyAlbumIds(): array

View file

@ -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<int> */
public function getNonEmptyArtistIds(): array

View file

@ -12,11 +12,6 @@ class InteractionRepository extends AbstractRepository
{
use ByCurrentUser;
public function getModelClass(): string
{
return Interaction::class;
}
/** @return Collection|array<Interaction> */
public function getUserFavorites(User $user): Collection
{

View file

@ -10,11 +10,6 @@ class PlaylistRepository extends AbstractRepository
{
use ByCurrentUser;
public function getModelClass(): string
{
return Playlist::class;
}
/** @return Collection|array<Playlist> */
public function getAllByCurrentUser(): Collection
{

View file

@ -7,8 +7,6 @@ use Illuminate\Database\Eloquent\Model;
interface RepositoryInterface
{
public function getModelClass(): string;
public function getOneById($id): ?Model;
/** @return Collection|array<Model> */

View file

@ -2,15 +2,8 @@
namespace App\Repositories;
use App\Models\Setting;
class SettingRepository extends AbstractRepository
{
public function getModelClass(): string
{
return Setting::class;
}
/** @return array<mixed> */
public function getAllAsKeyValueArray(): array
{

View file

@ -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));

View file

@ -0,0 +1,13 @@
<?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);
}
}

View file

@ -2,12 +2,6 @@
namespace App\Repositories;
use App\Models\User;
class UserRepository extends AbstractRepository
{
public function getModelClass(): string
{
return User::class;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
use App\Repositories\AlbumRepository;
use App\Repositories\ArtistRepository;
use App\Repositories\SongRepository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Laravel\Scout\Builder;
class SearchService
{
public const DEFAULT_EXCERPT_RESULT_COUNT = 6;
private $songRepository;
private $albumRepository;
private $artistRepository;
public function __construct(
SongRepository $songRepository,
AlbumRepository $albumRepository,
ArtistRepository $artistRepository
) {
$this->songRepository = $songRepository;
$this->albumRepository = $albumRepository;
$this->artistRepository = $artistRepository;
}
/** @return array<mixed> */
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<Model> */
private static function getTopResults(Builder $query, int $count): Collection
{
return $query->take($count)->get();
}
/** @return Collection|array<string> */
public function searchSongs(string $keywords): Collection
{
return $this->songRepository
->search($keywords)
->get()
->map(static function (Song $song): string {
return $song->id;
});
}
}

View file

@ -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",

501
composer.lock generated
View file

@ -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",

View file

@ -127,6 +127,9 @@ return [
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
Intervention\Image\ImageServiceProvider::class,
Laravel\Scout\ScoutServiceProvider::class,
TeamTNT\Scout\TNTSearchScoutServiceProvider::class,
/*
* Application Service Providers...
*/

127
config/ignition.php Normal file
View file

@ -0,0 +1,127 @@
<?php
use Facade\Ignition\SolutionProviders\MissingPackageSolutionProvider;
return [
/*
|--------------------------------------------------------------------------
| Editor
|--------------------------------------------------------------------------
|
| Choose your preferred editor to use when clicking any edit button.
|
| Supported: "phpstorm", "vscode", "vscode-insiders",
| "sublime", "atom"
|
*/
'editor' => 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/<name>/Code", "C:\Users\<name>\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',
];

117
config/scout.php Normal file
View file

@ -0,0 +1,117 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "null"
|
*/
'driver' => 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),
],
];

1
public/.gitignore vendored
View file

@ -1,6 +1,7 @@
css
fonts
img
images
js
manifest.json
manifest-remote.json

View file

@ -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"
}

@ -1 +1 @@
Subproject commit dbdcc595ba5f625c291ce6042a7e230d0273ded3
Subproject commit c0dac561063ccf35a5faeddfc9d9629f594c1f2d

View file

@ -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([

2
storage/search-indexes/.gitignore vendored Normal file
View file

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