mirror of
https://github.com/koel/koel
synced 2024-11-24 21:23:06 +00:00
Merge pull request #1466 from koel/next
This commit is contained in:
commit
4c923fae93
683 changed files with 34779 additions and 12561 deletions
|
@ -1,6 +1,8 @@
|
|||
[*.{js,css,sass,scss,json,coffee,vue,html}]
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.php]
|
||||
[{*.php, *.xml, *.xml.dist}]
|
||||
indent_size = 4
|
||||
|
|
46
.env.example
46
.env.example
|
@ -6,7 +6,7 @@ APP_NAME=Koel
|
|||
# pgsql (PostgreSQL)
|
||||
# sqlsrv (Microsoft SQL Server)
|
||||
# sqlite-persistent (Local sqlite file)
|
||||
# IMPORTANT: This value must present for artisan koel:init command to work.
|
||||
# IMPORTANT: This value must present for `artisan koel:init` command to work.
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
|
@ -46,16 +46,42 @@ MEMORY_LIMIT=
|
|||
# Can be either 'php' (default), 'x-sendfile', or 'x-accel-redirect'
|
||||
# See https://docs.koel.dev/#streaming-music for more information.
|
||||
# Note: This setting doesn't have effect if the media needs transcoding (e.g. FLAC).
|
||||
# ##################################################
|
||||
# IMPORTANT: It's HIGHLY recommended to use 'x-sendfile' or 'x-accel-redirect' if
|
||||
# you plan to use the Koel mobile apps.
|
||||
# ##################################################
|
||||
STREAMING_METHOD=php
|
||||
|
||||
|
||||
# If you want Koel to integrate with Last.fm, set the API details here.
|
||||
# See https://docs.koel.dev/3rd-party.html#last-fm for more information
|
||||
# Full text search driver.
|
||||
# Koel supports all drivers supported by Laravel (see https://laravel.com/docs/9.x/scout).
|
||||
# Available drivers: 'tntsearch' (default), 'database', 'algolia' or 'meilisearch'.
|
||||
# For Algolia or MeiliSearch, you need to provide the corresponding credentials.
|
||||
SCOUT_DRIVER=tntsearch
|
||||
ALGOLIA_APP_ID=
|
||||
ALGOLIA_SECRET=
|
||||
MEILISEARCH_HOST=
|
||||
MEILISEARCH_KEY=
|
||||
|
||||
|
||||
# Last.fm API can be used to fetch artist and album information, as well as to
|
||||
# allow users to connect to their Last.fm account and scrobble.
|
||||
# To integrate Koel with Last.fm, create an API account at
|
||||
# https://www.last.fm/api/account/create and set the credentials here.
|
||||
# Consult Koel's doc for more information.
|
||||
LASTFM_API_KEY=
|
||||
LASTFM_API_SECRET=
|
||||
|
||||
|
||||
# If you want to use Amazon S3 with Koel, fill the info here and follow the
|
||||
# Spotify API can be used to fetch artist and album images.
|
||||
# To integrate Koel with Spotify, create a Spotify application at
|
||||
# https://developer.spotify.com/dashboard/applications and set the credentials here.
|
||||
# Consult Koel's doc for more information.
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
|
||||
|
||||
# To use Amazon S3 with Koel, fill the info here and follow the
|
||||
# installation guide at https://docs.koel.dev/aws-s3.html
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
@ -63,7 +89,7 @@ AWS_REGION=
|
|||
AWS_ENDPOINT=
|
||||
|
||||
|
||||
# If you want Koel to integrate with YouTube, set the API key here.
|
||||
# To integrate Koel with YouTube, set the API key here.
|
||||
# See https://docs.koel.dev/3rd-party.html#youtube for more information.
|
||||
YOUTUBE_API_KEY=
|
||||
|
||||
|
@ -74,8 +100,7 @@ YOUTUBE_API_KEY=
|
|||
CDN_URL=
|
||||
|
||||
|
||||
# If you want to transcode FLAC to MP3 and stream it on the fly, make sure the
|
||||
# following settings are sane.
|
||||
# To transcode FLAC to MP3 and stream it on the fly, make sure the following settings are sane.
|
||||
|
||||
# The full path of ffmpeg binary.
|
||||
FFMPEG_PATH=/usr/local/bin/ffmpeg
|
||||
|
@ -90,12 +115,6 @@ OUTPUT_BIT_RATE=128
|
|||
# environment, such a download will (silently) fail.
|
||||
ALLOW_DOWNLOAD=true
|
||||
|
||||
# If this is set to true, the query to get artist, album, and song information will be cached.
|
||||
# This can give a boost to Koel's boot time, especially if your library is huge.
|
||||
# However, the cache deserialization process can be memory sensitive, so if you encounter
|
||||
# errors, try setting this to false.
|
||||
CACHE_MEDIA=true
|
||||
|
||||
|
||||
# Koel attempts to detect if your website use HTTPS and generates secure URLs accordingly.
|
||||
# If this attempts for any reason, you can force it by setting this value to true.
|
||||
|
@ -128,4 +147,3 @@ MAIL_PORT=2525
|
|||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
libs
|
||||
tests
|
41
.eslintrc
41
.eslintrc
|
@ -1,19 +1,37 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parser": "vue-eslint-parser",
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-recommended"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"cypress/fixtures",
|
||||
"cypress/screenshots",
|
||||
"resources/assets/js/tests/__coverage__"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"KOEL_ENV": "readonly",
|
||||
"FileReader": "readonly",
|
||||
"defineProps": "readonly",
|
||||
"defineEmits": "readonly",
|
||||
"defineExpose": "readonly",
|
||||
"withDefaults": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"camelcase": 0,
|
||||
"no-multi-str": 0,
|
||||
"no-empty": 0,
|
||||
"quotes": 0,
|
||||
"no-use-before-define": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/camelcase": 0,
|
||||
"@typescript-eslint/member-delimiter-style": 0,
|
||||
|
@ -21,6 +39,13 @@
|
|||
"@typescript-eslint/no-inferrable-types": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/ban-ts-ignore": 0
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"standard/no-callback-literal": 0,
|
||||
"vue/valid-v-on": 0,
|
||||
"vue/no-side-effects-in-computed-properties": 0,
|
||||
"vue/max-attributes-per-line": 0,
|
||||
"vue/no-v-html": 0
|
||||
}
|
||||
}
|
||||
|
|
14
.github/workflows/e2e.yml
vendored
14
.github/workflows/e2e.yml
vendored
|
@ -1,8 +1,10 @@
|
|||
name: e2e
|
||||
name: End to End Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# @fixme Tmp.disable until ready
|
||||
# - next
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
|
@ -19,11 +21,11 @@ jobs:
|
|||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 7.4
|
||||
php-version: 8.1
|
||||
tools: composer:v2
|
||||
extensions: pdo_sqlite
|
||||
- name: Install PHP dependencies
|
||||
uses: ramsey/composer-install@v1
|
||||
uses: ramsey/composer-install@v2
|
||||
with:
|
||||
composer-options: --prefer-dist
|
||||
- name: Generate app key
|
||||
|
@ -32,12 +34,6 @@ jobs:
|
|||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install JavaScript dependencies
|
||||
run: |
|
||||
cd ./resources/assets && yarn install --no-progress
|
||||
cd ../.. && yarn install --no-progress
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
- name: Run E2E tests
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
|
|
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
|
@ -1,7 +1,7 @@
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
name: Upload Release Assets
|
||||
|
||||
|
@ -14,22 +14,20 @@ jobs:
|
|||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 7.4
|
||||
php-version: 8.1
|
||||
tools: composer:v2
|
||||
extensions: pdo_sqlite
|
||||
extensions: pdo_sqlite, zip, gd
|
||||
- name: Install PHP dependencies
|
||||
uses: ramsey/composer-install@v1
|
||||
uses: ramsey/composer-install@v2
|
||||
with:
|
||||
composer-options: --prefer-dist
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: 16
|
||||
- name: Build project
|
||||
run: |
|
||||
sudo apt install pngquant zip unzip
|
||||
|
@ -39,10 +37,10 @@ jobs:
|
|||
run: |
|
||||
sed -i 's/DB_CONNECTION=sqlite/DB_CONNECTION=sqlite-persistent/' .env
|
||||
sed -i 's/DB_DATABASE=koel/DB_DATABASE=koel.db/' .env
|
||||
rm -rf .git ./node_modules ./resources/assets/.git ./resources/assets/node_modules ./storage/search-indexes/*.index ./koel.db ./.env
|
||||
rm -rf .git ./node_modules ./storage/search-indexes/*.index ./koel.db ./.env
|
||||
cd ../
|
||||
zip -r /tmp/koel-${{ steps.get_version.outputs.VERSION }}.zip koel/
|
||||
tar -zcvf /tmp/koel-${{ steps.get_version.outputs.VERSION }}.tar.gz koel/
|
||||
tar -zcf /tmp/koel-${{ steps.get_version.outputs.VERSION }}.tar.gz koel/
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
|
@ -59,7 +57,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing its ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: /tmp/koel-${{ steps.get_version.outputs.VERSION }}.zip
|
||||
asset_name: koel-${{ steps.get_version.outputs.VERSION }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
@ -69,7 +67,7 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing its ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: /tmp/koel-${{ steps.get_version.outputs.VERSION }}.tar.gz
|
||||
asset_name: koel-${{ steps.get_version.outputs.VERSION }}.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
|
|
@ -1,16 +1,32 @@
|
|||
name: unit
|
||||
name: Backend Unit Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
paths:
|
||||
- '!resources/assets/**'
|
||||
- .github/workflows/unit-backend.yml
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
- next
|
||||
paths:
|
||||
- '!resources/assets/**'
|
||||
- .github/workflows/unit-backend.yml
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
paths:
|
||||
- '!resources/assets/**'
|
||||
- .github/workflows/unit-backend.yml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: [ 7.4, 8.0 ]
|
||||
php-version: [ 8.0, 8.1 ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
@ -33,6 +49,12 @@ jobs:
|
|||
run: composer analyze -- --no-progress
|
||||
- name: Run tests
|
||||
run: composer coverage
|
||||
- name: Upload logs if broken
|
||||
uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: logs
|
||||
path: storage/logs
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
46
.github/workflows/unit-frontend.yml
vendored
Normal file
46
.github/workflows/unit-frontend.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: Frontend Unit Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
paths:
|
||||
- resources/assets/**
|
||||
- .github/workflows/unit-frontend.yml
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
paths:
|
||||
- resources/assets/**
|
||||
- .github/workflows/unit-frontend.yml
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
paths:
|
||||
- resources/assets/**
|
||||
- .github/workflows/unit-frontend.yml
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [ 14, 16, 17 ] # 15 conflicts with @typescript-eslint/eslint-plugin@5
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
- name: Run unit tests
|
||||
run: yarn test:unit
|
||||
- name: Collect coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -74,8 +74,9 @@ Temporary Items
|
|||
*~
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/screenshots/
|
||||
cypress/videos/
|
||||
cypress/downloads/
|
||||
|
||||
/log
|
||||
coverage.xml
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "resources/assets"]
|
||||
path = resources/assets
|
||||
url = https://github.com/koel/core.git
|
|
@ -832,7 +832,7 @@ paths:
|
|||
security:
|
||||
- Bearer Token: []
|
||||
/api/settings:
|
||||
post:
|
||||
put:
|
||||
summary: Save the application settings
|
||||
tags:
|
||||
- settings
|
||||
|
|
|
@ -11,20 +11,16 @@ class ChangePasswordCommand extends Command
|
|||
{
|
||||
use AskForPassword;
|
||||
|
||||
protected $signature = "koel:admin:change-password
|
||||
protected $signature = "koel:admin:change-password
|
||||
{email? : The user's email. If empty, will get the default admin user.}";
|
||||
protected $description = "Change a user's password";
|
||||
|
||||
private Hash $hash;
|
||||
|
||||
public function __construct(Hash $hash)
|
||||
public function __construct(private Hash $hash)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->hash = $hash;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
public function handle(): int
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
|
||||
|
@ -34,7 +30,7 @@ class ChangePasswordCommand extends Command
|
|||
if (!$user) {
|
||||
$this->error('The user account cannot be found.');
|
||||
|
||||
return;
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->comment("Changing the user's password (ID: $user->id, email: $user->email)");
|
||||
|
@ -43,5 +39,7 @@ class ChangePasswordCommand extends Command
|
|||
$user->save();
|
||||
|
||||
$this->comment('Alrighty, the new password has been saved. Enjoy! 👌');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,6 @@ class ImportSearchableEntitiesCommand extends Command
|
|||
$this->call('scout:import', ['model' => $entity]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,15 @@ use App\Console\Commands\Traits\AskForPassword;
|
|||
use App\Exceptions\InstallationFailedException;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SettingRepository;
|
||||
use App\Services\MediaCacheService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel as Artisan;
|
||||
use Illuminate\Contracts\Hashing\Hasher as Hash;
|
||||
use Illuminate\Database\DatabaseManager as DB;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Str;
|
||||
use Jackiedo\DotenvEditor\DotenvEditor;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
class InitCommand extends Command
|
||||
|
@ -22,48 +24,40 @@ class InitCommand extends Command
|
|||
private const DEFAULT_ADMIN_NAME = 'Koel';
|
||||
private const DEFAULT_ADMIN_EMAIL = 'admin@koel.dev';
|
||||
private const DEFAULT_ADMIN_PASSWORD = 'KoelIsCool';
|
||||
private const NON_INTERACTION_MAX_ATTEMPT_COUNT = 10;
|
||||
private const NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT = 10;
|
||||
|
||||
protected $signature = 'koel:init {--no-assets}';
|
||||
protected $description = 'Install or upgrade Koel';
|
||||
|
||||
private MediaCacheService $mediaCacheService;
|
||||
private Artisan $artisan;
|
||||
private DotenvEditor $dotenvEditor;
|
||||
private Hash $hash;
|
||||
private DB $db;
|
||||
private SettingRepository $settingRepository;
|
||||
private bool $adminSeeded = false;
|
||||
|
||||
public function __construct(
|
||||
MediaCacheService $mediaCacheService,
|
||||
SettingRepository $settingRepository,
|
||||
Artisan $artisan,
|
||||
Hash $hash,
|
||||
DotenvEditor $dotenvEditor,
|
||||
DB $db
|
||||
private MediaCacheService $mediaCacheService,
|
||||
private Artisan $artisan,
|
||||
private Hash $hash,
|
||||
private DotenvEditor $dotenvEditor,
|
||||
private DB $db,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->mediaCacheService = $mediaCacheService;
|
||||
$this->artisan = $artisan;
|
||||
$this->dotenvEditor = $dotenvEditor;
|
||||
$this->hash = $hash;
|
||||
$this->db = $db;
|
||||
$this->settingRepository = $settingRepository;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
public function handle(): int
|
||||
{
|
||||
$this->comment('Attempting to install or upgrade Koel.');
|
||||
$this->comment('Remember, you can always install/upgrade manually following the guide here:');
|
||||
$this->info('📙 ' . config('koel.misc.docs_url') . PHP_EOL);
|
||||
$this->alert('KOEL INSTALLATION WIZARD');
|
||||
$this->info(
|
||||
'As a reminder, you can always install/upgrade manually following the guide at '
|
||||
. config('koel.misc.docs_url')
|
||||
. PHP_EOL
|
||||
);
|
||||
|
||||
if ($this->inNoInteractionMode()) {
|
||||
$this->info('Running in no-interaction mode');
|
||||
$this->components->info('Running in no-interaction mode');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->clearCaches();
|
||||
$this->loadEnvFile();
|
||||
$this->maybeGenerateAppKey();
|
||||
$this->maybeSetUpDatabase();
|
||||
$this->migrateDatabase();
|
||||
|
@ -71,32 +65,79 @@ class InitCommand extends Command
|
|||
$this->maybeSetMediaPath();
|
||||
$this->maybeCompileFrontEndAssets();
|
||||
} catch (Throwable $e) {
|
||||
$this->error("Oops! Koel installation or upgrade didn't finish successfully.");
|
||||
$this->error('Please try again, or visit ' . config('koel.misc.docs_url') . ' for manual installation.');
|
||||
$this->error('😥 Sorry for this. You deserve better.');
|
||||
$this->logger->error($e);
|
||||
|
||||
return;
|
||||
$this->components->error("Oops! Koel installation or upgrade didn't finish successfully.");
|
||||
$this->components->error('Please check the error log at storage/logs/laravel.log and try again.');
|
||||
$this->components->error('You can also visit ' . config('koel.misc.docs_url') . ' for other options.');
|
||||
$this->components->error('😥 Sorry for this. You deserve better.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->comment(PHP_EOL . '🎆 Success! Koel can now be run from localhost with `php artisan serve`.');
|
||||
$this->newLine();
|
||||
$this->output->success('All done!');
|
||||
$this->info('Koel can now be run from localhost with `php artisan serve`.');
|
||||
|
||||
if ($this->adminSeeded) {
|
||||
$this->comment(
|
||||
$this->info(
|
||||
sprintf('Log in with email %s and password %s', self::DEFAULT_ADMIN_EMAIL, self::DEFAULT_ADMIN_PASSWORD)
|
||||
);
|
||||
}
|
||||
|
||||
if (Setting::get('media_path')) {
|
||||
$this->comment('You can also scan for media with `php artisan koel:sync`.');
|
||||
$this->info('You can also scan for media now with `php artisan koel:sync`.');
|
||||
}
|
||||
|
||||
$this->comment('Again, visit 📙 ' . config('koel.misc.docs_url') . ' for the official documentation.');
|
||||
$this->comment(
|
||||
$this->info('Again, visit 📙 ' . config('koel.misc.docs_url') . ' for more tips and tweaks.');
|
||||
|
||||
$this->info(
|
||||
"Feeling generous and want to support Koel's development? Check out "
|
||||
. config('koel.misc.sponsor_github_url')
|
||||
. ' 🤗'
|
||||
);
|
||||
$this->comment('Thanks for using Koel. You rock! 🤘');
|
||||
|
||||
$this->info('Thanks for using Koel. You rock! 🤘');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function clearCaches(): void
|
||||
{
|
||||
$this->components->task('Clearing caches', function (): void {
|
||||
$this->artisan->call('config:clear');
|
||||
$this->artisan->call('cache:clear');
|
||||
});
|
||||
}
|
||||
|
||||
private function loadEnvFile(): void
|
||||
{
|
||||
if (!file_exists(base_path('.env'))) {
|
||||
$this->components->task('Copying .env file', static function (): void {
|
||||
copy(base_path('.env.example'), base_path('.env'));
|
||||
});
|
||||
} else {
|
||||
$this->components->info('.env file exists -- skipping');
|
||||
}
|
||||
|
||||
$this->dotenvEditor->load(base_path('.env'));
|
||||
}
|
||||
|
||||
private function maybeGenerateAppKey(): void
|
||||
{
|
||||
$key = $this->laravel['config']['app.key'];
|
||||
|
||||
$this->components->task($key ? 'Retrieving app key' : 'Generating app key', function () use (&$key): void {
|
||||
if (!$key) {
|
||||
// Generate the key manually to prevent some clashes with `php artisan key:generate`
|
||||
$key = $this->generateRandomKey();
|
||||
$this->dotenvEditor->setKey('APP_KEY', $key);
|
||||
$this->laravel['config']['app.key'] = $key;
|
||||
}
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->components->info('Using app key: ' . Str::limit($key, 16));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,10 +146,8 @@ class InitCommand extends Command
|
|||
private function setUpDatabase(): void
|
||||
{
|
||||
$config = [
|
||||
'DB_CONNECTION' => '',
|
||||
'DB_HOST' => '',
|
||||
'DB_PORT' => '',
|
||||
'DB_DATABASE' => '',
|
||||
'DB_USERNAME' => '',
|
||||
'DB_PASSWORD' => '',
|
||||
];
|
||||
|
@ -163,16 +202,83 @@ class InitCommand extends Command
|
|||
|
||||
private function setUpAdminAccount(): void
|
||||
{
|
||||
$this->info("Creating default admin account");
|
||||
$this->components->task('Creating default admin account', function (): void {
|
||||
User::create([
|
||||
'name' => self::DEFAULT_ADMIN_NAME,
|
||||
'email' => self::DEFAULT_ADMIN_EMAIL,
|
||||
'password' => $this->hash->make(self::DEFAULT_ADMIN_PASSWORD),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'name' => self::DEFAULT_ADMIN_NAME,
|
||||
'email' => self::DEFAULT_ADMIN_EMAIL,
|
||||
'password' => $this->hash->make(self::DEFAULT_ADMIN_PASSWORD),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$this->adminSeeded = true;
|
||||
});
|
||||
}
|
||||
|
||||
$this->adminSeeded = true;
|
||||
private function maybeSeedDatabase(): void
|
||||
{
|
||||
if (!User::count()) {
|
||||
$this->setUpAdminAccount();
|
||||
|
||||
$this->components->task('Seeding data', function (): void {
|
||||
$this->artisan->call('db:seed', ['--force' => true]);
|
||||
});
|
||||
} else {
|
||||
$this->newLine();
|
||||
$this->components->info('Data already seeded -- skipping');
|
||||
}
|
||||
}
|
||||
|
||||
private function maybeSetUpDatabase(): void
|
||||
{
|
||||
$attempt = 0;
|
||||
|
||||
while (true) {
|
||||
// In non-interactive mode, we must not endlessly attempt to connect.
|
||||
// Doing so will just end up with a huge amount of "failed to connect" logs.
|
||||
// We do retry a little, though, just in case there's some kind of temporary failure.
|
||||
if ($this->inNoInteractionMode() && $attempt >= self::NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT) {
|
||||
$this->components->error('Maximum database connection attempts reached. Giving up.');
|
||||
break;
|
||||
}
|
||||
|
||||
$attempt++;
|
||||
|
||||
try {
|
||||
// Make sure the config cache is cleared before another attempt.
|
||||
$this->artisan->call('config:clear');
|
||||
$this->db->reconnect()->getPdo();
|
||||
|
||||
break;
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e);
|
||||
|
||||
// We only try to update credentials if running in interactive mode.
|
||||
// Otherwise, we require admin intervention to fix them.
|
||||
// This avoids inadvertently wiping credentials if there's a connection failure.
|
||||
if ($this->inNoInteractionMode()) {
|
||||
$warning = sprintf(
|
||||
"Cannot connect to the database. Attempt: %d/%d",
|
||||
$attempt,
|
||||
self::NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT
|
||||
);
|
||||
|
||||
$this->components->warn($warning);
|
||||
} else {
|
||||
$this->components->warn("Cannot connect to the database. Let's set it up.");
|
||||
$this->setUpDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function migrateDatabase(): void
|
||||
{
|
||||
$this->components->task('Migrating database', function (): void {
|
||||
$this->artisan->call('migrate', ['--force' => true]);
|
||||
});
|
||||
|
||||
// Clear the media cache, just in case we did any media-related migration
|
||||
$this->mediaCacheService->clear();
|
||||
}
|
||||
|
||||
private function maybeSetMediaPath(): void
|
||||
|
@ -187,6 +293,7 @@ class InitCommand extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('The absolute path to your media directory. If this is skipped (left blank) now, you can set it later via the web interface.'); // @phpcs-ignore-line
|
||||
|
||||
while (true) {
|
||||
|
@ -196,103 +303,23 @@ class InitCommand extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
if ($this->isValidMediaPath($path)) {
|
||||
if (self::isValidMediaPath($path)) {
|
||||
Setting::set('media_path', $path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->error('The path does not exist or not readable. Try again.');
|
||||
$this->components->error('The path does not exist or not readable. Try again?');
|
||||
}
|
||||
}
|
||||
|
||||
private function maybeGenerateAppKey(): void
|
||||
{
|
||||
if (!config('app.key')) {
|
||||
$this->info('Generating app key');
|
||||
$this->artisan->call('key:generate');
|
||||
} else {
|
||||
$this->comment('App key exists -- skipping');
|
||||
}
|
||||
}
|
||||
|
||||
private function maybeSeedDatabase(): void
|
||||
{
|
||||
if (!User::count()) {
|
||||
$this->setUpAdminAccount();
|
||||
$this->info('Seeding initial data');
|
||||
$this->artisan->call('db:seed', ['--force' => true]);
|
||||
} else {
|
||||
$this->comment('Data seeded -- skipping');
|
||||
}
|
||||
}
|
||||
|
||||
private function maybeSetUpDatabase(): void
|
||||
{
|
||||
$attemptCount = 0;
|
||||
|
||||
while (true) {
|
||||
// In non-interactive mode, we must not endlessly attempt to connect.
|
||||
// Doing so will just end up with a huge amount of "failed to connect" logs.
|
||||
// We do retry a little, though, just in case there's some kind of temporary failure.
|
||||
if ($this->inNoInteractionMode() && $attemptCount >= self::NON_INTERACTION_MAX_ATTEMPT_COUNT) {
|
||||
$this->warn("Maximum database connection attempts reached. Giving up.");
|
||||
break;
|
||||
}
|
||||
|
||||
$attemptCount++;
|
||||
|
||||
try {
|
||||
// Make sure the config cache is cleared before another attempt.
|
||||
$this->artisan->call('config:clear');
|
||||
$this->db->reconnect()->getPdo();
|
||||
|
||||
break;
|
||||
} catch (Throwable $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
// We only try to update credentials if running in interactive mode.
|
||||
// Otherwise, we require admin intervention to fix them.
|
||||
// This avoids inadvertently wiping credentials if there's a connection failure.
|
||||
if ($this->inNoInteractionMode()) {
|
||||
$warning = sprintf(
|
||||
"%sKoel cannot connect to the database. Attempt: %d/%d",
|
||||
PHP_EOL,
|
||||
$attemptCount,
|
||||
self::NON_INTERACTION_MAX_ATTEMPT_COUNT
|
||||
);
|
||||
$this->warn($warning);
|
||||
} else {
|
||||
$this->warn(sprintf("%sKoel cannot connect to the database. Let's set it up.", PHP_EOL));
|
||||
$this->setUpDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function migrateDatabase(): void
|
||||
{
|
||||
$this->info('Migrating database');
|
||||
$this->artisan->call('migrate', ['--force' => true]);
|
||||
|
||||
// Clear the media cache, just in case we did any media-related migration
|
||||
$this->mediaCacheService->clear();
|
||||
}
|
||||
|
||||
private function maybeCompileFrontEndAssets(): void
|
||||
{
|
||||
if ($this->inNoAssetsMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Now to front-end stuff');
|
||||
|
||||
// We need to run several yarn commands:
|
||||
// - The first to install node_modules in the resources/assets submodule
|
||||
// - The second and third for the root folder, to build Koel's front-end assets with Mix.
|
||||
|
||||
chdir('./resources/assets');
|
||||
$this->info('├── Installing Node modules in resources/assets directory');
|
||||
$this->components->info('Now to front-end stuff');
|
||||
|
||||
$runOkOrThrow = static function (string $command): void {
|
||||
passthru($command, $status);
|
||||
|
@ -300,31 +327,35 @@ class InitCommand extends Command
|
|||
};
|
||||
|
||||
$runOkOrThrow('yarn install --colors');
|
||||
|
||||
chdir('../..');
|
||||
$this->info('└── Compiling assets');
|
||||
|
||||
$runOkOrThrow('yarn install --colors');
|
||||
$runOkOrThrow('yarn build --colors');
|
||||
}
|
||||
|
||||
private function isValidMediaPath(string $path): bool
|
||||
{
|
||||
return is_dir($path) && is_readable($path);
|
||||
$this->components->info('Compiling assets');
|
||||
$runOkOrThrow('yarn build');
|
||||
}
|
||||
|
||||
private function setMediaPathFromEnvFile(): void
|
||||
{
|
||||
with(config('koel.media_path'), function (?string $path): void {
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
$path = config('koel.media_path');
|
||||
|
||||
if ($this->isValidMediaPath($path)) {
|
||||
Setting::set('media_path', $path);
|
||||
} else {
|
||||
$this->warn(sprintf('The path %s does not exist or not readable. Skipping.', $path));
|
||||
}
|
||||
});
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isValidMediaPath($path)) {
|
||||
Setting::set('media_path', $path);
|
||||
} else {
|
||||
$this->components->warn(sprintf('The path %s does not exist or not readable. Skipping.', $path));
|
||||
}
|
||||
}
|
||||
|
||||
private static function isValidMediaPath(string $path): bool
|
||||
{
|
||||
return is_dir($path) && is_readable($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random key for the application.
|
||||
*/
|
||||
private function generateRandomKey(): string
|
||||
{
|
||||
return 'base64:' . base64_encode(Encrypter::generateKey($this->laravel['config']['app.cipher']));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Services\LibraryManager;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PruneLibraryCommand extends Command
|
||||
|
@ -10,9 +10,16 @@ class PruneLibraryCommand extends Command
|
|||
protected $signature = 'koel:prune';
|
||||
protected $description = 'Remove empty artists and albums';
|
||||
|
||||
public function handle(): void
|
||||
public function __construct(private LibraryManager $libraryManager)
|
||||
{
|
||||
event(new LibraryChanged());
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->libraryManager->prune();
|
||||
$this->info('Empty artists and albums removed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,72 +4,76 @@ namespace App\Console\Commands;
|
|||
|
||||
use App\Libraries\WatchRecord\InotifyWatchRecord;
|
||||
use App\Models\Setting;
|
||||
use App\Services\FileSynchronizer;
|
||||
use App\Services\MediaSyncService;
|
||||
use App\Values\SyncResult;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
class SyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'koel:sync
|
||||
{record? : A single watch record. Consult Wiki for more info.}
|
||||
{--tags= : The comma-separated tags to sync into the database}
|
||||
{--ignore= : The comma-separated tags to ignore (exclude) from syncing}
|
||||
{--force : Force re-syncing even unchanged files}';
|
||||
|
||||
protected $description = 'Sync songs found in configured directory against the database.';
|
||||
private int $ignored = 0;
|
||||
private int $invalid = 0;
|
||||
private int $synced = 0;
|
||||
private MediaSyncService $mediaSyncService;
|
||||
|
||||
private ?ProgressBar $progressBar = null;
|
||||
private ?string $mediaPath;
|
||||
private ProgressBar $progressBar;
|
||||
|
||||
public function __construct(MediaSyncService $mediaSyncService)
|
||||
public function __construct(private MediaSyncService $mediaSyncService)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->mediaSyncService = $mediaSyncService;
|
||||
$this->mediaSyncService->on('paths-gathered', function (array $paths): void {
|
||||
$this->progressBar = new ProgressBar($this->output, count($paths));
|
||||
});
|
||||
|
||||
$this->mediaSyncService->on('progress', [$this, 'onSyncProgress']);
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
public function handle(): int
|
||||
{
|
||||
$this->ensureMediaPath();
|
||||
$this->mediaPath = $this->getMediaPath();
|
||||
|
||||
$record = $this->argument('record');
|
||||
|
||||
if (!$record) {
|
||||
if ($record) {
|
||||
$this->syncSingleRecord($record);
|
||||
} else {
|
||||
$this->syncAll();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->syngle($record);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all files in the configured media path.
|
||||
*/
|
||||
protected function syncAll(): void
|
||||
private function syncAll(): void
|
||||
{
|
||||
$this->info('Syncing media from ' . Setting::get('media_path') . PHP_EOL);
|
||||
$this->components->info('Scanning ' . $this->mediaPath);
|
||||
|
||||
// Get the tags to sync.
|
||||
// The tags to ignore from syncing.
|
||||
// Notice that this is only meaningful for existing records.
|
||||
// New records will have every applicable field sync'ed in.
|
||||
$tags = $this->option('tags') ? explode(',', $this->option('tags')) : [];
|
||||
// New records will have every applicable field synced in.
|
||||
$ignores = $this->option('ignore') ? explode(',', $this->option('ignore')) : [];
|
||||
|
||||
$this->mediaSyncService->sync(null, $tags, $this->option('force'), $this);
|
||||
$results = $this->mediaSyncService->sync($ignores, $this->option('force'));
|
||||
|
||||
$this->output->writeln(
|
||||
PHP_EOL . PHP_EOL
|
||||
. "<info>Completed! $this->synced new or updated song(s)</info>, "
|
||||
. "$this->ignored unchanged song(s), "
|
||||
. "and <comment>$this->invalid invalid file(s)</comment>."
|
||||
);
|
||||
$this->newLine(2);
|
||||
$this->components->info('Scanning completed!');
|
||||
|
||||
$this->components->bulletList([
|
||||
"<fg=green>{$results->success()->count()}</> new or updated song(s)",
|
||||
"<fg=yellow>{$results->skipped()->count()}</> unchanged song(s)",
|
||||
"<fg=red>{$results->error()->count()}</> invalid file(s)",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SYNc a sinGLE file or directory. See my awesome pun?
|
||||
*
|
||||
* @param string $record The watch record.
|
||||
* As of current we only support inotifywait.
|
||||
* Some examples:
|
||||
|
@ -79,45 +83,39 @@ class SyncCommand extends Command
|
|||
*
|
||||
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
|
||||
*/
|
||||
public function syngle(string $record): void
|
||||
private function syncSingleRecord(string $record): void
|
||||
{
|
||||
$this->mediaSyncService->syncByWatchRecord(new InotifyWatchRecord($record));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a song's sync status to console.
|
||||
*/
|
||||
public function logSyncStatusToConsole(string $path, int $result, ?string $reason = null): void
|
||||
public function onSyncProgress(SyncResult $result): void
|
||||
{
|
||||
$name = basename($path);
|
||||
if (!$this->option('verbose')) {
|
||||
$this->progressBar->advance();
|
||||
|
||||
if ($result === FileSynchronizer::SYNC_RESULT_UNMODIFIED) {
|
||||
++$this->ignored;
|
||||
} elseif ($result === FileSynchronizer::SYNC_RESULT_BAD_FILE) {
|
||||
if ($this->option('verbose')) {
|
||||
$this->error(PHP_EOL . "'$name' is not a valid media file: " . $reason);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
++$this->invalid;
|
||||
} else {
|
||||
++$this->synced;
|
||||
$path = trim(Str::replaceFirst($this->mediaPath, '', $result->path), DIRECTORY_SEPARATOR);
|
||||
|
||||
$this->components->twoColumnDetail($path, match (true) {
|
||||
$result->isSuccess() => "<fg=green>OK</>",
|
||||
$result->isSkipped() => "<fg=yellow>SKIPPED</>",
|
||||
$result->isError() => "<fg=red>ERROR</>",
|
||||
default => throw new RuntimeException("Unknown sync result type: {$result->type}")
|
||||
});
|
||||
|
||||
if ($result->isError()) {
|
||||
$this->output->writeln("<fg=red>$result->error</>");
|
||||
}
|
||||
}
|
||||
|
||||
public function createProgressBar(int $max): void
|
||||
private function getMediaPath(): string
|
||||
{
|
||||
$this->progressBar = $this->getOutput()->createProgressBar($max);
|
||||
}
|
||||
$path = Setting::get('media_path');
|
||||
|
||||
public function advanceProgressBar(): void
|
||||
{
|
||||
$this->progressBar->advance();
|
||||
}
|
||||
|
||||
private function ensureMediaPath(): void
|
||||
{
|
||||
if (Setting::get('media_path')) {
|
||||
return;
|
||||
if ($path) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$this->warn("Media path hasn't been configured. Let's set it up.");
|
||||
|
@ -132,5 +130,7 @@ class SyncCommand extends Command
|
|||
|
||||
$this->error('The path does not exist or is not readable. Try again.');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ class TidyLibraryCommand extends Command
|
|||
protected $signature = 'koel:tidy';
|
||||
protected $hidden = true;
|
||||
|
||||
public function handle(): void
|
||||
public function handle(): int
|
||||
{
|
||||
$this->warn('koel:tidy has been renamed. Use koel:prune instead.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Album;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AlbumInformationFetched extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
private Album $album;
|
||||
private array $information;
|
||||
|
||||
public function __construct(Album $album, array $information)
|
||||
{
|
||||
$this->album = $album;
|
||||
$this->information = $information;
|
||||
}
|
||||
|
||||
public function getAlbum(): Album
|
||||
{
|
||||
return $this->album;
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function getInformation(): array
|
||||
{
|
||||
return $this->information;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Artist;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ArtistInformationFetched
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
private Artist $artist;
|
||||
private array $information;
|
||||
|
||||
public function __construct(Artist $artist, array $information)
|
||||
{
|
||||
$this->artist = $artist;
|
||||
$this->information = $information;
|
||||
}
|
||||
|
||||
public function getArtist(): Artist
|
||||
{
|
||||
return $this->artist;
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function getInformation(): array
|
||||
{
|
||||
return $this->information;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class MediaCacheObsolete extends Event
|
||||
{
|
||||
}
|
|
@ -2,17 +2,14 @@
|
|||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Values\SyncResult;
|
||||
use App\Values\SyncResultCollection;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MediaSyncCompleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public SyncResult $result;
|
||||
|
||||
public function __construct(SyncResult $result)
|
||||
public function __construct(public SyncResultCollection $results)
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,19 +3,13 @@
|
|||
namespace App\Events;
|
||||
|
||||
use App\Models\Interaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SongLikeToggled extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public Interaction $interaction;
|
||||
public ?User $user = null;
|
||||
|
||||
public function __construct(Interaction $interaction, ?User $user = null)
|
||||
public function __construct(public Interaction $interaction)
|
||||
{
|
||||
$this->interaction = $interaction;
|
||||
$this->user = $user ?: auth()->user();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,7 @@ class SongStartedPlaying extends Event
|
|||
{
|
||||
use SerializesModels;
|
||||
|
||||
public Song $song;
|
||||
public User $user;
|
||||
|
||||
public function __construct(Song $song, User $user)
|
||||
public function __construct(public Song $song, public User $user)
|
||||
{
|
||||
$this->song = $song;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,7 @@ class SongsBatchLiked extends Event
|
|||
{
|
||||
use SerializesModels;
|
||||
|
||||
public Collection $songs;
|
||||
public User $user;
|
||||
|
||||
public function __construct(Collection $songs, User $user)
|
||||
public function __construct(public Collection $songs, public User $user)
|
||||
{
|
||||
$this->songs = $songs;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,7 @@ class SongsBatchUnliked extends Event
|
|||
{
|
||||
use SerializesModels;
|
||||
|
||||
public Collection $songs;
|
||||
public User $user;
|
||||
|
||||
public function __construct(Collection $songs, User $user)
|
||||
public function __construct(public Collection $songs, public User $user)
|
||||
{
|
||||
$this->songs = $songs;
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
|
19
app/Exceptions/SpotifyIntegrationDisabledException.php
Normal file
19
app/Exceptions/SpotifyIntegrationDisabledException.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class SpotifyIntegrationDisabledException extends Exception
|
||||
{
|
||||
private function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self('Spotify integration is disabled.');
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Factories;
|
||||
|
||||
use App\Values\SmartPlaylistRule;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class SmartPlaylistRuleParameterFactory
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $value
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function createParameters(string $model, string $operator, array $value): array
|
||||
{
|
||||
$ruleParameterMap = [
|
||||
SmartPlaylistRule::OPERATOR_BEGINS_WITH => [$model, 'LIKE', "$value[0]%"],
|
||||
SmartPlaylistRule::OPERATOR_ENDS_WITH => [$model, 'LIKE', "%$value[0]"],
|
||||
SmartPlaylistRule::OPERATOR_IS => [$model, '=', $value[0]],
|
||||
SmartPlaylistRule::OPERATOR_IS_NOT => [$model, '<>', $value[0]],
|
||||
SmartPlaylistRule::OPERATOR_CONTAINS => [$model, 'LIKE', "%$value[0]%"],
|
||||
SmartPlaylistRule::OPERATOR_NOT_CONTAIN => [$model, 'NOT LIKE', "%$value[0]%"],
|
||||
SmartPlaylistRule::OPERATOR_IS_LESS_THAN => [$model, '<', $value[0]],
|
||||
SmartPlaylistRule::OPERATOR_IS_GREATER_THAN => [$model, '>', $value[0]],
|
||||
SmartPlaylistRule::OPERATOR_IS_BETWEEN => [$model, $value],
|
||||
SmartPlaylistRule::OPERATOR_NOT_IN_LAST => static fn (): array => [$model, '<', now()->subDays($value[0])],
|
||||
SmartPlaylistRule::OPERATOR_IN_LAST => static fn (): array => [$model, '>=', now()->subDays($value[0])],
|
||||
];
|
||||
|
||||
Assert::keyExists($ruleParameterMap, $operator);
|
||||
|
||||
return is_callable($ruleParameterMap[$operator])
|
||||
? $ruleParameterMap[$operator]()
|
||||
: $ruleParameterMap[$operator];
|
||||
}
|
||||
}
|
|
@ -11,21 +11,12 @@ use App\Services\TranscodingService;
|
|||
|
||||
class StreamerFactory
|
||||
{
|
||||
private DirectStreamerInterface $directStreamer;
|
||||
private TranscodingStreamerInterface $transcodingStreamer;
|
||||
private ObjectStorageStreamerInterface $objectStorageStreamer;
|
||||
private TranscodingService $transcodingService;
|
||||
|
||||
public function __construct(
|
||||
DirectStreamerInterface $directStreamer,
|
||||
TranscodingStreamerInterface $transcodingStreamer,
|
||||
ObjectStorageStreamerInterface $objectStorageStreamer,
|
||||
TranscodingService $transcodingService
|
||||
private DirectStreamerInterface $directStreamer,
|
||||
private TranscodingStreamerInterface $transcodingStreamer,
|
||||
private ObjectStorageStreamerInterface $objectStorageStreamer,
|
||||
private TranscodingService $transcodingService
|
||||
) {
|
||||
$this->directStreamer = $directStreamer;
|
||||
$this->transcodingStreamer = $transcodingStreamer;
|
||||
$this->objectStorageStreamer = $objectStorageStreamer;
|
||||
$this->transcodingService = $transcodingService;
|
||||
}
|
||||
|
||||
public function createStreamer(
|
||||
|
|
|
@ -14,48 +14,24 @@ function static_url(?string $name = null): string
|
|||
return $cdnUrl ? $cdnUrl . '/' . trim(ltrim($name, '/')) : trim(asset($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* A copy of Laravel Mix but catered to our directory structure.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
function asset_rev(string $file, ?string $manifestFile = null): string
|
||||
function album_cover_path(?string $fileName): ?string
|
||||
{
|
||||
static $manifest = null;
|
||||
|
||||
$manifestFile = $manifestFile ?: public_path('mix-manifest.json');
|
||||
|
||||
if ($manifest === null) {
|
||||
$manifest = json_decode(file_get_contents($manifestFile), true);
|
||||
}
|
||||
|
||||
if (isset($manifest[$file])) {
|
||||
return file_exists(public_path('hot'))
|
||||
? "http://localhost:8080$manifest[$file]"
|
||||
: static_url($manifest[$file]);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("File $file not defined in asset manifest.");
|
||||
return $fileName ? public_path(config('koel.album_cover_dir') . $fileName) : null;
|
||||
}
|
||||
|
||||
function album_cover_path(string $fileName): string
|
||||
function album_cover_url(?string $fileName): ?string
|
||||
{
|
||||
return public_path(config('koel.album_cover_dir') . $fileName);
|
||||
return $fileName ? static_url(config('koel.album_cover_dir') . $fileName) : null;
|
||||
}
|
||||
|
||||
function album_cover_url(string $fileName): string
|
||||
function artist_image_path(?string $fileName): ?string
|
||||
{
|
||||
return static_url(config('koel.album_cover_dir') . $fileName);
|
||||
return $fileName ? public_path(config('koel.artist_image_dir') . $fileName) : null;
|
||||
}
|
||||
|
||||
function artist_image_path(string $fileName): string
|
||||
function artist_image_url(?string $fileName): ?string
|
||||
{
|
||||
return public_path(config('koel.artist_image_dir') . $fileName);
|
||||
}
|
||||
|
||||
function artist_image_url(string $fileName): string
|
||||
{
|
||||
return static_url(config('koel.artist_image_dir') . $fileName);
|
||||
return $fileName ? static_url(config('koel.artist_image_dir') . $fileName) : null;
|
||||
}
|
||||
|
||||
function koel_version(): string
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\AlbumCoverUpdateRequest;
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaMetadataService;
|
||||
|
@ -10,11 +11,8 @@ use Illuminate\Http\JsonResponse;
|
|||
|
||||
class AlbumCoverController extends Controller
|
||||
{
|
||||
private MediaMetadataService $mediaMetadataService;
|
||||
|
||||
public function __construct(MediaMetadataService $mediaMetadataService)
|
||||
public function __construct(private MediaMetadataService $mediaMetadataService)
|
||||
{
|
||||
$this->mediaMetadataService = $mediaMetadataService;
|
||||
}
|
||||
|
||||
public function update(AlbumCoverUpdateRequest $request, Album $album)
|
||||
|
|
|
@ -2,17 +2,15 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaMetadataService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class AlbumThumbnailController extends Controller
|
||||
{
|
||||
private MediaMetadataService $mediaMetadataService;
|
||||
|
||||
public function __construct(MediaMetadataService $mediaMetadataService)
|
||||
public function __construct(private MediaMetadataService $mediaMetadataService)
|
||||
{
|
||||
$this->mediaMetadataService = $mediaMetadataService;
|
||||
}
|
||||
|
||||
public function show(Album $album): JsonResponse
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ArtistImageUpdateRequest;
|
||||
use App\Models\Artist;
|
||||
use App\Services\MediaMetadataService;
|
||||
|
@ -10,11 +11,8 @@ use Illuminate\Http\JsonResponse;
|
|||
|
||||
class ArtistImageController extends Controller
|
||||
{
|
||||
private MediaMetadataService $mediaMetadataService;
|
||||
|
||||
public function __construct(MediaMetadataService $mediaMetadataService)
|
||||
public function __construct(private MediaMetadataService $mediaMetadataService)
|
||||
{
|
||||
$this->mediaMetadataService = $mediaMetadataService;
|
||||
}
|
||||
|
||||
public function update(ArtistImageUpdateRequest $request, Artist $artist)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\UserLoginRequest;
|
||||
use App\Models\User;
|
||||
use App\Repositories\UserRepository;
|
||||
|
@ -15,23 +16,13 @@ class AuthController extends Controller
|
|||
{
|
||||
use ThrottlesLogins;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
private HashManager $hash;
|
||||
private TokenManager $tokenManager;
|
||||
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
UserRepository $userRepository,
|
||||
HashManager $hash,
|
||||
TokenManager $tokenManager,
|
||||
?Authenticatable $currentUser
|
||||
private UserRepository $userRepository,
|
||||
private HashManager $hash,
|
||||
private TokenManager $tokenManager,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
$this->userRepository = $userRepository;
|
||||
$this->hash = $hash;
|
||||
$this->tokenManager = $tokenManager;
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
public function login(UserLoginRequest $request)
|
||||
|
@ -50,7 +41,9 @@ class AuthController extends Controller
|
|||
|
||||
public function logout()
|
||||
{
|
||||
$this->tokenManager->destroyTokens($this->currentUser);
|
||||
if ($this->user) {
|
||||
$this->tokenManager->destroyTokens($this->user);
|
||||
}
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller as BaseController;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Repositories\InteractionRepository;
|
||||
use App\Repositories\PlaylistRepository;
|
||||
|
@ -18,41 +19,19 @@ class DataController extends Controller
|
|||
{
|
||||
private const RECENTLY_PLAYED_EXCERPT_COUNT = 7;
|
||||
|
||||
private LastfmService $lastfmService;
|
||||
private YouTubeService $youTubeService;
|
||||
private ITunesService $iTunesService;
|
||||
private MediaCacheService $mediaCacheService;
|
||||
private SettingRepository $settingRepository;
|
||||
private PlaylistRepository $playlistRepository;
|
||||
private InteractionRepository $interactionRepository;
|
||||
private UserRepository $userRepository;
|
||||
private ApplicationInformationService $applicationInformationService;
|
||||
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
/** @param User $currentUser */
|
||||
public function __construct(
|
||||
LastfmService $lastfmService,
|
||||
YouTubeService $youTubeService,
|
||||
ITunesService $iTunesService,
|
||||
MediaCacheService $mediaCacheService,
|
||||
SettingRepository $settingRepository,
|
||||
PlaylistRepository $playlistRepository,
|
||||
InteractionRepository $interactionRepository,
|
||||
UserRepository $userRepository,
|
||||
ApplicationInformationService $applicationInformationService,
|
||||
?Authenticatable $currentUser
|
||||
private LastfmService $lastfmService,
|
||||
private YouTubeService $youTubeService,
|
||||
private ITunesService $iTunesService,
|
||||
private MediaCacheService $mediaCacheService,
|
||||
private SettingRepository $settingRepository,
|
||||
private PlaylistRepository $playlistRepository,
|
||||
private InteractionRepository $interactionRepository,
|
||||
private UserRepository $userRepository,
|
||||
private ApplicationInformationService $applicationInformationService,
|
||||
private ?Authenticatable $currentUser
|
||||
) {
|
||||
$this->lastfmService = $lastfmService;
|
||||
$this->youTubeService = $youTubeService;
|
||||
$this->iTunesService = $iTunesService;
|
||||
$this->mediaCacheService = $mediaCacheService;
|
||||
$this->settingRepository = $settingRepository;
|
||||
$this->playlistRepository = $playlistRepository;
|
||||
$this->interactionRepository = $interactionRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->applicationInformationService = $applicationInformationService;
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
|
|
@ -2,20 +2,29 @@
|
|||
|
||||
namespace App\Http\Controllers\API\Interaction;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\BatchInteractionRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\InteractionService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class BatchLikeController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private InteractionService $interactionService, protected ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function store(BatchInteractionRequest $request)
|
||||
{
|
||||
$interactions = $this->interactionService->batchLike((array) $request->songs, $this->currentUser);
|
||||
$interactions = $this->interactionService->batchLike((array) $request->songs, $this->user);
|
||||
|
||||
return response()->json($interactions);
|
||||
}
|
||||
|
||||
public function destroy(BatchInteractionRequest $request)
|
||||
{
|
||||
$this->interactionService->batchUnlike((array) $request->songs, $this->currentUser);
|
||||
$this->interactionService->batchUnlike((array) $request->songs, $this->user);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\Interaction;
|
||||
|
||||
use App\Http\Controllers\Controller as BaseController;
|
||||
use App\Models\User;
|
||||
use App\Services\InteractionService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
protected InteractionService $interactionService;
|
||||
|
||||
/** @var User */
|
||||
protected ?Authenticatable $currentUser = null;
|
||||
|
||||
public function __construct(InteractionService $interactionService, ?Authenticatable $currentUser)
|
||||
{
|
||||
$this->interactionService = $interactionService;
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
}
|
|
@ -2,12 +2,21 @@
|
|||
|
||||
namespace App\Http\Controllers\API\Interaction;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\SongLikeRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\InteractionService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class LikeController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private InteractionService $interactionService, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function store(SongLikeRequest $request)
|
||||
{
|
||||
return response()->json($this->interactionService->toggleLike($request->song, $this->currentUser));
|
||||
return response()->json($this->interactionService->toggleLike($request->song, $this->user));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,22 @@
|
|||
namespace App\Http\Controllers\API\Interaction;
|
||||
|
||||
use App\Events\SongStartedPlaying;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\Interaction\StorePlayCountRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\InteractionService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class PlayCountController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private InteractionService $interactionService, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function store(StorePlayCountRequest $request)
|
||||
{
|
||||
$interaction = $this->interactionService->increasePlayCount($request->song, $this->currentUser);
|
||||
$interaction = $this->interactionService->increasePlayCount($request->song, $this->user);
|
||||
event(new SongStartedPlaying($interaction->song, $interaction->user));
|
||||
|
||||
return response()->json($interaction);
|
||||
|
|
|
@ -2,26 +2,20 @@
|
|||
|
||||
namespace App\Http\Controllers\API\Interaction;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Repositories\InteractionRepository;
|
||||
use App\Services\InteractionService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class RecentlyPlayedController extends Controller
|
||||
{
|
||||
private InteractionRepository $interactionRepository;
|
||||
|
||||
public function __construct(
|
||||
InteractionService $interactionService,
|
||||
InteractionRepository $interactionRepository,
|
||||
?Authenticatable $currentUser
|
||||
) {
|
||||
parent::__construct($interactionService, $currentUser);
|
||||
|
||||
$this->interactionRepository = $interactionRepository;
|
||||
/** @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->currentUser, $count));
|
||||
return response()->json($this->interactionRepository->getRecentlyPlayed($this->user, $count));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\LastfmSetSessionKeyRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\LastfmService;
|
||||
|
@ -9,20 +10,14 @@ use Illuminate\Contracts\Auth\Authenticatable;
|
|||
|
||||
class LastfmController extends Controller
|
||||
{
|
||||
private LastfmService $lastfm;
|
||||
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
public function __construct(LastfmService $lastfm, ?Authenticatable $currentUser)
|
||||
/** @param User $currentUser */
|
||||
public function __construct(private LastfmService $lastfm, private ?Authenticatable $currentUser)
|
||||
{
|
||||
$this->lastfm = $lastfm;
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
public function setSessionKey(LastfmSetSessionKeyRequest $request)
|
||||
{
|
||||
$this->lastfm->setUserSessionKey($this->currentUser, trim($request->key));
|
||||
$this->lastfm->setUserSessionKey($this->currentUser, $request->key);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
namespace App\Http\Controllers\API\MediaInformation;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaInformationService;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
public function __construct(private MediaInformationService $mediaInformationService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(Album $album)
|
||||
{
|
||||
return response()->json($this->mediaInformationService->getAlbumInformation($album));
|
||||
return response()->json($this->mediaInformationService->getAlbumInformation($album)?->toArray() ?: []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
namespace App\Http\Controllers\API\MediaInformation;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artist;
|
||||
use App\Services\MediaInformationService;
|
||||
|
||||
class ArtistController extends Controller
|
||||
{
|
||||
public function __construct(private MediaInformationService $mediaInformationService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(Artist $artist)
|
||||
{
|
||||
return response()->json($this->mediaInformationService->getArtistInformation($artist));
|
||||
return response()->json($this->mediaInformationService->getArtistInformation($artist)?->toArray() ?: []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\MediaInformation;
|
||||
|
||||
use App\Http\Controllers\API\Controller as BaseController;
|
||||
use App\Services\MediaInformationService;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
protected MediaInformationService $mediaInformationService;
|
||||
|
||||
public function __construct(MediaInformationService $mediaInformationService)
|
||||
{
|
||||
$this->mediaInformationService = $mediaInformationService;
|
||||
}
|
||||
}
|
|
@ -2,27 +2,25 @@
|
|||
|
||||
namespace App\Http\Controllers\API\MediaInformation;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Song;
|
||||
use App\Services\MediaInformationService;
|
||||
use App\Services\YouTubeService;
|
||||
|
||||
class SongController extends Controller
|
||||
{
|
||||
private YouTubeService $youTubeService;
|
||||
|
||||
public function __construct(MediaInformationService $mediaInformationService, YouTubeService $youTubeService)
|
||||
{
|
||||
parent::__construct($mediaInformationService);
|
||||
|
||||
$this->youTubeService = $youTubeService;
|
||||
public function __construct(
|
||||
private MediaInformationService $mediaInformationService,
|
||||
private YouTubeService $youTubeService
|
||||
) {
|
||||
}
|
||||
|
||||
public function show(Song $song)
|
||||
{
|
||||
return response()->json([
|
||||
'lyrics' => $song->lyrics,
|
||||
'album_info' => $this->mediaInformationService->getAlbumInformation($song->album),
|
||||
'artist_info' => $this->mediaInformationService->getArtistInformation($song->artist),
|
||||
'lyrics' => nl2br($song->lyrics), // backward compat
|
||||
'album_info' => $this->mediaInformationService->getAlbumInformation($song->album)?->toArray() ?: [],
|
||||
'artist_info' => $this->mediaInformationService->getArtistInformation($song->artist)?->toArray() ?: [],
|
||||
'youtube' => $this->youTubeService->searchVideosRelatedToSong($song),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\ObjectStorage;
|
||||
|
||||
use App\Http\Controllers\API\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\ObjectStorage\S3;
|
||||
|
||||
use App\Http\Controllers\API\ObjectStorage\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\API\ObjectStorage\S3;
|
||||
|
||||
use App\Exceptions\SongPathNotFoundException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ObjectStorage\S3\PutSongRequest;
|
||||
use App\Http\Requests\API\ObjectStorage\S3\RemoveSongRequest;
|
||||
use App\Services\S3Service;
|
||||
|
@ -10,23 +11,20 @@ use Illuminate\Http\Response;
|
|||
|
||||
class SongController extends Controller
|
||||
{
|
||||
private S3Service $s3Service;
|
||||
|
||||
public function __construct(S3Service $s3Service)
|
||||
public function __construct(private S3Service $s3Service)
|
||||
{
|
||||
$this->s3Service = $s3Service;
|
||||
}
|
||||
|
||||
public function put(PutSongRequest $request)
|
||||
{
|
||||
$artist = array_get($request->tags, 'artist', '');
|
||||
$albumartist = trim(array_get($request->tags, 'albumartist', ''));
|
||||
|
||||
$song = $this->s3Service->createSongEntry(
|
||||
$request->bucket,
|
||||
$request->key,
|
||||
$artist,
|
||||
array_get($request->tags, 'album'),
|
||||
(bool) $albumartist && $albumartist !== $artist,
|
||||
trim(array_get($request->tags, 'albumartist')),
|
||||
array_get($request->tags, 'cover'),
|
||||
trim(array_get($request->tags, 'title', '')),
|
||||
(int) array_get($request->tags, 'duration', 0),
|
||||
|
@ -41,7 +39,7 @@ class SongController extends Controller
|
|||
{
|
||||
try {
|
||||
$this->s3Service->deleteSongEntry($request->bucket, $request->key);
|
||||
} catch (SongPathNotFoundException $exception) {
|
||||
} catch (SongPathNotFoundException) {
|
||||
abort(Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\PlaylistStoreRequest;
|
||||
use App\Http\Requests\API\PlaylistUpdateRequest;
|
||||
use App\Models\Playlist;
|
||||
|
@ -12,20 +13,12 @@ use Illuminate\Contracts\Auth\Authenticatable;
|
|||
|
||||
class PlaylistController extends Controller
|
||||
{
|
||||
private PlaylistRepository $playlistRepository;
|
||||
private PlaylistService $playlistService;
|
||||
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
PlaylistRepository $playlistRepository,
|
||||
PlaylistService $playlistService,
|
||||
?Authenticatable $currentUser
|
||||
private PlaylistRepository $playlistRepository,
|
||||
private PlaylistService $playlistService,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
$this->playlistRepository = $playlistRepository;
|
||||
$this->playlistService = $playlistService;
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
@ -37,7 +30,7 @@ class PlaylistController extends Controller
|
|||
{
|
||||
$playlist = $this->playlistService->createPlaylist(
|
||||
$request->name,
|
||||
$this->currentUser,
|
||||
$this->user,
|
||||
(array) $request->songs,
|
||||
$request->rules
|
||||
);
|
||||
|
@ -62,6 +55,6 @@ class PlaylistController extends Controller
|
|||
|
||||
$playlist->delete();
|
||||
|
||||
return response()->json();
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,22 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\PlaylistSongUpdateRequest;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\User;
|
||||
use App\Services\PlaylistService;
|
||||
use App\Services\SmartPlaylistService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class PlaylistSongController extends Controller
|
||||
{
|
||||
private SmartPlaylistService $smartPlaylistService;
|
||||
|
||||
public function __construct(SmartPlaylistService $smartPlaylistService)
|
||||
{
|
||||
$this->smartPlaylistService = $smartPlaylistService;
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private SmartPlaylistService $smartPlaylistService,
|
||||
private PlaylistService $playlistService,
|
||||
private Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Playlist $playlist)
|
||||
|
@ -21,19 +26,20 @@ class PlaylistSongController extends Controller
|
|||
|
||||
return response()->json(
|
||||
$playlist->is_smart
|
||||
? $this->smartPlaylistService->getSongs($playlist)->pluck('id')
|
||||
? $this->smartPlaylistService->getSongs($playlist, $this->user)->pluck('id')
|
||||
: $playlist->songs->pluck('id')
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public function update(PlaylistSongUpdateRequest $request, Playlist $playlist)
|
||||
{
|
||||
$this->authorize('owner', $playlist);
|
||||
|
||||
abort_if($playlist->is_smart, 403, 'A smart playlist\'s content cannot be updated manually.');
|
||||
abort_if($playlist->is_smart, 403, 'A smart playlist cannot be populated manually.');
|
||||
|
||||
$playlist->songs()->sync((array) $request->songs);
|
||||
$this->playlistService->populatePlaylist($playlist, (array) $request->songs);
|
||||
|
||||
return response()->json();
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,41 +2,38 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ProfileUpdateRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use App\Services\TokenManager;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Hashing\Hasher as Hash;
|
||||
use Illuminate\Contracts\Hashing\Hasher;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
private Hash $hash;
|
||||
private TokenManager $tokenManager;
|
||||
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
public function __construct(Hash $hash, TokenManager $tokenManager, ?Authenticatable $currentUser)
|
||||
{
|
||||
$this->hash = $hash;
|
||||
$this->tokenManager = $tokenManager;
|
||||
$this->currentUser = $currentUser;
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private Hasher $hash,
|
||||
private TokenManager $tokenManager,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
return response()->json($this->currentUser);
|
||||
return UserResource::make($this->user);
|
||||
}
|
||||
|
||||
public function update(ProfileUpdateRequest $request)
|
||||
{
|
||||
if (config('koel.misc.demo')) {
|
||||
return response()->json();
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
throw_unless(
|
||||
$this->hash->check($request->current_password, $this->currentUser->password),
|
||||
$this->hash->check($request->current_password, $this->user->password),
|
||||
ValidationException::withMessages(['current_password' => 'Invalid current password'])
|
||||
);
|
||||
|
||||
|
@ -46,12 +43,14 @@ class ProfileController extends Controller
|
|||
$data['password'] = $this->hash->make($request->new_password);
|
||||
}
|
||||
|
||||
$this->currentUser->update($data);
|
||||
$this->user->update($data);
|
||||
|
||||
$responseData = $request->new_password
|
||||
? ['token' => $this->tokenManager->refreshToken($this->currentUser)->plainTextToken]
|
||||
: [];
|
||||
$response = UserResource::make($this->user)->response();
|
||||
|
||||
return response()->json($responseData);
|
||||
if ($request->new_password) {
|
||||
$response->header('Authorization', $this->tokenManager->refreshToken($this->user)->plainTextToken);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ScrobbleStoreRequest;
|
||||
use App\Jobs\ScrobbleJob;
|
||||
use App\Models\Song;
|
||||
|
@ -10,12 +11,9 @@ use Illuminate\Contracts\Auth\Authenticatable;
|
|||
|
||||
class ScrobbleController extends Controller
|
||||
{
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
public function __construct(?Authenticatable $currentUser)
|
||||
/** @param User $currentUser */
|
||||
public function __construct(private ?Authenticatable $currentUser)
|
||||
{
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
public function store(ScrobbleStoreRequest $request, Song $song)
|
||||
|
|
|
@ -2,31 +2,23 @@
|
|||
|
||||
namespace App\Http\Controllers\API\Search;
|
||||
|
||||
use App\Http\Controllers\API\Controller;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SearchService;
|
||||
use Illuminate\Http\Request;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ExcerptSearchController extends Controller
|
||||
{
|
||||
private SearchService $searchService;
|
||||
|
||||
public function __construct(SearchService $searchService)
|
||||
public function __construct(private SearchService $searchService)
|
||||
{
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (!$request->get('q')) {
|
||||
throw new InvalidArgumentException('A search query is required.');
|
||||
}
|
||||
throw_unless((bool) $request->get('q'), 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.');
|
||||
}
|
||||
throw_if($count < 0, new InvalidArgumentException('Invalid count parameter.'));
|
||||
|
||||
return [
|
||||
'results' => $this->searchService->excerptSearch($request->get('q'), $count),
|
||||
|
|
|
@ -2,25 +2,20 @@
|
|||
|
||||
namespace App\Http\Controllers\API\Search;
|
||||
|
||||
use App\Http\Controllers\API\Controller;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SearchService;
|
||||
use Illuminate\Http\Request;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class SongSearchController extends Controller
|
||||
{
|
||||
private SearchService $searchService;
|
||||
|
||||
public function __construct(SearchService $searchService)
|
||||
public function __construct(private SearchService $searchService)
|
||||
{
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (!$request->get('q')) {
|
||||
throw new InvalidArgumentException('A search query is required.');
|
||||
}
|
||||
throw_unless((bool) $request->get('q'), new InvalidArgumentException('A search query is required.'));
|
||||
|
||||
return [
|
||||
'songs' => $this->searchService->searchSongs($request->get('q')),
|
||||
|
|
|
@ -2,26 +2,23 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\SettingRequest;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\MediaSyncService;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
private MediaSyncService $mediaSyncService;
|
||||
|
||||
public function __construct(MediaSyncService $mediaSyncService)
|
||||
public function __construct(private MediaSyncService $mediaSyncService)
|
||||
{
|
||||
$this->mediaSyncService = $mediaSyncService;
|
||||
}
|
||||
|
||||
// @TODO: This should be a PUT request
|
||||
public function store(SettingRequest $request)
|
||||
public function update(SettingRequest $request)
|
||||
{
|
||||
Setting::set('media_path', rtrim(trim($request->media_path), '/'));
|
||||
$this->authorize('admin', User::class);
|
||||
|
||||
// In a next version we should opt for a "MediaPathChanged" event,
|
||||
// but let's just do this async now.
|
||||
Setting::set('media_path', rtrim(trim($request->media_path), '/'));
|
||||
$this->mediaSyncService->sync();
|
||||
|
||||
return response()->noContent();
|
||||
|
|
|
@ -2,30 +2,48 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\SongUpdateRequest;
|
||||
use App\Models\Song;
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Http\Resources\ArtistResource;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\AlbumRepository;
|
||||
use App\Repositories\ArtistRepository;
|
||||
use App\Services\LibraryManager;
|
||||
use App\Services\SongService;
|
||||
use App\Values\SongUpdateData;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class SongController extends Controller
|
||||
{
|
||||
private ArtistRepository $artistRepository;
|
||||
private AlbumRepository $albumRepository;
|
||||
|
||||
public function __construct(ArtistRepository $artistRepository, AlbumRepository $albumRepository)
|
||||
{
|
||||
$this->artistRepository = $artistRepository;
|
||||
$this->albumRepository = $albumRepository;
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private SongService $songService,
|
||||
private AlbumRepository $albumRepository,
|
||||
private ArtistRepository $artistRepository,
|
||||
private LibraryManager $libraryManager,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function update(SongUpdateRequest $request)
|
||||
{
|
||||
$updatedSongs = Song::updateInfo($request->songs, $request->data);
|
||||
$updatedSongs = $this->songService->updateSongs($request->songs, SongUpdateData::fromRequest($request));
|
||||
$albums = $this->albumRepository->getByIds($updatedSongs->pluck('album_id')->toArray(), $this->user);
|
||||
|
||||
$artists = $this->artistRepository->getByIds(
|
||||
array_merge(
|
||||
$updatedSongs->pluck('artist_id')->all(),
|
||||
$updatedSongs->pluck('album_artist_id')->all()
|
||||
)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'artists' => $this->artistRepository->getByIds($updatedSongs->pluck('artist_id')->all()),
|
||||
'albums' => $this->albumRepository->getByIds($updatedSongs->pluck('album_id')->all()),
|
||||
'songs' => $updatedSongs,
|
||||
'songs' => SongResource::collection($updatedSongs),
|
||||
'albums' => AlbumResource::collection($albums),
|
||||
'artists' => ArtistResource::collection($artists),
|
||||
'removed' => $this->libraryManager->prune(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,35 +2,42 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Events\MediaCacheObsolete;
|
||||
use App\Exceptions\MediaPathNotSetException;
|
||||
use App\Exceptions\SongUploadFailedException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\UploadRequest;
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\AlbumRepository;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\UploadService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class UploadController extends Controller
|
||||
{
|
||||
private UploadService $uploadService;
|
||||
/** @param User $user */
|
||||
public function __invoke(
|
||||
UploadService $uploadService,
|
||||
AlbumRepository $albumRepository,
|
||||
SongRepository $songRepository,
|
||||
UploadRequest $request,
|
||||
Authenticatable $user
|
||||
) {
|
||||
$this->authorize('admin', User::class);
|
||||
|
||||
public function __construct(UploadService $uploadService)
|
||||
{
|
||||
$this->uploadService = $uploadService;
|
||||
}
|
||||
|
||||
public function store(UploadRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$song = $this->uploadService->handleUploadedFile($request->file);
|
||||
$song = $songRepository->getOne($uploadService->handleUploadedFile($request->file)->id);
|
||||
|
||||
return response()->json([
|
||||
'song' => SongResource::make($song),
|
||||
'album' => AlbumResource::make($albumRepository->getOne($song->album_id)),
|
||||
]);
|
||||
} catch (MediaPathNotSetException $e) {
|
||||
abort(Response::HTTP_FORBIDDEN, $e->getMessage());
|
||||
} catch (SongUploadFailedException $e) {
|
||||
abort(Response::HTTP_BAD_REQUEST, $e->getMessage());
|
||||
}
|
||||
|
||||
event(new MediaCacheObsolete());
|
||||
|
||||
return response()->json($song->load('album', 'artist'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,48 +2,56 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\UserStoreRequest;
|
||||
use App\Http\Requests\API\UserUpdateRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Hashing\Hasher as Hash;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Services\UserService;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
private Hash $hash;
|
||||
|
||||
public function __construct(Hash $hash)
|
||||
public function __construct(private UserRepository $userRepository, private UserService $userService)
|
||||
{
|
||||
$this->hash = $hash;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('admin', User::class);
|
||||
|
||||
return UserResource::collection($this->userRepository->getAll());
|
||||
}
|
||||
|
||||
public function store(UserStoreRequest $request)
|
||||
{
|
||||
return response()->json(User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => $this->hash->make($request->password),
|
||||
'is_admin' => $request->is_admin,
|
||||
]));
|
||||
$this->authorize('admin', User::class);
|
||||
|
||||
return UserResource::make($this->userService->createUser(
|
||||
$request->name,
|
||||
$request->email,
|
||||
$request->password,
|
||||
$request->get('is_admin') ?: false
|
||||
));
|
||||
}
|
||||
|
||||
public function update(UserUpdateRequest $request, User $user)
|
||||
{
|
||||
$data = $request->only('name', 'email', 'is_admin');
|
||||
$this->authorize('admin', User::class);
|
||||
|
||||
if ($request->password) {
|
||||
$data['password'] = $this->hash->make($request->password);
|
||||
}
|
||||
|
||||
$user->update($data);
|
||||
|
||||
return response()->json($user);
|
||||
return UserResource::make($this->userService->updateUser(
|
||||
$user,
|
||||
$request->name,
|
||||
$request->email,
|
||||
$request->password,
|
||||
$request->get('is_admin') ?: false
|
||||
));
|
||||
}
|
||||
|
||||
public function destroy(User $user)
|
||||
{
|
||||
$this->authorize('destroy', $user);
|
||||
|
||||
$user->delete();
|
||||
$this->userService->deleteUser($user);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
@ -2,17 +2,15 @@
|
|||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\YouTubeSearchRequest;
|
||||
use App\Models\Song;
|
||||
use App\Services\YouTubeService;
|
||||
|
||||
class YouTubeController extends Controller
|
||||
{
|
||||
private YouTubeService $youTubeService;
|
||||
|
||||
public function __construct(YouTubeService $youTubeService)
|
||||
public function __construct(private YouTubeService $youTubeService)
|
||||
{
|
||||
$this->youTubeService = $youTubeService;
|
||||
}
|
||||
|
||||
public function searchVideosRelatedToSong(YouTubeSearchRequest $request, Song $song)
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
|
||||
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));
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
|
||||
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));
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Controllers\API\Controller as BaseController;
|
||||
use App\Services\DownloadService;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
protected DownloadService $downloadService;
|
||||
|
||||
public function __construct(DownloadService $downloadService)
|
||||
{
|
||||
$this->downloadService = $downloadService;
|
||||
}
|
||||
}
|
|
@ -2,24 +2,25 @@
|
|||
|
||||
namespace App\Http\Controllers\Download;
|
||||
|
||||
use App\Http\Requests\Download\Request;
|
||||
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
|
||||
{
|
||||
private InteractionRepository $interactionRepository;
|
||||
|
||||
public function __construct(DownloadService $downloadService, InteractionRepository $interactionRepository)
|
||||
{
|
||||
parent::__construct($downloadService);
|
||||
|
||||
$this->interactionRepository = $interactionRepository;
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private DownloadService $downloadService,
|
||||
private InteractionRepository $interactionRepository,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
public function show()
|
||||
{
|
||||
$songs = $this->interactionRepository->getUserFavorites($request->user());
|
||||
$songs = $this->interactionRepository->getUserFavorites($this->user);
|
||||
|
||||
return response()->download($this->downloadService->from($songs));
|
||||
}
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
|
||||
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('owner', $playlist);
|
||||
|
|
|
@ -2,19 +2,15 @@
|
|||
|
||||
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
|
||||
{
|
||||
private SongRepository $songRepository;
|
||||
|
||||
public function __construct(DownloadService $downloadService, SongRepository $songRepository)
|
||||
public function __construct(private DownloadService $downloadService, private SongRepository $songRepository)
|
||||
{
|
||||
parent::__construct($downloadService);
|
||||
|
||||
$this->songRepository = $songRepository;
|
||||
}
|
||||
|
||||
public function show(SongRequest $request)
|
||||
|
|
|
@ -10,13 +10,8 @@ use Illuminate\Http\Response;
|
|||
|
||||
class ITunesController extends Controller
|
||||
{
|
||||
private ITunesService $iTunesService;
|
||||
private TokenManager $tokenManager;
|
||||
|
||||
public function __construct(ITunesService $iTunesService, TokenManager $tokenManager)
|
||||
public function __construct(private ITunesService $iTunesService, private TokenManager $tokenManager)
|
||||
{
|
||||
$this->iTunesService = $iTunesService;
|
||||
$this->tokenManager = $tokenManager;
|
||||
}
|
||||
|
||||
public function viewSong(ViewSongOnITunesRequest $request, Album $album)
|
||||
|
|
|
@ -11,17 +11,12 @@ use Illuminate\Http\Response;
|
|||
|
||||
class LastfmController extends Controller
|
||||
{
|
||||
private LastfmService $lastfm;
|
||||
private TokenManager $tokenManager;
|
||||
|
||||
/** @var User */
|
||||
private ?Authenticatable $currentUser;
|
||||
|
||||
public function __construct(LastfmService $lastfm, TokenManager $tokenManager, ?Authenticatable $currentUser)
|
||||
{
|
||||
$this->lastfm = $lastfm;
|
||||
$this->tokenManager = $tokenManager;
|
||||
$this->currentUser = $currentUser;
|
||||
/** @param User $currentUser */
|
||||
public function __construct(
|
||||
private LastfmService $lastfm,
|
||||
private TokenManager $tokenManager,
|
||||
private ?Authenticatable $currentUser
|
||||
) {
|
||||
}
|
||||
|
||||
public function connect()
|
||||
|
|
|
@ -8,17 +8,14 @@ use App\Models\Song;
|
|||
|
||||
class PlayController extends Controller
|
||||
{
|
||||
private StreamerFactory $streamerFactory;
|
||||
|
||||
public function __construct(StreamerFactory $streamerFactory)
|
||||
public function __construct(private StreamerFactory $streamerFactory)
|
||||
{
|
||||
$this->streamerFactory = $streamerFactory;
|
||||
}
|
||||
|
||||
public function show(SongPlayRequest $request, Song $song, ?bool $transcode = null, ?int $bitRate = null)
|
||||
{
|
||||
return $this->streamerFactory
|
||||
->createStreamer($song, $transcode, $bitRate, floatval($request->time))
|
||||
->createStreamer($song, $transcode, $bitRate, (float) $request->time)
|
||||
->stream();
|
||||
}
|
||||
}
|
||||
|
|
33
app/Http/Controllers/V6/API/AlbumController.php
Normal file
33
app/Http/Controllers/V6/API/AlbumController.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Models\Album;
|
||||
use App\Models\User;
|
||||
use App\Repositories\AlbumRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private AlbumRepository $albumRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$pagination = Album::withMeta($this->user)
|
||||
->isStandard()
|
||||
->orderBy('albums.name')
|
||||
->simplePaginate(21);
|
||||
|
||||
return AlbumResource::collection($pagination);
|
||||
}
|
||||
|
||||
public function show(Album $album)
|
||||
{
|
||||
return AlbumResource::make($this->albumRepository->getOne($album->id, $this->user));
|
||||
}
|
||||
}
|
23
app/Http/Controllers/V6/API/AlbumSongController.php
Normal file
23
app/Http/Controllers/V6/API/AlbumSongController.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Album;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class AlbumSongController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private SongRepository $songRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Album $album)
|
||||
{
|
||||
return SongResource::collection($this->songRepository->getByAlbum($album, $this->user));
|
||||
}
|
||||
}
|
33
app/Http/Controllers/V6/API/ArtistController.php
Normal file
33
app/Http/Controllers/V6/API/ArtistController.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ArtistResource;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
use App\Repositories\ArtistRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class ArtistController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private ArtistRepository $artistRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$pagination = Artist::withMeta($this->user)
|
||||
->isStandard()
|
||||
->orderBy('artists.name')
|
||||
->simplePaginate(21);
|
||||
|
||||
return ArtistResource::collection($pagination);
|
||||
}
|
||||
|
||||
public function show(Artist $artist)
|
||||
{
|
||||
return ArtistResource::make($this->artistRepository->getOne($artist->id, $this->user));
|
||||
}
|
||||
}
|
23
app/Http/Controllers/V6/API/ArtistSongController.php
Normal file
23
app/Http/Controllers/V6/API/ArtistSongController.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class ArtistSongController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private SongRepository $songRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Artist $artist)
|
||||
{
|
||||
return SongResource::collection($this->songRepository->getByArtist($artist, $this->user));
|
||||
}
|
||||
}
|
53
app/Http/Controllers/V6/API/DataController.php
Normal file
53
app/Http/Controllers/V6/API/DataController.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\PlaylistRepository;
|
||||
use App\Repositories\SettingRepository;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\ApplicationInformationService;
|
||||
use App\Services\ITunesService;
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\YouTubeService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class DataController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private LastfmService $lastfmService,
|
||||
private YouTubeService $youTubeService,
|
||||
private ITunesService $iTunesService,
|
||||
private SettingRepository $settingRepository,
|
||||
private PlaylistRepository $playlistRepository,
|
||||
private SongRepository $songRepository,
|
||||
private ApplicationInformationService $applicationInformationService,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return response()->json([
|
||||
'settings' => $this->user->is_admin ? $this->settingRepository->getAllAsKeyValueArray() : [],
|
||||
'playlists' => $this->playlistRepository->getAllByCurrentUser(),
|
||||
'current_user' => UserResource::make($this->user),
|
||||
'use_last_fm' => $this->lastfmService->used(),
|
||||
'use_you_tube' => $this->youTubeService->enabled(), // @todo clean this mess up
|
||||
'use_i_tunes' => $this->iTunesService->used(),
|
||||
'allow_download' => config('koel.download.allow'),
|
||||
'supports_transcoding' => config('koel.streaming.ffmpeg_path')
|
||||
&& is_executable(config('koel.streaming.ffmpeg_path')),
|
||||
'cdn_url' => static_url(),
|
||||
'current_version' => koel_version(),
|
||||
'latest_version' => $this->user->is_admin
|
||||
? $this->applicationInformationService->getLatestVersionNumber()
|
||||
: koel_version(),
|
||||
'song_count' => $this->songRepository->count(),
|
||||
'song_length' => $this->songRepository->getTotalLength(),
|
||||
]);
|
||||
}
|
||||
}
|
19
app/Http/Controllers/V6/API/ExcerptSearchController.php
Normal file
19
app/Http/Controllers/V6/API/ExcerptSearchController.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\V6\Requests\SearchRequest;
|
||||
use App\Http\Resources\ExcerptSearchResource;
|
||||
use App\Models\User;
|
||||
use App\Services\V6\SearchService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class ExcerptSearchController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __invoke(SearchRequest $request, SearchService $searchService, Authenticatable $user)
|
||||
{
|
||||
return ExcerptSearchResource::make($searchService->excerptSearch($request->q, $user));
|
||||
}
|
||||
}
|
22
app/Http/Controllers/V6/API/FavoriteSongController.php
Normal file
22
app/Http/Controllers/V6/API/FavoriteSongController.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class FavoriteSongController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private SongRepository $songRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return SongResource::collection($this->songRepository->getFavorites($this->user));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Album;
|
||||
use App\Services\MediaInformationService;
|
||||
|
||||
class FetchAlbumInformationController extends Controller
|
||||
{
|
||||
public function __invoke(Album $album, MediaInformationService $informationService)
|
||||
{
|
||||
return response()->json($informationService->getAlbumInformation($album));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artist;
|
||||
use App\Services\MediaInformationService;
|
||||
|
||||
class FetchArtistInformationController extends Controller
|
||||
{
|
||||
public function __invoke(Artist $artist, MediaInformationService $informationService)
|
||||
{
|
||||
return response()->json($informationService->getArtistInformation($artist));
|
||||
}
|
||||
}
|
33
app/Http/Controllers/V6/API/OverviewController.php
Normal file
33
app/Http/Controllers/V6/API/OverviewController.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Http\Resources\ArtistResource;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Repositories\AlbumRepository;
|
||||
use App\Repositories\ArtistRepository;
|
||||
use App\Repositories\SongRepository;
|
||||
|
||||
class OverviewController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private SongRepository $songRepository,
|
||||
private AlbumRepository $albumRepository,
|
||||
private ArtistRepository $artistRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return response()->json([
|
||||
'most_played_songs' => SongResource::collection($this->songRepository->getMostPlayed()),
|
||||
'recently_played_songs' => SongResource::collection($this->songRepository->getRecentlyPlayed()),
|
||||
'recently_added_albums' => AlbumResource::collection($this->albumRepository->getRecentlyAdded()),
|
||||
'recently_added_songs' => SongResource::collection($this->songRepository->getRecentlyAdded()),
|
||||
'most_played_artists' => ArtistResource::collection($this->artistRepository->getMostPlayed()),
|
||||
'most_played_albums' => AlbumResource::collection($this->albumRepository->getMostPlayed()),
|
||||
]);
|
||||
}
|
||||
}
|
27
app/Http/Controllers/V6/API/PlayCountController.php
Normal file
27
app/Http/Controllers/V6/API/PlayCountController.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Events\SongStartedPlaying;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\Interaction\StorePlayCountRequest;
|
||||
use App\Http\Resources\InteractionResource;
|
||||
use App\Models\User;
|
||||
use App\Services\InteractionService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class PlayCountController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private InteractionService $interactionService, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function store(StorePlayCountRequest $request)
|
||||
{
|
||||
$interaction = $this->interactionService->increasePlayCount($request->song, $this->user);
|
||||
event(new SongStartedPlaying($interaction->song, $interaction->user));
|
||||
|
||||
return InteractionResource::make($interaction);
|
||||
}
|
||||
}
|
60
app/Http/Controllers/V6/API/PlaylistSongController.php
Normal file
60
app/Http/Controllers/V6/API/PlaylistSongController.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\V6\Requests\AddSongsToPlaylistRequest;
|
||||
use App\Http\Controllers\V6\Requests\RemoveSongsFromPlaylistRequest;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\PlaylistService;
|
||||
use App\Services\SmartPlaylistService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class PlaylistSongController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private SongRepository $songRepository,
|
||||
private PlaylistService $playlistService,
|
||||
private SmartPlaylistService $smartPlaylistService,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Playlist $playlist)
|
||||
{
|
||||
$this->authorize('owner', $playlist);
|
||||
|
||||
return SongResource::collection(
|
||||
$playlist->is_smart
|
||||
? $this->smartPlaylistService->getSongs($playlist, $this->user)
|
||||
: $this->songRepository->getByStandardPlaylist($playlist, $this->user)
|
||||
);
|
||||
}
|
||||
|
||||
public function add(Playlist $playlist, AddSongsToPlaylistRequest $request)
|
||||
{
|
||||
$this->authorize('owner', $playlist);
|
||||
|
||||
abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN);
|
||||
|
||||
$this->playlistService->addSongsToPlaylist($playlist, $request->songs);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
public function remove(Playlist $playlist, RemoveSongsFromPlaylistRequest $request)
|
||||
{
|
||||
$this->authorize('owner', $playlist);
|
||||
|
||||
abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN);
|
||||
|
||||
$this->playlistService->removeSongsFromPlaylist($playlist, $request->songs);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
34
app/Http/Controllers/V6/API/QueueController.php
Normal file
34
app/Http/Controllers/V6/API/QueueController.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\V6\Requests\QueueFetchSongRequest;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class QueueController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private SongRepository $songRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function fetchSongs(QueueFetchSongRequest $request)
|
||||
{
|
||||
if ($request->order === 'rand') {
|
||||
return SongResource::collection($this->songRepository->getRandom($request->limit, $this->user));
|
||||
} else {
|
||||
return SongResource::collection(
|
||||
$this->songRepository->getForQueue(
|
||||
$request->sort,
|
||||
$request->order,
|
||||
$request->limit,
|
||||
$this->user,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
24
app/Http/Controllers/V6/API/RecentlyPlayedSongController.php
Normal file
24
app/Http/Controllers/V6/API/RecentlyPlayedSongController.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class RecentlyPlayedSongController extends Controller
|
||||
{
|
||||
private const MAX_ITEM_COUNT = 128;
|
||||
|
||||
/** @param User $user */
|
||||
public function __construct(private SongRepository $songRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return SongResource::collection($this->songRepository->getRecentlyPlayed(self::MAX_ITEM_COUNT, $this->user));
|
||||
}
|
||||
}
|
35
app/Http/Controllers/V6/API/SongController.php
Normal file
35
app/Http/Controllers/V6/API/SongController.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\V6\Requests\SongListRequest;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class SongController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(private SongRepository $songRepository, private ?Authenticatable $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(Song $song)
|
||||
{
|
||||
return SongResource::make($this->songRepository->getOne($song->id));
|
||||
}
|
||||
|
||||
public function index(SongListRequest $request)
|
||||
{
|
||||
return SongResource::collection(
|
||||
$this->songRepository->getForListing(
|
||||
$request->sort ?: 'songs.title',
|
||||
$request->order ?: 'asc',
|
||||
$this->user
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
19
app/Http/Controllers/V6/API/SongSearchController.php
Normal file
19
app/Http/Controllers/V6/API/SongSearchController.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\V6\Requests\SearchRequest;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\User;
|
||||
use App\Services\V6\SearchService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class SongSearchController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __invoke(SearchRequest $request, SearchService $searchService, Authenticatable $user)
|
||||
{
|
||||
return SongResource::collection($searchService->searchSongs($request->q, $user));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\Requests;
|
||||
|
||||
use App\Http\Requests\API\Request;
|
||||
|
||||
/**
|
||||
* @property-read array<string> $songs
|
||||
*/
|
||||
class AddSongsToPlaylistRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'songs' => 'required|array',
|
||||
'songs.*' => 'exists:songs,id',
|
||||
];
|
||||
}
|
||||
}
|
28
app/Http/Controllers/V6/Requests/QueueFetchSongRequest.php
Normal file
28
app/Http/Controllers/V6/Requests/QueueFetchSongRequest.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\Requests;
|
||||
|
||||
use App\Http\Requests\API\Request;
|
||||
use App\Repositories\SongRepository;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* @property-read string|null $sort
|
||||
* @property-read string $order
|
||||
* @property-read int $limit
|
||||
*/
|
||||
class QueueFetchSongRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'order' => ['required', Rule::in('asc', 'desc', 'rand')],
|
||||
'limit' => 'required|integer|min:1',
|
||||
'sort' => [
|
||||
'required_unless:order,rand',
|
||||
Rule::in(array_keys(SongRepository::SORT_COLUMNS_NORMALIZE_MAP)),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\Requests;
|
||||
|
||||
use App\Http\Requests\API\Request;
|
||||
|
||||
/**
|
||||
* @property-read array<string> $songs
|
||||
*/
|
||||
class RemoveSongsFromPlaylistRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'songs' => 'required|array',
|
||||
'songs.*' => 'exists:songs,id',
|
||||
];
|
||||
}
|
||||
}
|
17
app/Http/Controllers/V6/Requests/SearchRequest.php
Normal file
17
app/Http/Controllers/V6/Requests/SearchRequest.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\Requests;
|
||||
|
||||
use App\Http\Requests\API\Request;
|
||||
|
||||
/**
|
||||
* @property-read string $q
|
||||
*/
|
||||
class SearchRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return ['q' => 'required'];
|
||||
}
|
||||
}
|
13
app/Http/Controllers/V6/Requests/SongListRequest.php
Normal file
13
app/Http/Controllers/V6/Requests/SongListRequest.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\Requests;
|
||||
|
||||
use App\Http\Requests\API\Request;
|
||||
|
||||
/**
|
||||
* @property-read string $order
|
||||
* @property-read string $sort
|
||||
*/
|
||||
class SongListRequest extends Request
|
||||
{
|
||||
}
|
12
app/Http/Controllers/V6/Requests/YouTubeSearchRequest.php
Normal file
12
app/Http/Controllers/V6/Requests/YouTubeSearchRequest.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V6\Requests;
|
||||
|
||||
use App\Http\Requests\API\Request;
|
||||
|
||||
/**
|
||||
* @property-read string|null $pageToken
|
||||
*/
|
||||
class YouTubeSearchRequest extends Request
|
||||
{
|
||||
}
|
|
@ -5,14 +5,13 @@ namespace App\Http;
|
|||
use App\Http\Middleware\Authenticate;
|
||||
use App\Http\Middleware\ForceHttps;
|
||||
use App\Http\Middleware\ObjectStorageAuthenticate;
|
||||
use App\Http\Middleware\ThrottleRequests;
|
||||
use App\Http\Middleware\TrimStrings;
|
||||
use Illuminate\Auth\Middleware\Authorize;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
|
@ -25,7 +24,6 @@ class Kernel extends HttpKernel
|
|||
CheckForMaintenanceMode::class,
|
||||
ValidatePostSize::class,
|
||||
TrimStrings::class,
|
||||
ConvertEmptyStringsToNull::class,
|
||||
ForceHttps::class,
|
||||
];
|
||||
|
||||
|
|
|
@ -8,17 +8,14 @@ use Illuminate\Http\Request;
|
|||
|
||||
class Authenticate
|
||||
{
|
||||
protected Guard $auth;
|
||||
|
||||
public function __construct(Guard $auth)
|
||||
public function __construct(protected Guard $auth)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next) // @phpcs:ignore
|
||||
{
|
||||
if ($this->auth->guest()) {
|
||||
if ($request->ajax() || $request->route()->getName() === 'play') {
|
||||
if ($request->ajax() || $request->wantsJson() || $request->route()->getName() === 'play') {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
return redirect()->guest('/');
|
||||
|
|
|
@ -8,11 +8,8 @@ use Illuminate\Routing\UrlGenerator;
|
|||
|
||||
class ForceHttps
|
||||
{
|
||||
private UrlGenerator $url;
|
||||
|
||||
public function __construct(UrlGenerator $url)
|
||||
public function __construct(private UrlGenerator $url)
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next) // @phpcs:ignore
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Http\Middleware;
|
|||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
/**
|
||||
* Authenticate requests from Object Storage services (like S3).
|
||||
|
@ -13,9 +14,7 @@ class ObjectStorageAuthenticate
|
|||
{
|
||||
public function handle(Request $request, Closure $next) // @phpcs:ignore
|
||||
{
|
||||
if ($request->appKey !== config('app.key')) {
|
||||
return response('Unauthorized.', 401);
|
||||
}
|
||||
abort_unless($request->get('appKey') === config('app.key'), Response::HTTP_UNAUTHORIZED);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
|
19
app/Http/Middleware/ThrottleRequests.php
Normal file
19
app/Http/Middleware/ThrottleRequests.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests as BaseThrottleRequests;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ThrottleRequests extends BaseThrottleRequests
|
||||
{
|
||||
public function handle($request, Closure $next, $maxAttempts = 300, $decayMinutes = 1, $prefix = ''): Response
|
||||
{
|
||||
if (app()->environment('production')) {
|
||||
return parent::handle($request, $next, $maxAttempts, $decayMinutes, $prefix);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Requests\API;
|
||||
|
||||
/** @property string $cover */
|
||||
class AlbumCoverUpdateRequest extends AbstractMediaImageUpdateRequest
|
||||
class AlbumCoverUpdateRequest extends MediaImageUpdateRequest
|
||||
{
|
||||
protected function getImageFieldName(): string
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Requests\API;
|
||||
|
||||
/** @property string $image */
|
||||
class ArtistImageUpdateRequest extends AbstractMediaImageUpdateRequest
|
||||
class ArtistImageUpdateRequest extends MediaImageUpdateRequest
|
||||
{
|
||||
protected function getImageFieldName(): string
|
||||
{
|
||||
|
|
|
@ -4,6 +4,6 @@ namespace App\Http\Requests\API\Interaction;
|
|||
|
||||
use App\Http\Requests\API\Request as BaseRequest;
|
||||
|
||||
class Request extends BaseRequest
|
||||
abstract class Request extends BaseRequest
|
||||
{
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace App\Http\Requests\API;
|
|||
|
||||
use App\Rules\ImageData;
|
||||
|
||||
abstract class AbstractMediaImageUpdateRequest extends Request
|
||||
abstract class MediaImageUpdateRequest extends Request
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Http\Requests\API;
|
||||
|
||||
use App\Http\Requests\AbstractRequest;
|
||||
use App\Http\Requests\Request as BaseRequest;
|
||||
|
||||
class Request extends AbstractRequest
|
||||
abstract class Request extends BaseRequest
|
||||
{
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue