mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: allow bitrate options for mobile transcoding
This commit is contained in:
parent
6664e1d1ea
commit
c3e5e8ac5a
14 changed files with 143 additions and 18 deletions
|
@ -4,17 +4,20 @@ namespace App\Http\Controllers;
|
|||
|
||||
use App\Http\Requests\SongPlayRequest;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\Streamer\Streamer;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class PlayController extends Controller
|
||||
{
|
||||
public function __invoke(SongPlayRequest $request, Song $song, ?bool $transcode = null, ?int $bitRate = null)
|
||||
/** @param User $user */
|
||||
public function __invoke(Authenticatable $user, SongPlayRequest $request, Song $song, ?bool $transcode = null)
|
||||
{
|
||||
$this->authorize('access', $song);
|
||||
|
||||
return (new Streamer(song: $song, config: [
|
||||
'transcode' => (bool) $transcode,
|
||||
'bit_rate' => $bitRate,
|
||||
'bit_rate' => $user->preferences->transcodeQuality,
|
||||
'start_time' => (float) $request->time,
|
||||
]))->stream();
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
|||
'show_now_playing_notification' => 'boolean',
|
||||
'confirm_before_closing' => 'boolean',
|
||||
'transcode_on_mobile' => 'boolean',
|
||||
'transcode_quality' => 'integer',
|
||||
'show_album_art_overlay' => 'boolean',
|
||||
'lyrics_zoom_level' => 'integer',
|
||||
'make_uploads_public' => 'boolean',
|
||||
|
@ -29,6 +30,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
|||
'show_now_playing_notification',
|
||||
'confirm_before_closing',
|
||||
'transcode_on_mobile',
|
||||
'transcode_quality',
|
||||
'show_album_art_overlay',
|
||||
'make_uploads_public',
|
||||
'support_bar_no_bugging',
|
||||
|
@ -50,6 +52,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
|||
public bool $showNowPlayingNotification,
|
||||
public bool $confirmBeforeClosing,
|
||||
public bool $transcodeOnMobile,
|
||||
public int $transcodeQuality,
|
||||
public bool $showAlbumArtOverlay,
|
||||
public bool $makeUploadsPublic,
|
||||
public bool $supportBarNoBugging,
|
||||
|
@ -63,6 +66,10 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
|||
Assert::oneOf($this->artistsViewMode, ['list', 'thumbnails']);
|
||||
Assert::oneOf($this->albumsViewMode, ['list', 'thumbnails']);
|
||||
Assert::oneOf($this->activeExtraPanelTab, [null, 'Lyrics', 'Artist', 'Album', 'YouTube']);
|
||||
|
||||
if (!in_array($this->transcodeQuality, [64, 96, 128, 192, 256, 320], true)) {
|
||||
$this->transcodeQuality = 128;
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
|
@ -77,6 +84,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
|||
showNowPlayingNotification: $data['show_now_playing_notification'] ?? true,
|
||||
confirmBeforeClosing: $data['confirm_before_closing'] ?? false,
|
||||
transcodeOnMobile: $data['transcode_on_mobile'] ?? true,
|
||||
transcodeQuality: $data['transcode_quality'] ?? 128,
|
||||
showAlbumArtOverlay: $data['show_album_art_overlay'] ?? true,
|
||||
makeUploadsPublic: $data['make_uploads_public'] ?? false,
|
||||
supportBarNoBugging: $data['support_bar_no_bugging'] ?? false,
|
||||
|
@ -126,6 +134,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
|||
'confirm_before_closing' => $this->confirmBeforeClosing,
|
||||
'show_album_art_overlay' => $this->showAlbumArtOverlay,
|
||||
'transcode_on_mobile' => $this->transcodeOnMobile,
|
||||
'transcode_quality' => $this->transcodeQuality,
|
||||
'make_uploads_public' => $this->makeUploadsPublic,
|
||||
'lastfm_session_key' => $this->lastFmSessionKey,
|
||||
'support_bar_no_bugging' => $this->supportBarNoBugging,
|
||||
|
|
|
@ -39,7 +39,7 @@ Koel allows you to set a couple of preferences:
|
|||
* Whether to show a notification whenever a new song starts playing
|
||||
* Whether to confirm before closing Koel’s browser tab
|
||||
* Whether to show a translucent, blurred overlay of the current album’s art
|
||||
* Whether to transcode music on the fly (mobile only, useful if you have a slow network connection)
|
||||
* Whether to transcode music to a lower bitrate (mobile only, useful if you have a slow connection)
|
||||
* Whether to set your uploaded music as public by default <PlusBadge />
|
||||
|
||||
These preferences are saved immediately upon change and synced across all of your devices.
|
||||
|
|
|
@ -26,10 +26,17 @@ If you're using [Koel mobile app](https://koel.dev/#mobile) and can't play the s
|
|||
Koel always uses the native PHP method if you're transcoding or streaming from a cloud storage.
|
||||
:::
|
||||
|
||||
## Transcoding FLAC
|
||||
## FLAC Transcoding
|
||||
|
||||
Koel supports transcoding FLAC to mp3 on the fly when streaming music. This behavior can be controlled via a `TRANSCODE_FLAC` setting in `.env` file:
|
||||
By default, Koel streams FLAC files as-is, which means the lossless audio quality is preserved.
|
||||
However, you can opt to have FLAC transcoded to mp3 to, for example, reduce bandwidth or support older devices. This behavior can be controlled via the `TRANSCODE_FLAC` setting in `.env` file:
|
||||
|
||||
* `false`: Disable FLAC transcoding. Koel will stream FLAC files as-is, producing the lossless audio quality. This is the default behavior.
|
||||
* `true`: Enable FLAC transcoding. Koel will transcode FLAC to mp3 on the fly. You'll need to have [FFmpeg](https://ffmpeg.org/) installed on your server and set its executable path via the `FFMPEG_PATH` setting in the `.env` file. The transcoding quality can also be controlled via `OUTPUT_BIT_RATE` (defaults to `128`).
|
||||
* `false`: Disable transcoding and stream FLAC files as-is, producing the lossless audio quality. This is the default behavior.
|
||||
* `true`: Transcode FLAC to mp3 before streaming. You'll need to have [FFmpeg](https://ffmpeg.org/) installed on your server and set its executable path via the `FFMPEG_PATH` setting in the `.env` file. The transcoding quality can also be controlled via `OUTPUT_BIT_RATE` (defaults to `128`).
|
||||
|
||||
When transcoding is enabled, Koel caches the transcoded files to improve performance. If for any reason you want to clear the cache, run `php artisan cache:clear`.
|
||||
|
||||
## Transcoding on Mobile
|
||||
|
||||
On a mobile device where data usage is a concern, you might want to transcode all songs to a lower bitrate to save bandwidth.
|
||||
This can be done via the [Preferences screen](./profile-preferences#preferences) and requires the same FFmpeg setup as FLAC transcoding.
|
||||
|
|
|
@ -56,6 +56,7 @@ export default abstract class UnitTestCase {
|
|||
commonStore.state.allows_download = true
|
||||
commonStore.state.uses_i_tunes = true
|
||||
commonStore.state.supports_batch_downloading = true
|
||||
commonStore.state.supports_transcoding = true
|
||||
cb && cb()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import { screen } from '@testing-library/vue'
|
||||
import isMobile from 'ismobilejs'
|
||||
import { commonStore } from '@/stores'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import PreferencesForm from './PreferencesForm.vue'
|
||||
|
||||
|
@ -9,13 +10,19 @@ new class extends UnitTestCase {
|
|||
it('has "Transcode on mobile" option for mobile users', () => {
|
||||
isMobile.phone = true
|
||||
this.render(PreferencesForm)
|
||||
screen.getByRole('checkbox', { name: 'Convert and play media at 128kbps on mobile' })
|
||||
screen.getByTestId('transcode_on_mobile')
|
||||
})
|
||||
|
||||
it('does not have "Transcode on mobile" option for non-mobile users', async () => {
|
||||
isMobile.phone = false
|
||||
this.render(PreferencesForm)
|
||||
expect(screen.queryByRole('checkbox', { name: 'Convert and play media at 128kbps on mobile' })).toBeNull()
|
||||
expect(screen.queryByTestId('transcode_on_mobile')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not have "Transcode on mobile" option if transcoding is not supported', async () => {
|
||||
isMobile.phone = true
|
||||
commonStore.state.supports_transcoding = false
|
||||
expect(screen.queryByTestId('transcode_on_mobile')).toBeNull()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,22 @@
|
|||
Confirm before closing Koel
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow v-if="isPhone">
|
||||
<FormRow v-if="showTranscodingOption">
|
||||
<div>
|
||||
<CheckBox v-model="preferences.transcode_on_mobile" name="transcode_on_mobile" />
|
||||
Convert and play media at 128kbps on mobile
|
||||
<CheckBox
|
||||
v-model="preferences.transcode_on_mobile"
|
||||
data-testid="transcode_on_mobile"
|
||||
name="transcode_on_mobile"
|
||||
/>
|
||||
Convert and play media at
|
||||
<select
|
||||
v-model="preferences.transcode_quality"
|
||||
:disabled="!preferences.transcode_on_mobile"
|
||||
class="appearance-auto rounded"
|
||||
>
|
||||
<option v-for="quality in [64, 96, 128, 192, 256, 320]" :value="quality" :key="quality">{{ quality }}</option>
|
||||
</select>
|
||||
kbps on mobile
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
|
@ -41,7 +53,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import isMobile from 'ismobilejs'
|
||||
import { preferenceStore as preferences } from '@/stores'
|
||||
import { computed } from 'vue'
|
||||
import { commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useKoelPlus } from '@/composables'
|
||||
|
||||
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
||||
|
@ -49,6 +62,8 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
|||
|
||||
const isPhone = isMobile.phone
|
||||
const { isPlus } = useKoelPlus()
|
||||
|
||||
const showTranscodingOption = computed(() => isPhone && commonStore.state.supports_transcoding)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
|
|
@ -40,6 +40,46 @@ exports[`renders 1`] = `
|
|||
</section>
|
||||
`;
|
||||
|
||||
exports[`renders 2`] = `
|
||||
<section data-v-dcfb8776="" data-v-8ea4eaa5="" class="max-h-full min-h-full w-full flex flex-col transform-gpu">
|
||||
<header data-v-5691beb5="" data-v-8ea4eaa5="" class="expanded screen-header min-h-0 md:min-h-full flex items-end flex-shrink-0 relative content-stretch leading-normal p-6 border-b border-b-k-bg-secondary">
|
||||
<aside data-v-5691beb5="" class="thumbnail-wrapper hidden md:block overflow-hidden w-0 rounded-md">
|
||||
<article data-v-55bfc268="" data-v-8ea4eaa5="" data-v-5691beb5-s="" class="single thumbnail-stack aspect-square overflow-hidden grid bg-cover bg-no-repeat" style="background-image: url("undefined/resources/assets/img/covers/default.svg");"><span data-v-55bfc268="" style="" class="block will-change-transform w-full h-full bg-cover bg-no-repeat" data-testid="thumbnail"></span></article>
|
||||
</aside>
|
||||
<main data-v-5691beb5="" class="flex flex-1 gap-5 items-center overflow-hidden">
|
||||
<div data-v-5691beb5="" class="w-full flex-1 overflow-hidden">
|
||||
<h1 data-v-5691beb5="" class="name overflow-hidden whitespace-nowrap text-ellipsis mr-4 font-thin md:font-bold my-0 leading-tight"> All Songs
|
||||
<!--v-if-->
|
||||
</h1><span data-v-5691beb5="" class="meta text-k-text-secondary hidden text-[0.9rem] leading-loose"><span data-v-8ea4eaa5="" data-v-5691beb5-s="">420 songs</span><span data-v-8ea4eaa5="" data-v-5691beb5-s="">10 sec</span></span>
|
||||
</div>
|
||||
<div data-v-8ea4eaa5="" data-v-5691beb5-s="" class="controls w-full min-h-[32px] flex justify-between items-center gap-4">
|
||||
<div data-v-8ea4eaa5="" data-v-5691beb5-s="" class="relative" data-testid="song-list-controls">
|
||||
<div class="flex gap-2 flex-wrap"><span data-v-cf9b67d8="" class="btn-group inline-flex relative flex-nowrap" uppercase=""><button data-v-8943c846="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer btn-shuffle-all" type="button" data-testid="btn-shuffle-all" highlight="" title="Shuffle all. Press Alt/⌥ to change mode."><br data-testid="Icon" icon="[object Object]" fixed-width=""> All </button><!--v-if--><!--v-if--><!--v-if--></span>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<div>
|
||||
<div class="context-menu p-0 hidden">
|
||||
<div data-v-42061e3e="" class="add-to w-full max-w-[256px] min-w-[200px] p-3 space-y-3" data-testid="add-to-menu" tabindex="0">
|
||||
<section data-v-42061e3e="" class="existing-playlists">
|
||||
<p data-v-42061e3e="" class="mb-2 text-[0.9rem]">Add 0 items to</p>
|
||||
<ul data-v-42061e3e="" class="relative max-h-48 overflow-y-scroll space-y-1.5">
|
||||
<li data-v-42061e3e="" data-testid="queue" tabindex="0">Queue</li>
|
||||
<li data-v-42061e3e="" class="favorites" data-testid="add-to-favorites" tabindex="0"> Favorites </li>
|
||||
</ul>
|
||||
</section><button data-v-8943c846="" data-v-42061e3e="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer !w-full !border !border-solid !border-white/20" type="button" transparent=""> New Playlist… </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</main>
|
||||
</header>
|
||||
<main data-v-dcfb8776="" class="overflow-scroll flex flex-col b-16 md:b-6 p-6 flex-1 place-content-start"><br data-v-8ea4eaa5="" data-testid="song-list" class="-m-6"></main>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`renders in Plus edition 1`] = `
|
||||
<section data-v-dcfb8776="" data-v-8ea4eaa5="" class="max-h-full min-h-full w-full flex flex-col transform-gpu">
|
||||
<header data-v-5691beb5="" data-v-8ea4eaa5="" class="expanded screen-header min-h-0 md:min-h-full flex items-end flex-shrink-0 relative content-stretch leading-normal p-6 border-b border-b-k-bg-secondary">
|
||||
|
@ -78,3 +118,42 @@ exports[`renders in Plus edition 1`] = `
|
|||
<main data-v-dcfb8776="" class="overflow-scroll flex flex-col b-16 md:b-6 p-6 flex-1 place-content-start"><br data-v-8ea4eaa5="" data-testid="song-list" class="-m-6"></main>
|
||||
</section>
|
||||
`;
|
||||
|
||||
exports[`renders in Plus edition 2`] = `
|
||||
<section data-v-dcfb8776="" data-v-8ea4eaa5="" class="max-h-full min-h-full w-full flex flex-col transform-gpu">
|
||||
<header data-v-5691beb5="" data-v-8ea4eaa5="" class="expanded screen-header min-h-0 md:min-h-full flex items-end flex-shrink-0 relative content-stretch leading-normal p-6 border-b border-b-k-bg-secondary">
|
||||
<aside data-v-5691beb5="" class="thumbnail-wrapper hidden md:block overflow-hidden w-0 rounded-md">
|
||||
<article data-v-55bfc268="" data-v-8ea4eaa5="" data-v-5691beb5-s="" class="single thumbnail-stack aspect-square overflow-hidden grid bg-cover bg-no-repeat" style="background-image: url("undefined/resources/assets/img/covers/default.svg");"><span data-v-55bfc268="" style="" class="block will-change-transform w-full h-full bg-cover bg-no-repeat" data-testid="thumbnail"></span></article>
|
||||
</aside>
|
||||
<main data-v-5691beb5="" class="flex flex-1 gap-5 items-center overflow-hidden">
|
||||
<div data-v-5691beb5="" class="w-full flex-1 overflow-hidden">
|
||||
<h1 data-v-5691beb5="" class="name overflow-hidden whitespace-nowrap text-ellipsis mr-4 font-thin md:font-bold my-0 leading-tight"> All Songs
|
||||
<!--v-if-->
|
||||
</h1><span data-v-5691beb5="" class="meta text-k-text-secondary hidden text-[0.9rem] leading-loose"><span data-v-8ea4eaa5="" data-v-5691beb5-s="">420 songs</span><span data-v-8ea4eaa5="" data-v-5691beb5-s="">10 sec</span></span>
|
||||
</div>
|
||||
<div data-v-8ea4eaa5="" data-v-5691beb5-s="" class="controls w-full min-h-[32px] flex justify-between items-center gap-4">
|
||||
<div data-v-8ea4eaa5="" data-v-5691beb5-s="" class="relative" data-testid="song-list-controls">
|
||||
<div class="flex gap-2 flex-wrap"><span data-v-cf9b67d8="" class="btn-group inline-flex relative flex-nowrap" uppercase=""><button data-v-8943c846="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer btn-shuffle-all" type="button" data-testid="btn-shuffle-all" highlight="" title="Shuffle all. Press Alt/⌥ to change mode."><br data-testid="Icon" icon="[object Object]" fixed-width=""> All </button><!--v-if--><!--v-if--><!--v-if--></span>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<div>
|
||||
<div class="context-menu p-0 hidden">
|
||||
<div data-v-42061e3e="" class="add-to w-full max-w-[256px] min-w-[200px] p-3 space-y-3" data-testid="add-to-menu" tabindex="0">
|
||||
<section data-v-42061e3e="" class="existing-playlists">
|
||||
<p data-v-42061e3e="" class="mb-2 text-[0.9rem]">Add 0 items to</p>
|
||||
<ul data-v-42061e3e="" class="relative max-h-48 overflow-y-scroll space-y-1.5">
|
||||
<li data-v-42061e3e="" data-testid="queue" tabindex="0">Queue</li>
|
||||
<li data-v-42061e3e="" class="favorites" data-testid="add-to-favorites" tabindex="0"> Favorites </li>
|
||||
</ul>
|
||||
</section><button data-v-8943c846="" data-v-42061e3e="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer !w-full !border !border-solid !border-white/20" type="button" transparent=""> New Playlist… </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><label data-v-8ea4eaa5="" data-v-5691beb5-s="" class="text-k-text-secondary inline-flex items-center text-base"><input data-v-8ea4eaa5="" data-v-5691beb5-s="" class="relative align-bottom inline-block w-[32px] h-[20px] bg-gray-400 rounded-full shadow-inner cursor-pointer transition-all duration-200 ease-in-out mr-2 after:h-[16px] after:aspect-square after:absolute after:bg-white after:top-[2px] after:left-[2px] after:rounded-full after:transition-left after:duration-200 after:ease-in-out checked:bg-k-highlight checked:after:left-[14px]" type="checkbox"><span data-v-8ea4eaa5="" data-v-5691beb5-s="">Own songs only</span></label>
|
||||
</div>
|
||||
</main>
|
||||
</header>
|
||||
<main data-v-dcfb8776="" class="overflow-scroll flex flex-col b-16 md:b-6 p-6 flex-1 place-content-start"><br data-v-8ea4eaa5="" data-testid="song-list" class="-m-6"></main>
|
||||
</section>
|
||||
`;
|
||||
|
|
|
@ -3,3 +3,5 @@
|
|||
exports[`displays nothing if fetching fails 1`] = `<div style="background-image: none;" class="pointer-events-none fixed z-[1000] overflow-hidden opacity-10 bg-cover bg-center top-0 left-0 h-full w-full" data-testid="album-art-overlay"></div>`;
|
||||
|
||||
exports[`fetches and displays the album thumbnail 1`] = `<div style="background-image: url(http://test/thumb.jpg);" class="pointer-events-none fixed z-[1000] overflow-hidden opacity-10 bg-cover bg-center top-0 left-0 h-full w-full" data-testid="album-art-overlay"></div>`;
|
||||
|
||||
exports[`fetches and displays the album thumbnail 2`] = `<div style="background-image: url("http://test/thumb.jpg");" class="pointer-events-none fixed z-[1000] overflow-hidden opacity-10 bg-cover bg-center top-0 left-0 h-full w-full" data-testid="album-art-overlay"></div>`;
|
||||
|
|
|
@ -34,7 +34,8 @@ const initialState = {
|
|||
current_song: null,
|
||||
playback_position: 0
|
||||
} as QueueState,
|
||||
supports_batch_downloading: false
|
||||
supports_batch_downloading: false,
|
||||
supports_transcoding: false
|
||||
}
|
||||
|
||||
type CommonStoreState = typeof initialState
|
||||
|
|
|
@ -106,7 +106,7 @@ new class extends UnitTestCase {
|
|||
const songs = factory('song', 3)
|
||||
|
||||
const result: SongUpdateResult = {
|
||||
playables: factory('song', 3),
|
||||
songs: factory('song', 3),
|
||||
albums: factory('album', 2),
|
||||
artists: factory('artist', 2),
|
||||
removed: {
|
||||
|
@ -162,7 +162,7 @@ new class extends UnitTestCase {
|
|||
|
||||
isMobile.any = true
|
||||
preferenceStore.transcode_on_mobile = true
|
||||
expect(songStore.getSourceUrl(song)).toBe('http://test/play/foo/1/128?t=hadouken')
|
||||
expect(songStore.getSourceUrl(song)).toBe('http://test/play/foo/1?t=hadouken')
|
||||
})
|
||||
|
||||
it('gets shareable URL', () => {
|
||||
|
|
|
@ -143,7 +143,7 @@ export const songStore = {
|
|||
|
||||
getSourceUrl: (playable: Playable) => {
|
||||
return isMobile.any && preferenceStore.transcode_on_mobile
|
||||
? `${commonStore.state.cdn_url}play/${playable.id}/1/128?t=${authService.getAudioToken()}`
|
||||
? `${commonStore.state.cdn_url}play/${playable.id}/1?t=${authService.getAudioToken()}`
|
||||
: `${commonStore.state.cdn_url}play/${playable.id}?t=${authService.getAudioToken()}`
|
||||
},
|
||||
|
||||
|
|
1
resources/assets/js/types.d.ts
vendored
1
resources/assets/js/types.d.ts
vendored
|
@ -321,6 +321,7 @@ interface UserPreferences extends Record<string, any> {
|
|||
artists_view_mode: ArtistAlbumViewMode | null,
|
||||
albums_view_mode: ArtistAlbumViewMode | null,
|
||||
transcode_on_mobile: boolean
|
||||
transcode_quality: number
|
||||
support_bar_no_bugging: boolean
|
||||
show_album_art_overlay: boolean
|
||||
lyrics_zoom_level: number | null
|
||||
|
|
|
@ -37,7 +37,7 @@ Route::middleware('web')->group(static function (): void {
|
|||
Route::get('dropbox/authorize', AuthorizeDropboxController::class)->name('dropbox.authorize');
|
||||
|
||||
Route::middleware('audio.auth')->group(static function (): void {
|
||||
Route::get('play/{song}/{transcode?}/{bitrate?}', PlayController::class)->name('song.play');
|
||||
Route::get('play/{song}/{transcode?}', PlayController::class)->name('song.play');
|
||||
|
||||
if (config('koel.download.allow')) {
|
||||
Route::prefix('download')->group(static function (): void {
|
||||
|
|
Loading…
Reference in a new issue