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\Http\Requests\SongPlayRequest;
|
||||||
use App\Models\Song;
|
use App\Models\Song;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Streamer\Streamer;
|
use App\Services\Streamer\Streamer;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
|
|
||||||
class PlayController extends Controller
|
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);
|
$this->authorize('access', $song);
|
||||||
|
|
||||||
return (new Streamer(song: $song, config: [
|
return (new Streamer(song: $song, config: [
|
||||||
'transcode' => (bool) $transcode,
|
'transcode' => (bool) $transcode,
|
||||||
'bit_rate' => $bitRate,
|
'bit_rate' => $user->preferences->transcodeQuality,
|
||||||
'start_time' => (float) $request->time,
|
'start_time' => (float) $request->time,
|
||||||
]))->stream();
|
]))->stream();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
||||||
'show_now_playing_notification' => 'boolean',
|
'show_now_playing_notification' => 'boolean',
|
||||||
'confirm_before_closing' => 'boolean',
|
'confirm_before_closing' => 'boolean',
|
||||||
'transcode_on_mobile' => 'boolean',
|
'transcode_on_mobile' => 'boolean',
|
||||||
|
'transcode_quality' => 'integer',
|
||||||
'show_album_art_overlay' => 'boolean',
|
'show_album_art_overlay' => 'boolean',
|
||||||
'lyrics_zoom_level' => 'integer',
|
'lyrics_zoom_level' => 'integer',
|
||||||
'make_uploads_public' => 'boolean',
|
'make_uploads_public' => 'boolean',
|
||||||
|
@ -29,6 +30,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
||||||
'show_now_playing_notification',
|
'show_now_playing_notification',
|
||||||
'confirm_before_closing',
|
'confirm_before_closing',
|
||||||
'transcode_on_mobile',
|
'transcode_on_mobile',
|
||||||
|
'transcode_quality',
|
||||||
'show_album_art_overlay',
|
'show_album_art_overlay',
|
||||||
'make_uploads_public',
|
'make_uploads_public',
|
||||||
'support_bar_no_bugging',
|
'support_bar_no_bugging',
|
||||||
|
@ -50,6 +52,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
||||||
public bool $showNowPlayingNotification,
|
public bool $showNowPlayingNotification,
|
||||||
public bool $confirmBeforeClosing,
|
public bool $confirmBeforeClosing,
|
||||||
public bool $transcodeOnMobile,
|
public bool $transcodeOnMobile,
|
||||||
|
public int $transcodeQuality,
|
||||||
public bool $showAlbumArtOverlay,
|
public bool $showAlbumArtOverlay,
|
||||||
public bool $makeUploadsPublic,
|
public bool $makeUploadsPublic,
|
||||||
public bool $supportBarNoBugging,
|
public bool $supportBarNoBugging,
|
||||||
|
@ -63,6 +66,10 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
||||||
Assert::oneOf($this->artistsViewMode, ['list', 'thumbnails']);
|
Assert::oneOf($this->artistsViewMode, ['list', 'thumbnails']);
|
||||||
Assert::oneOf($this->albumsViewMode, ['list', 'thumbnails']);
|
Assert::oneOf($this->albumsViewMode, ['list', 'thumbnails']);
|
||||||
Assert::oneOf($this->activeExtraPanelTab, [null, 'Lyrics', 'Artist', 'Album', 'YouTube']);
|
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
|
public static function fromArray(array $data): self
|
||||||
|
@ -77,6 +84,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
||||||
showNowPlayingNotification: $data['show_now_playing_notification'] ?? true,
|
showNowPlayingNotification: $data['show_now_playing_notification'] ?? true,
|
||||||
confirmBeforeClosing: $data['confirm_before_closing'] ?? false,
|
confirmBeforeClosing: $data['confirm_before_closing'] ?? false,
|
||||||
transcodeOnMobile: $data['transcode_on_mobile'] ?? true,
|
transcodeOnMobile: $data['transcode_on_mobile'] ?? true,
|
||||||
|
transcodeQuality: $data['transcode_quality'] ?? 128,
|
||||||
showAlbumArtOverlay: $data['show_album_art_overlay'] ?? true,
|
showAlbumArtOverlay: $data['show_album_art_overlay'] ?? true,
|
||||||
makeUploadsPublic: $data['make_uploads_public'] ?? false,
|
makeUploadsPublic: $data['make_uploads_public'] ?? false,
|
||||||
supportBarNoBugging: $data['support_bar_no_bugging'] ?? false,
|
supportBarNoBugging: $data['support_bar_no_bugging'] ?? false,
|
||||||
|
@ -126,6 +134,7 @@ final class UserPreferences implements Arrayable, JsonSerializable
|
||||||
'confirm_before_closing' => $this->confirmBeforeClosing,
|
'confirm_before_closing' => $this->confirmBeforeClosing,
|
||||||
'show_album_art_overlay' => $this->showAlbumArtOverlay,
|
'show_album_art_overlay' => $this->showAlbumArtOverlay,
|
||||||
'transcode_on_mobile' => $this->transcodeOnMobile,
|
'transcode_on_mobile' => $this->transcodeOnMobile,
|
||||||
|
'transcode_quality' => $this->transcodeQuality,
|
||||||
'make_uploads_public' => $this->makeUploadsPublic,
|
'make_uploads_public' => $this->makeUploadsPublic,
|
||||||
'lastfm_session_key' => $this->lastFmSessionKey,
|
'lastfm_session_key' => $this->lastFmSessionKey,
|
||||||
'support_bar_no_bugging' => $this->supportBarNoBugging,
|
'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 show a notification whenever a new song starts playing
|
||||||
* Whether to confirm before closing Koel’s browser tab
|
* Whether to confirm before closing Koel’s browser tab
|
||||||
* Whether to show a translucent, blurred overlay of the current album’s art
|
* 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 />
|
* 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.
|
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.
|
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.
|
* `false`: Disable transcoding and 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`).
|
* `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.allows_download = true
|
||||||
commonStore.state.uses_i_tunes = true
|
commonStore.state.uses_i_tunes = true
|
||||||
commonStore.state.supports_batch_downloading = true
|
commonStore.state.supports_batch_downloading = true
|
||||||
|
commonStore.state.supports_transcoding = true
|
||||||
cb && cb()
|
cb && cb()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it } from 'vitest'
|
||||||
import { screen } from '@testing-library/vue'
|
import { screen } from '@testing-library/vue'
|
||||||
import isMobile from 'ismobilejs'
|
import isMobile from 'ismobilejs'
|
||||||
|
import { commonStore } from '@/stores'
|
||||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||||
import PreferencesForm from './PreferencesForm.vue'
|
import PreferencesForm from './PreferencesForm.vue'
|
||||||
|
|
||||||
|
@ -9,13 +10,19 @@ new class extends UnitTestCase {
|
||||||
it('has "Transcode on mobile" option for mobile users', () => {
|
it('has "Transcode on mobile" option for mobile users', () => {
|
||||||
isMobile.phone = true
|
isMobile.phone = true
|
||||||
this.render(PreferencesForm)
|
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 () => {
|
it('does not have "Transcode on mobile" option for non-mobile users', async () => {
|
||||||
isMobile.phone = false
|
isMobile.phone = false
|
||||||
this.render(PreferencesForm)
|
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
|
Confirm before closing Koel
|
||||||
</div>
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow v-if="isPhone">
|
<FormRow v-if="showTranscodingOption">
|
||||||
<div>
|
<div>
|
||||||
<CheckBox v-model="preferences.transcode_on_mobile" name="transcode_on_mobile" />
|
<CheckBox
|
||||||
Convert and play media at 128kbps on mobile
|
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>
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
|
@ -41,7 +53,8 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import isMobile from 'ismobilejs'
|
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 { useKoelPlus } from '@/composables'
|
||||||
|
|
||||||
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
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 isPhone = isMobile.phone
|
||||||
const { isPlus } = useKoelPlus()
|
const { isPlus } = useKoelPlus()
|
||||||
|
|
||||||
|
const showTranscodingOption = computed(() => isPhone && commonStore.state.supports_transcoding)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
|
|
@ -40,6 +40,46 @@ exports[`renders 1`] = `
|
||||||
</section>
|
</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`] = `
|
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">
|
<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">
|
<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>
|
<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>
|
</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[`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 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,
|
current_song: null,
|
||||||
playback_position: 0
|
playback_position: 0
|
||||||
} as QueueState,
|
} as QueueState,
|
||||||
supports_batch_downloading: false
|
supports_batch_downloading: false,
|
||||||
|
supports_transcoding: false
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommonStoreState = typeof initialState
|
type CommonStoreState = typeof initialState
|
||||||
|
|
|
@ -106,7 +106,7 @@ new class extends UnitTestCase {
|
||||||
const songs = factory('song', 3)
|
const songs = factory('song', 3)
|
||||||
|
|
||||||
const result: SongUpdateResult = {
|
const result: SongUpdateResult = {
|
||||||
playables: factory('song', 3),
|
songs: factory('song', 3),
|
||||||
albums: factory('album', 2),
|
albums: factory('album', 2),
|
||||||
artists: factory('artist', 2),
|
artists: factory('artist', 2),
|
||||||
removed: {
|
removed: {
|
||||||
|
@ -162,7 +162,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
isMobile.any = true
|
isMobile.any = true
|
||||||
preferenceStore.transcode_on_mobile = 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', () => {
|
it('gets shareable URL', () => {
|
||||||
|
|
|
@ -143,7 +143,7 @@ export const songStore = {
|
||||||
|
|
||||||
getSourceUrl: (playable: Playable) => {
|
getSourceUrl: (playable: Playable) => {
|
||||||
return isMobile.any && preferenceStore.transcode_on_mobile
|
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()}`
|
: `${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,
|
artists_view_mode: ArtistAlbumViewMode | null,
|
||||||
albums_view_mode: ArtistAlbumViewMode | null,
|
albums_view_mode: ArtistAlbumViewMode | null,
|
||||||
transcode_on_mobile: boolean
|
transcode_on_mobile: boolean
|
||||||
|
transcode_quality: number
|
||||||
support_bar_no_bugging: boolean
|
support_bar_no_bugging: boolean
|
||||||
show_album_art_overlay: boolean
|
show_album_art_overlay: boolean
|
||||||
lyrics_zoom_level: number | null
|
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::get('dropbox/authorize', AuthorizeDropboxController::class)->name('dropbox.authorize');
|
||||||
|
|
||||||
Route::middleware('audio.auth')->group(static function (): void {
|
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')) {
|
if (config('koel.download.allow')) {
|
||||||
Route::prefix('download')->group(static function (): void {
|
Route::prefix('download')->group(static function (): void {
|
||||||
|
|
Loading…
Reference in a new issue