Add On-Demand Streaming/Download Support (#2836)
This branch represents the initial earliest commit of a new option for radio stations that also want to make a portion of their media library available to the public (such as podcast episodes, royalty-free music, etc.) in an on-demand fashion. Note: because of royalty restrictions with most radio stations, this feature is turned OFF by default on all stations, and even when enabled, station administrators must select each individual playlist that will contain on-demand available media.
This commit is contained in:
parent
109b65efcc
commit
bd6d3203b1
|
@ -502,4 +502,16 @@ return [
|
|||
],
|
||||
],
|
||||
],
|
||||
|
||||
'station_on_demand' => [
|
||||
'order' => 10,
|
||||
'require' => ['vue', 'vue-translations', 'bootstrap-vue'],
|
||||
'files' => [
|
||||
'js' => [
|
||||
[
|
||||
'src' => 'dist/station_on_demand.js',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -93,6 +93,18 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
'enable_on_demand' => [
|
||||
'toggle',
|
||||
[
|
||||
'label' => __('Enable On-Demand Streaming and Downloads'),
|
||||
'description' => __('If enabled, music from playlists with on-demand streaming enabled will be available to stream and download via a specialized public page.'),
|
||||
'selected_text' => __('Yes'),
|
||||
'deselected_text' => __('No'),
|
||||
'default' => false,
|
||||
'form_group_class' => 'col-sm-6',
|
||||
],
|
||||
],
|
||||
|
||||
'default_album_art_url' => [
|
||||
'text',
|
||||
[
|
||||
|
|
|
@ -44,6 +44,13 @@ return function (\App\Event\BuildStationMenu $e) {
|
|||
'external' => true,
|
||||
'visible' => $station->getEnablePublicPage(),
|
||||
],
|
||||
'ondemand' => [
|
||||
'label' => __('On-Demand Media'),
|
||||
'icon' => 'cloud_download',
|
||||
'url' => $router->named('public:ondemand', ['station_id' => $station->getShortName()]),
|
||||
'external' => true,
|
||||
'visible' => $station->getEnableOnDemand(),
|
||||
],
|
||||
'files' => [
|
||||
'label' => __('Music Files'),
|
||||
'icon' => 'library_music',
|
||||
|
|
|
@ -156,6 +156,13 @@ return function (App $app) {
|
|||
->setName('api:requests:submit')
|
||||
->add(new Middleware\RateLimit('api', 5, 2));
|
||||
|
||||
$group->get('/ondemand', Controller\Api\Stations\OnDemand\ListAction::class)
|
||||
->setName('api:stations:ondemand:list');
|
||||
|
||||
$group->get('/ondemand/download/{media_id}', Controller\Api\Stations\OnDemand\DownloadAction::class)
|
||||
->setName('api:stations:ondemand:download')
|
||||
->add(new Middleware\RateLimit('ondemand', 1, 2));
|
||||
|
||||
$group->get('/listeners', Controller\Api\Stations\ListenersController::class . ':indexAction')
|
||||
->setName('api:listeners:index')
|
||||
->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
|
||||
|
|
|
@ -9,23 +9,22 @@ return function (App $app) {
|
|||
|
||||
$app->group('/public/{station_id}', function (RouteCollectorProxy $group) {
|
||||
|
||||
$group->get('', Controller\Frontend\PublicController::class . ':indexAction')
|
||||
$group->get('[/{embed:embed}]', Controller\Frontend\PublicPages\PlayerAction::class)
|
||||
->setName('public:index');
|
||||
|
||||
$group->get('/embed', Controller\Frontend\PublicController::class . ':embedAction')
|
||||
->setName('public:embed');
|
||||
|
||||
$group->get('/embed-requests', Controller\Frontend\PublicController::class . ':embedrequestsAction')
|
||||
$group->get('/embed-requests', Controller\Frontend\PublicPages\RequestsAction::class)
|
||||
->setName('public:embedrequests');
|
||||
|
||||
$group->get('/playlist[/{format}]', Controller\Frontend\PublicController::class . ':playlistAction')
|
||||
$group->get('/playlist[/{format}]', Controller\Frontend\PublicPages\PlaylistAction::class)
|
||||
->setName('public:playlist');
|
||||
|
||||
$group->get('/dj', Controller\Frontend\PublicController::class . ':djAction')
|
||||
$group->get('/dj', Controller\Frontend\PublicPages\WebDjAction::class)
|
||||
->setName('public:dj');
|
||||
|
||||
$group->get('/ondemand[/{embed:embed}]', Controller\Frontend\PublicPages\OnDemandAction::class)
|
||||
->setName('public:ondemand');
|
||||
|
||||
})
|
||||
->add(Middleware\GetStation::class)
|
||||
->add(Middleware\EnableView::class);
|
||||
|
||||
};
|
|
@ -269,6 +269,11 @@ var vueProjects = {
|
|||
'src_file': 'vue/StationStreamers.vue',
|
||||
'filename': 'station_streamers.js',
|
||||
'library': 'StationStreamers'
|
||||
},
|
||||
'station_on_demand': {
|
||||
'src_file': 'vue/StationOnDemand.vue',
|
||||
'filename': 'station_on_demand.js',
|
||||
'library': 'StationOnDemand'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +1,42 @@
|
|||
<template>
|
||||
<div class="ml-3 player-inline" v-if="isPlaying">
|
||||
<audio ref="player"/>
|
||||
<div>
|
||||
<audio-player ref="player"></audio-player>
|
||||
|
||||
<div class="inline-seek d-inline-flex align-items-center ml-1" v-if="duration !== 0">
|
||||
<div class="flex-shrink-0 mx-1 text-muted time-display">
|
||||
{{ currentTimeText }}
|
||||
<div class="ml-3 player-inline" v-if="is_playing">
|
||||
<div class="inline-seek d-inline-flex align-items-center ml-1" v-if="duration !== 0">
|
||||
<div class="flex-shrink-0 mx-1 text-muted time-display">
|
||||
{{ currentTimeText }}
|
||||
</div>
|
||||
<div class="flex-fill mx-2">
|
||||
<input type="range" :title="langSeek" class="player-seek-range custom-range" min="0" max="100"
|
||||
step="1" v-model="progress">
|
||||
</div>
|
||||
<div class="flex-shrink-0 mx-1 text-muted time-display">
|
||||
{{ durationText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-fill mx-2">
|
||||
<input type="range" :title="langSeek" class="player-seek-range custom-range" min="0" max="100"
|
||||
step="1" v-model="progress">
|
||||
</div>
|
||||
<div class="flex-shrink-0 mx-1 text-muted time-display">
|
||||
{{ durationText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-sm btn-outline-light px-2 ml-1" href="#" @click.prevent="stop()">
|
||||
<i class="material-icons" aria-hidden="true">pause</i>
|
||||
<span class="sr-only" v-translate>Pause</span>
|
||||
</a>
|
||||
<div class="inline-volume-controls d-inline-flex align-items-center ml-1">
|
||||
<div class="flex-shrink-0">
|
||||
<a class="btn btn-sm btn-outline-light px-2" href="#" @click.prevent="volume = 0">
|
||||
<i class="material-icons" aria-hidden="true">volume_mute</i>
|
||||
<span class="sr-only" v-translate>Mute</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-fill mx-1">
|
||||
<input type="range" :title="langVolume" class="player-volume-range custom-range" min="0" max="100"
|
||||
step="1" v-model="volume">
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<a class="btn btn-sm btn-outline-light px-2" href="#" @click.prevent="volume = 100">
|
||||
<i class="material-icons" aria-hidden="true">volume_up</i>
|
||||
<span class="sr-only" v-translate>Full Volume</span>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-light px-2 ml-1" href="#" @click.prevent="stop()">
|
||||
<i class="material-icons" aria-hidden="true">pause</i>
|
||||
<span class="sr-only" v-translate>Pause</span>
|
||||
</a>
|
||||
<div class="inline-volume-controls d-inline-flex align-items-center ml-1">
|
||||
<div class="flex-shrink-0">
|
||||
<a class="btn btn-sm btn-outline-light px-2" href="#" @click.prevent="volume = 0">
|
||||
<i class="material-icons" aria-hidden="true">volume_mute</i>
|
||||
<span class="sr-only" v-translate>Mute</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-fill mx-1">
|
||||
<input type="range" :title="langVolume" class="player-volume-range custom-range" min="0" max="100"
|
||||
step="1" v-model="volume">
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<a class="btn btn-sm btn-outline-light px-2" href="#" @click.prevent="volume = 100">
|
||||
<i class="material-icons" aria-hidden="true">volume_up</i>
|
||||
<span class="sr-only" v-translate>Full Volume</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,42 +65,17 @@
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import store from 'store';
|
||||
import getLogarithmicVolume from './inc/logarithmic_volume';
|
||||
import AudioPlayer from './components/AudioPlayer';
|
||||
|
||||
export default {
|
||||
components: { AudioPlayer },
|
||||
data () {
|
||||
return {
|
||||
'isPlaying': false,
|
||||
'volume': 55,
|
||||
'audio': null,
|
||||
'duration': 0,
|
||||
'currentTime': 0
|
||||
is_mounted: false
|
||||
};
|
||||
},
|
||||
created () {
|
||||
// Allow pausing from the mobile metadata update.
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
// Check webstorage for existing volume preference.
|
||||
if (store.enabled && store.get('player_volume') !== undefined) {
|
||||
this.volume = store.get('player_volume', this.volume);
|
||||
}
|
||||
|
||||
this.$eventHub.$on('player_toggle', (url) => {
|
||||
if (this.isPlaying && this.audio.src === url) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.stop();
|
||||
Vue.nextTick(() => {
|
||||
this.play(url);
|
||||
});
|
||||
}
|
||||
});
|
||||
mounted () {
|
||||
this.is_mounted = true;
|
||||
},
|
||||
computed: {
|
||||
langSeek () {
|
||||
|
@ -109,87 +86,71 @@
|
|||
},
|
||||
durationText () {
|
||||
let minutes = Math.floor(this.duration / 60),
|
||||
seconds_int = this.duration - minutes * 60,
|
||||
seconds_str = seconds_int.toString(),
|
||||
seconds = seconds_str.substr(0, 2);
|
||||
seconds_int = this.duration - minutes * 60,
|
||||
seconds_str = seconds_int.toString(),
|
||||
seconds = seconds_str.substr(0, 2);
|
||||
|
||||
return minutes + ':' + seconds;
|
||||
},
|
||||
currentTimeText () {
|
||||
let current_minute = parseInt(this.currentTime / 60) % 60,
|
||||
current_seconds_long = this.currentTime % 60,
|
||||
current_seconds = current_seconds_long.toFixed();
|
||||
current_seconds_long = this.currentTime % 60,
|
||||
current_seconds = current_seconds_long.toFixed();
|
||||
|
||||
return (current_minute < 10 ? '0' + current_minute : current_minute) + ':' + (current_seconds < 10 ? '0' + current_seconds : current_seconds);
|
||||
},
|
||||
progress: {
|
||||
get () {
|
||||
return (this.duration !== 0) ? Math.round((this.currentTime / this.duration) * 100, 2) : 0;
|
||||
},
|
||||
set (progress) {
|
||||
if (this.audio !== null) {
|
||||
this.audio.currentTime = (progress / 100) * this.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
volume (volume) {
|
||||
if (this.audio !== null) {
|
||||
this.audio.volume = getLogarithmicVolume(volume);
|
||||
duration () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.enabled) {
|
||||
store.set('player_volume', volume);
|
||||
return this.$refs.player.getDuration();
|
||||
},
|
||||
currentTime () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getCurrentTime();
|
||||
},
|
||||
is_playing () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.isPlaying();
|
||||
},
|
||||
volume: {
|
||||
get () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getVolume();
|
||||
},
|
||||
set (vol) {
|
||||
this.$refs.player.setVolume(vol);
|
||||
}
|
||||
},
|
||||
progress: {
|
||||
get () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getProgress();
|
||||
},
|
||||
set (progress) {
|
||||
this.$refs.player.setProgress(progress);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play (url) {
|
||||
if (this.isPlaying) {
|
||||
this.stop();
|
||||
Vue.nextTick(() => {
|
||||
this.play(url);
|
||||
});
|
||||
}
|
||||
|
||||
this.isPlaying = true;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
this.audio = this.$refs.player;
|
||||
|
||||
this.audio.onended = () => {
|
||||
this.stop();
|
||||
};
|
||||
|
||||
this.audio.ontimeupdate = () => {
|
||||
this.duration = (this.audio.duration !== Infinity && !isNaN(this.audio.duration)) ? this.audio.duration : 0;
|
||||
this.currentTime = this.audio.currentTime;
|
||||
};
|
||||
|
||||
this.audio.volume = getLogarithmicVolume(this.volume);
|
||||
|
||||
this.audio.src = url;
|
||||
|
||||
this.audio.load();
|
||||
this.audio.play();
|
||||
});
|
||||
|
||||
this.$eventHub.$emit('player_playing', url);
|
||||
this.$refs.player.play(url);
|
||||
},
|
||||
stop () {
|
||||
if (!this.isPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$eventHub.$emit('player_stopped', this.audio.src);
|
||||
|
||||
this.audio.pause();
|
||||
this.audio.src = '';
|
||||
|
||||
this.duration = 0;
|
||||
this.currentTime = 0;
|
||||
this.isPlaying = false;
|
||||
this.$refs.player.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<template>
|
||||
<div class="radio-player-widget">
|
||||
<template v-if="is_playing">
|
||||
<audio ref="player" v-bind:title="np.now_playing.song.text"/>
|
||||
</template>
|
||||
<audio-player ref="player" v-bind:title="np.now_playing.song.text"></audio-player>
|
||||
|
||||
<div class="now-playing-details">
|
||||
<div class="now-playing-art" v-if="show_album_art && np.now_playing.song.art">
|
||||
|
@ -216,10 +214,10 @@
|
|||
<script>
|
||||
import axios from 'axios';
|
||||
import NchanSubscriber from 'nchan';
|
||||
import store from 'store';
|
||||
import getLogarithmicVolume from './inc/logarithmic_volume';
|
||||
import AudioPlayer from './components/AudioPlayer';
|
||||
|
||||
export default {
|
||||
components: { AudioPlayer },
|
||||
props: {
|
||||
now_playing_uri: {
|
||||
type: String,
|
||||
|
@ -261,43 +259,22 @@
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
'is_mounted': false,
|
||||
'np': this.initial_now_playing,
|
||||
'np_elapsed': 0,
|
||||
'is_playing': false,
|
||||
'volume': 55,
|
||||
'current_stream': {
|
||||
'name': '',
|
||||
'url': ''
|
||||
},
|
||||
'audio': null,
|
||||
'np_timeout': null,
|
||||
'nchan_subscriber': null,
|
||||
'clock_interval': null
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
this.is_mounted = true;
|
||||
this.clock_interval = setInterval(this.iterateTimer, 1000);
|
||||
|
||||
// Allow pausing from the mobile metadata update.
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
// Check webstorage for existing volume preference.
|
||||
if (store.enabled && store.get('player_volume') !== undefined) {
|
||||
this.volume = store.get('player_volume', this.volume);
|
||||
}
|
||||
|
||||
// Check the query string if browser supports easy query string access.
|
||||
if (typeof URLSearchParams !== 'undefined') {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('volume')) {
|
||||
this.volume = parseInt(urlParams.get('volume'));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert initial NP data from prop to data.
|
||||
this.setNowPlaying(this.np);
|
||||
|
||||
|
@ -368,80 +345,40 @@
|
|||
time_display_total () {
|
||||
let time_total = this.np.now_playing.duration;
|
||||
return (time_total) ? this.formatTime(time_total) : null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
volume (volume) {
|
||||
if (this.audio !== null) {
|
||||
this.audio.volume = getLogarithmicVolume(volume);
|
||||
},
|
||||
is_playing () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.enabled) {
|
||||
store.set('player_volume', volume);
|
||||
return this.$refs.player.isPlaying();
|
||||
},
|
||||
volume: {
|
||||
get () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getVolume();
|
||||
},
|
||||
set (vol) {
|
||||
this.$refs.player.setVolume(vol);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play () {
|
||||
if (this.is_playing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.is_playing = true;
|
||||
|
||||
// Wait for "next tick" to force Vue to recreate the <audio> element.
|
||||
Vue.nextTick(() => {
|
||||
this.audio = this.$refs.player;
|
||||
|
||||
// Handle audio errors.
|
||||
this.audio.onerror = (e) => {
|
||||
if (e.target.error.code === e.target.error.MEDIA_ERR_NETWORK && this.audio.src !== '') {
|
||||
console.log('Network interrupted stream. Automatically reconnecting shortly...');
|
||||
setTimeout(this.play, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
this.audio.onended = () => {
|
||||
if (this.is_playing) {
|
||||
this.stop();
|
||||
|
||||
console.log('Network interrupted stream. Automatically reconnecting shortly...');
|
||||
setTimeout(this.play, 5000);
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
|
||||
this.audio.volume = getLogarithmicVolume(this.volume);
|
||||
|
||||
this.audio.src = this.current_stream.url;
|
||||
this.audio.load();
|
||||
|
||||
this.audio.play();
|
||||
});
|
||||
this.$refs.player.play(this.current_stream.url);
|
||||
},
|
||||
stop () {
|
||||
this.audio.pause();
|
||||
this.audio.src = '';
|
||||
|
||||
this.is_playing = false;
|
||||
this.$refs.player.stop();
|
||||
},
|
||||
toggle () {
|
||||
if (this.is_playing) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
this.$refs.player.toggle(this.current_stream.url);
|
||||
},
|
||||
switchStream (new_stream) {
|
||||
this.current_stream = new_stream;
|
||||
|
||||
// Stop the existing stream, then wait for the next Vue "tick", at which point the <audio> element will
|
||||
// no longer exist, then recreate it via play() command.
|
||||
this.stop();
|
||||
Vue.nextTick(() => {
|
||||
this.play();
|
||||
});
|
||||
this.play();
|
||||
},
|
||||
checkNowPlaying () {
|
||||
if (this.use_nchan) {
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink">
|
||||
<h2 class="card-title py-2">
|
||||
<template v-if="stationName">
|
||||
{{ stationName }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<translate>On-Demand Media</translate>
|
||||
</template>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex-fill text-right">
|
||||
<inline-player ref="player"></inline-player>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive table-responsive-lg">
|
||||
<data-table ref="datatable" class="on-demand-table" paginated select-fields
|
||||
:fields="fields" :api-url="listUrl">
|
||||
<template v-slot:cell(download_url)="row">
|
||||
<a class="file-icon btn-audio" href="#" :data-url="row.item.download_url"
|
||||
@click.prevent="playAudio(row.item.download_url)" :title="langPlayPause">
|
||||
<i class="material-icons" aria-hidden="true" v-if="now_playing_url === row.item.download_url">pause_circle_filled</i>
|
||||
<i class="material-icons" aria-hidden="true" v-else>play_circle_filled</i>
|
||||
</a>
|
||||
|
||||
<a class="name" :href="row.item.download_url" target="_blank" :title="langDownload">
|
||||
<i class="material-icons">cloud_download</i>
|
||||
</a>
|
||||
</template>
|
||||
<template v-slot:cell(media_art)="row">
|
||||
<a :href="row.item.media_art" class="album-art" target="_blank"
|
||||
data-fancybox="gallery">
|
||||
<img class="media_manager_album_art" :alt="langAlbumArt" :src="row.item.media_art">
|
||||
</a>
|
||||
</template>
|
||||
<template v-slot:cell(size)="row">
|
||||
<template v-if="!row.item.size"> </template>
|
||||
<template v-else>
|
||||
{{ formatFileSize(row.item.size) }}
|
||||
</template>
|
||||
</template>
|
||||
</data-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.on-demand-table {
|
||||
table.b-table {
|
||||
thead tr th:nth-child(1),
|
||||
tbody tr td:nth-child(1) {
|
||||
padding-right: 0.75rem;
|
||||
width: 3rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
thead tr th:nth-child(2),
|
||||
tbody tr td:nth-child(2) {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
thead tr th:nth-child(3),
|
||||
tbody tr td:nth-child(3) {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
img.media_manager_album_art {
|
||||
width: 40px;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import InlinePlayer from './InlinePlayer';
|
||||
import DataTable from './components/DataTable';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
components: { DataTable, InlinePlayer },
|
||||
props: {
|
||||
listUrl: String,
|
||||
stationName: String,
|
||||
customFields: Array
|
||||
},
|
||||
data () {
|
||||
let fields = [
|
||||
{ key: 'download_url', label: ' ', sortable: false, selectable: false },
|
||||
{ key: 'media_art', label: this.$gettext('Art'), sortable: false, selectable: false },
|
||||
{ key: 'media_title', label: this.$gettext('Title'), sortable: true, selectable: true },
|
||||
{
|
||||
key: 'media_artist',
|
||||
label: this.$gettext('Artist'),
|
||||
sortable: true,
|
||||
selectable: true
|
||||
},
|
||||
{ key: 'media_album', label: this.$gettext('Album'), sortable: true, selectable: true }
|
||||
];
|
||||
|
||||
_.forEach(this.customFields.slice(), (field) => {
|
||||
fields.push({
|
||||
key: field.display_key,
|
||||
label: field.label,
|
||||
sortable: true,
|
||||
selectable: true,
|
||||
visible: false
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
now_playing_url: '',
|
||||
fields: fields
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$eventHub.$on('player_stopped', () => {
|
||||
this.now_playing_url = '';
|
||||
});
|
||||
|
||||
this.$eventHub.$on('player_playing', (url) => {
|
||||
this.now_playing_url = url;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
langAlbumArt () {
|
||||
return this.$gettext('Album Art');
|
||||
},
|
||||
langPlayPause () {
|
||||
return this.$gettext('Play/Pause');
|
||||
},
|
||||
langDownload () {
|
||||
return this.$gettext('Download');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
playAudio (url) {
|
||||
if (this.now_playing_url === url) {
|
||||
this.$refs.player.stop();
|
||||
} else {
|
||||
this.$refs.player.play(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -76,6 +76,9 @@
|
|||
v-if="row.item.source === 'songs' && row.item.order === 'sequential'">
|
||||
<translate>Sequential</translate>
|
||||
</span>
|
||||
<span class="badge badge-info" v-if="row.item.include_in_on_demand">
|
||||
<translate>On-Demand</translate>
|
||||
</span>
|
||||
<span class="badge badge-success" v-if="row.item.include_in_automation">
|
||||
<translate>Auto-Assigned</translate>
|
||||
</span>
|
||||
|
@ -170,8 +173,8 @@
|
|||
methods: {
|
||||
langToggleButton (record) {
|
||||
return (record.is_enabled)
|
||||
? this.$gettext('Disable')
|
||||
: this.$gettext('Enable');
|
||||
? this.$gettext('Disable')
|
||||
: this.$gettext('Enable');
|
||||
},
|
||||
formatTime (time) {
|
||||
return moment(time).tz(this.stationTimeZone).format('LT');
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<audio ref="audio" v-if="is_playing" v-bind:title="title"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from 'store';
|
||||
import getLogarithmicVolume from '../inc/logarithmic_volume';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
'is_playing': false,
|
||||
'audio': null,
|
||||
'volume': 55,
|
||||
'duration': 0,
|
||||
'currentTime': 0
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
// Allow pausing from the mobile metadata update.
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
// Check webstorage for existing volume preference.
|
||||
if (store.enabled && store.get('player_volume') !== undefined) {
|
||||
this.volume = store.get('player_volume', this.volume);
|
||||
}
|
||||
|
||||
// Check the query string if browser supports easy query string access.
|
||||
if (typeof URLSearchParams !== 'undefined') {
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('volume')) {
|
||||
this.volume = parseInt(urlParams.get('volume'));
|
||||
}
|
||||
}
|
||||
|
||||
this.$eventHub.$on('player_toggle', (url) => {
|
||||
this.toggle(url);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
volume (volume) {
|
||||
if (this.audio !== null) {
|
||||
this.audio.volume = getLogarithmicVolume(volume);
|
||||
}
|
||||
|
||||
if (store.enabled) {
|
||||
store.set('player_volume', volume);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stop () {
|
||||
if (this.audio !== null) {
|
||||
this.$eventHub.$emit('player_stopped', this.audio.src);
|
||||
|
||||
this.audio.pause();
|
||||
this.audio.src = '';
|
||||
}
|
||||
|
||||
this.duration = 0;
|
||||
this.currentTime = 0;
|
||||
|
||||
this.is_playing = false;
|
||||
},
|
||||
play (url) {
|
||||
if (this.is_playing) {
|
||||
this.stop();
|
||||
Vue.nextTick(() => {
|
||||
this.play(url);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.is_playing = true;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
this.audio = this.$refs.audio;
|
||||
|
||||
// Handle audio errors.
|
||||
this.audio.onerror = (e) => {
|
||||
if (e.target.error.code === e.target.error.MEDIA_ERR_NETWORK && this.audio.src !== '') {
|
||||
console.log('Network interrupted stream. Automatically reconnecting shortly...');
|
||||
setTimeout(() => {
|
||||
this.play(url);
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
this.audio.onended = () => {
|
||||
this.stop();
|
||||
};
|
||||
|
||||
this.audio.ontimeupdate = () => {
|
||||
this.duration = (this.audio.duration !== Infinity && !isNaN(this.audio.duration)) ? this.audio.duration : 0;
|
||||
this.currentTime = this.audio.currentTime;
|
||||
};
|
||||
|
||||
this.audio.volume = getLogarithmicVolume(this.volume);
|
||||
|
||||
this.audio.src = url;
|
||||
|
||||
this.audio.load();
|
||||
this.audio.play();
|
||||
});
|
||||
|
||||
this.$eventHub.$emit('player_playing', url);
|
||||
},
|
||||
toggle (url) {
|
||||
if (this.is_playing && this.audio.src === url) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.stop();
|
||||
Vue.nextTick(() => {
|
||||
this.play(url);
|
||||
});
|
||||
}
|
||||
},
|
||||
isPlaying () {
|
||||
return this.is_playing;
|
||||
},
|
||||
getVolume () {
|
||||
return this.volume;
|
||||
},
|
||||
setVolume (vol) {
|
||||
this.volume = vol;
|
||||
},
|
||||
getCurrentTime () {
|
||||
return this.currentTime;
|
||||
},
|
||||
getDuration () {
|
||||
return this.duration;
|
||||
},
|
||||
getProgress () {
|
||||
return (this.duration !== 0) ? Math.round((this.currentTime / this.duration) * 100, 2) : 0;
|
||||
},
|
||||
setProgress (progress) {
|
||||
if (this.audio !== null) {
|
||||
this.audio.currentTime = (progress / 100) * this.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -53,8 +53,8 @@
|
|||
computed: {
|
||||
langTitle () {
|
||||
return this.isEditMode
|
||||
? this.$gettext('Edit Playlist')
|
||||
: this.$gettext('Add Playlist');
|
||||
? this.$gettext('Edit Playlist')
|
||||
: this.$gettext('Add Playlist');
|
||||
},
|
||||
isEditMode () {
|
||||
return this.editUrl !== null;
|
||||
|
@ -64,6 +64,7 @@
|
|||
form: {
|
||||
'name': { required },
|
||||
'is_enabled': { required },
|
||||
'include_in_on_demand': {},
|
||||
'weight': { required },
|
||||
'type': { required },
|
||||
'source': { required },
|
||||
|
@ -94,6 +95,7 @@
|
|||
this.form = {
|
||||
'name': '',
|
||||
'is_enabled': true,
|
||||
'include_in_on_demand': false,
|
||||
'weight': 3,
|
||||
'type': 'default',
|
||||
'source': 'songs',
|
||||
|
@ -130,6 +132,7 @@
|
|||
this.form = {
|
||||
'name': d.name,
|
||||
'is_enabled': d.is_enabled,
|
||||
'include_in_on_demand': d.include_in_on_demand,
|
||||
'weight': d.weight,
|
||||
'type': d.type,
|
||||
'source': d.source,
|
||||
|
@ -161,11 +164,11 @@
|
|||
|
||||
axios({
|
||||
method: (this.isEditMode)
|
||||
? 'PUT'
|
||||
: 'POST',
|
||||
? 'PUT'
|
||||
: 'POST',
|
||||
url: (this.isEditMode)
|
||||
? this.editUrl
|
||||
: this.createUrl,
|
||||
? this.editUrl
|
||||
: this.createUrl,
|
||||
data: this.form
|
||||
}).then((resp) => {
|
||||
let notifyMessage = this.$gettext('Changes saved.');
|
||||
|
|
|
@ -36,6 +36,15 @@
|
|||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group class="col-md-12" label-for="form_edit_include_in_on_demand">
|
||||
<template v-slot:description>
|
||||
<translate>If this station has on-demand streaming and downloading enabled, only songs that are in playlists with this setting enabled will be visible.</translate>
|
||||
</template>
|
||||
<b-form-checkbox id="form_edit_include_in_on_demand" v-model="form.include_in_on_demand.$model">
|
||||
<translate>Include in On-Demand Player</translate>
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group class="col-md-12" label-for="edit_form_type">
|
||||
<template v-slot:label>
|
||||
<translate>Playlist Type</translate>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,14 +5,10 @@ msgstr ""
|
|||
"Generated-By: easygettext\n"
|
||||
"Project-Id-Version: \n"
|
||||
|
||||
#: ./vue/StationPlaylists.vue:137
|
||||
#: ./vue/StationPlaylists.vue:143
|
||||
msgid "# Songs"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:69
|
||||
msgid "Start/end date cannot be used on playlists with advanced settings!"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSource.vue:13
|
||||
msgid "A playlist containing media files hosted on this server."
|
||||
msgstr ""
|
||||
|
@ -30,7 +26,7 @@ msgid "Account List"
|
|||
msgstr ""
|
||||
|
||||
#: ./vue/StationMedia.vue:189
|
||||
#: ./vue/StationPlaylists.vue:134
|
||||
#: ./vue/StationPlaylists.vue:140
|
||||
#: ./vue/StationStreamers.vue:83
|
||||
#: ./vue/station_playlists/PlaylistReorderModal.vue:11
|
||||
#: ./vue/station_streamers/StreamerBroadcastsModal.vue:71
|
||||
|
@ -46,7 +42,7 @@ msgstr ""
|
|||
msgid "Add Playlist"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:112
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:114
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:106
|
||||
msgid "Add Schedule Item"
|
||||
msgstr ""
|
||||
|
@ -57,7 +53,7 @@ msgid "Add Streamer"
|
|||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormAdvanced.vue:50
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:72
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:81
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
|
@ -70,17 +66,19 @@ msgid "Advanced Playback Settings"
|
|||
msgstr ""
|
||||
|
||||
#: ./vue/StationMedia.vue:153
|
||||
#: ./vue/StationOnDemand.vue:77
|
||||
#: ./vue/station_playlists/PlaylistReorderModal.vue:10
|
||||
msgid "Album"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/RadioPlayer.vue:323
|
||||
#: ./vue/RadioPlayer.vue:300
|
||||
#: ./vue/StationMedia.vue:212
|
||||
#: ./vue/StationOnDemand.vue:106
|
||||
#: ./vue/station_media/form/MediaFormAlbumArt.vue:42
|
||||
msgid "Album Art"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:143
|
||||
#: ./vue/StationPlaylists.vue:149
|
||||
msgid "All Playlists"
|
||||
msgstr ""
|
||||
|
||||
|
@ -92,27 +90,32 @@ msgstr ""
|
|||
msgid "Amplify: Amplification (dB)"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/MediaEditModal.vue:172
|
||||
#: ./vue/station_media/MediaEditModal.vue:174
|
||||
#: ./vue/station_media/MediaMoveFilesModal.vue:92
|
||||
#: ./vue/station_media/MediaNewDirectoryModal.vue:76
|
||||
#: ./vue/station_playlists/PlaylistEditModal.vue:179
|
||||
#: ./vue/station_playlists/PlaylistEditModal.vue:182
|
||||
#: ./vue/station_playlists/PlaylistImportModal.vue:68
|
||||
#: ./vue/station_streamers/StreamerEditModal.vue:152
|
||||
msgid "An error occurred and your request could not be completed."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:228
|
||||
#: ./vue/StationPlaylists.vue:234
|
||||
#: ./vue/station_media/MediaToolbar.vue:194
|
||||
msgid "Applying changes..."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationOnDemand.vue:69
|
||||
msgid "Art"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationMedia.vue:148
|
||||
#: ./vue/StationOnDemand.vue:73
|
||||
#: ./vue/station_playlists/PlaylistReorderModal.vue:9
|
||||
#: ./vue/webcaster/settings.vue:115
|
||||
msgid "Artist"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:79
|
||||
#: ./vue/StationPlaylists.vue:82
|
||||
msgid "Auto-Assigned"
|
||||
msgstr ""
|
||||
|
||||
|
@ -124,7 +127,7 @@ msgstr ""
|
|||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:202
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:211
|
||||
#: ./vue/station_streamers/form/StreamerFormBasicInfo.vue:90
|
||||
msgid "Basic Info"
|
||||
msgstr ""
|
||||
|
@ -137,8 +140,8 @@ msgstr ""
|
|||
msgid "Broadcasts"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/MediaEditModal.vue:164
|
||||
#: ./vue/station_playlists/PlaylistEditModal.vue:171
|
||||
#: ./vue/station_media/MediaEditModal.vue:166
|
||||
#: ./vue/station_playlists/PlaylistEditModal.vue:174
|
||||
#: ./vue/station_streamers/StreamerEditModal.vue:144
|
||||
msgid "Changes saved."
|
||||
msgstr ""
|
||||
|
@ -180,7 +183,7 @@ msgstr ""
|
|||
msgid "Cue"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:201
|
||||
#: ./vue/StationPlaylists.vue:207
|
||||
msgid "Custom"
|
||||
msgstr ""
|
||||
|
||||
|
@ -208,12 +211,12 @@ msgstr ""
|
|||
msgid "Custom Fields"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:188
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:197
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:31
|
||||
#: ./vue/StationPlaylists.vue:244
|
||||
#: ./vue/StationPlaylists.vue:250
|
||||
#: ./vue/StationStreamers.vue:43
|
||||
#: ./vue/StationStreamers.vue:121
|
||||
#: ./vue/station_media/MediaToolbar.vue:53
|
||||
|
@ -235,7 +238,7 @@ msgstr ""
|
|||
msgid "Delete broadcast?"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:245
|
||||
#: ./vue/StationPlaylists.vue:251
|
||||
msgid "Delete playlist?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -252,12 +255,12 @@ msgstr ""
|
|||
msgid "Directory Name"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:170
|
||||
#: ./vue/StationPlaylists.vue:176
|
||||
msgid "Disable"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:82
|
||||
#: ./vue/StationPlaylists.vue:181
|
||||
#: ./vue/StationPlaylists.vue:85
|
||||
#: ./vue/StationPlaylists.vue:187
|
||||
#: ./vue/StationStreamers.vue:30
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
@ -274,6 +277,7 @@ msgstr ""
|
|||
msgid "Down"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationOnDemand.vue:112
|
||||
#: ./vue/station_streamers/StreamerBroadcastsModal.vue:8
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
@ -288,7 +292,7 @@ msgstr ""
|
|||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/MediaEditModal.vue:82
|
||||
#: ./vue/station_media/MediaEditModal.vue:83
|
||||
msgid "Edit Media"
|
||||
msgstr ""
|
||||
|
||||
|
@ -300,7 +304,7 @@ msgstr ""
|
|||
msgid "Edit Streamer"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:171
|
||||
#: ./vue/StationPlaylists.vue:177
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
|
@ -316,7 +320,7 @@ msgstr ""
|
|||
msgid "Encoder"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:82
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:84
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:77
|
||||
msgid "End Date"
|
||||
msgstr ""
|
||||
|
@ -351,19 +355,20 @@ msgstr ""
|
|||
msgid "Files removed:"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:137
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:139
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:131
|
||||
msgid "Friday"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/InlinePlayer.vue:35
|
||||
#: ./vue/RadioPlayer.vue:320
|
||||
#: ./vue/InlinePlayer.vue:36
|
||||
#: ./vue/RadioPlayer.vue:297
|
||||
#: ./vue/components/Waveform.vue:38
|
||||
msgid "Full Volume"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:186
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:45
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:87
|
||||
#: ./vue/StationPlaylists.vue:192
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:54
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:96
|
||||
msgid "General Rotation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -371,7 +376,7 @@ msgstr ""
|
|||
msgid "Hide Metadata from Listeners (\"Jingle Mode\")"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:190
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:199
|
||||
msgid "High"
|
||||
msgstr ""
|
||||
|
||||
|
@ -387,7 +392,7 @@ msgstr ""
|
|||
msgid "If any of these options are enabled, this playlist will be managed directly via Liquidsoap instead of via AzuraCast. This can have unintended effects and should only be used when you are comfortable with the results."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:93
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:102
|
||||
msgid "If auto-assignment is enabled, use this playlist as one of the targets for songs to be redistributed into. This will overwrite the existing contents of this playlist."
|
||||
msgstr ""
|
||||
|
||||
|
@ -411,16 +416,24 @@ msgstr ""
|
|||
msgid "If the end time is before the start time, the schedule entry will continue overnight."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:158
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:40
|
||||
msgid "If this station has on-demand streaming and downloading enabled, only songs that are in playlists with this setting enabled will be visible."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:164
|
||||
#: ./vue/station_playlists/PlaylistImportModal.vue:20
|
||||
#: ./vue/station_playlists/PlaylistImportModal.vue:42
|
||||
msgid "Import from PLS/M3U"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:96
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:105
|
||||
msgid "Include in Automated Assignment"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:43
|
||||
msgid "Include in On-Demand Player"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_streamers/form/StreamerFormBasicInfo.vue:50
|
||||
msgid "Internal notes or comments about the user, visible only on this control panel."
|
||||
msgstr ""
|
||||
|
@ -445,11 +458,11 @@ msgstr ""
|
|||
msgid "Jingle Mode"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:77
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:86
|
||||
msgid "Learn about Advanced Playlists"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:98
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:100
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:92
|
||||
msgid "Leave blank to play on every day of the week."
|
||||
msgstr ""
|
||||
|
@ -458,7 +471,7 @@ msgstr ""
|
|||
msgid "Length"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/RadioPlayer.vue:14
|
||||
#: ./vue/RadioPlayer.vue:12
|
||||
#: ./vue/station_streamers/StreamerBroadcastsModal.vue:52
|
||||
msgid "Live"
|
||||
msgstr ""
|
||||
|
@ -467,11 +480,11 @@ msgstr ""
|
|||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:186
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:195
|
||||
msgid "Low"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:75
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:84
|
||||
msgid "Manually define how this playlist is used in Liquidsoap configuration."
|
||||
msgstr ""
|
||||
|
||||
|
@ -491,7 +504,7 @@ msgstr ""
|
|||
msgid "Microphone Source"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:159
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:168
|
||||
msgid "Minute of Hour to Play"
|
||||
msgstr ""
|
||||
|
||||
|
@ -503,12 +516,12 @@ msgstr ""
|
|||
msgid "Modified"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:133
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:135
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:127
|
||||
msgid "Monday"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:149
|
||||
#: ./vue/StationPlaylists.vue:155
|
||||
msgid "More"
|
||||
msgstr ""
|
||||
|
||||
|
@ -528,8 +541,9 @@ msgstr ""
|
|||
msgid "MP3"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/InlinePlayer.vue:25
|
||||
#: ./vue/RadioPlayer.vue:314
|
||||
#: ./vue/InlinePlayer.vue:26
|
||||
#: ./vue/RadioPlayer.vue:291
|
||||
#: ./vue/components/Waveform.vue:28
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
|
||||
|
@ -574,38 +588,46 @@ msgstr ""
|
|||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:135
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:144
|
||||
msgid "Number of Minutes Between Plays"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:111
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:120
|
||||
msgid "Number of Songs Between Plays"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:193
|
||||
#: ./vue/StationPlaylists.vue:79
|
||||
msgid "On-Demand"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationOnDemand.vue:6
|
||||
msgid "On-Demand Media"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:199
|
||||
msgid "Once per %{minutes} Minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:189
|
||||
#: ./vue/StationPlaylists.vue:195
|
||||
msgid "Once per %{songs} Songs"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:65
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:153
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:74
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:162
|
||||
msgid "Once per Hour"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:197
|
||||
#: ./vue/StationPlaylists.vue:203
|
||||
msgid "Once per Hour (at %{minute})"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:58
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:129
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:67
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:138
|
||||
msgid "Once per x Minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:51
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:105
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:60
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:114
|
||||
msgid "Once per x Songs"
|
||||
msgstr ""
|
||||
|
||||
|
@ -621,32 +643,33 @@ msgstr ""
|
|||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/InlinePlayer.vue:19
|
||||
#: ./vue/RadioPlayer.vue:311
|
||||
#: ./vue/InlinePlayer.vue:20
|
||||
#: ./vue/RadioPlayer.vue:288
|
||||
msgid "Pause"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/RadioPlayer.vue:308
|
||||
#: ./vue/RadioPlayer.vue:285
|
||||
msgid "Play"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:61
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:70
|
||||
msgid "Play exactly once every <i>x</i> minutes."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:54
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:63
|
||||
msgid "Play exactly once every <i>x</i> songs."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:68
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:77
|
||||
msgid "Play once per hour at the specified minute."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationMedia.vue:221
|
||||
#: ./vue/StationOnDemand.vue:109
|
||||
msgid "Play/Pause"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:135
|
||||
#: ./vue/StationPlaylists.vue:141
|
||||
msgid "Playlist"
|
||||
msgstr ""
|
||||
|
||||
|
@ -666,7 +689,7 @@ msgstr ""
|
|||
msgid "Playlist order set."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:40
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:49
|
||||
msgid "Playlist Type"
|
||||
msgstr ""
|
||||
|
||||
|
@ -736,7 +759,7 @@ msgstr ""
|
|||
msgid "Rename File/Directory"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:152
|
||||
#: ./vue/StationPlaylists.vue:158
|
||||
msgid "Reorder"
|
||||
msgstr ""
|
||||
|
||||
|
@ -752,7 +775,7 @@ msgstr ""
|
|||
msgid "Replace Album Cover Art"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:155
|
||||
#: ./vue/StationPlaylists.vue:161
|
||||
msgid "Reshuffle"
|
||||
msgstr ""
|
||||
|
||||
|
@ -765,7 +788,7 @@ msgstr ""
|
|||
msgid "Sample Rate"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:138
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:140
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:132
|
||||
msgid "Saturday"
|
||||
msgstr ""
|
||||
|
@ -780,17 +803,17 @@ msgstr ""
|
|||
msgid "Save Changes"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:145
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:147
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:139
|
||||
msgid "Schedule"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:146
|
||||
#: ./vue/StationPlaylists.vue:152
|
||||
#: ./vue/StationStreamers.vue:95
|
||||
msgid "Schedule View"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:95
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:97
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:89
|
||||
msgid "Scheduled Play Days of Week"
|
||||
msgstr ""
|
||||
|
@ -800,7 +823,7 @@ msgstr ""
|
|||
msgid "Scheduled Time #%{num}"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:136
|
||||
#: ./vue/StationPlaylists.vue:142
|
||||
msgid "Scheduling"
|
||||
msgstr ""
|
||||
|
||||
|
@ -816,7 +839,7 @@ msgstr ""
|
|||
msgid "Seconds from the start of the song that the AutoDJ should stop playing."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/InlinePlayer.vue:105
|
||||
#: ./vue/InlinePlayer.vue:82
|
||||
msgid "Seek"
|
||||
msgstr ""
|
||||
|
||||
|
@ -844,19 +867,23 @@ msgstr ""
|
|||
msgid "Sequential"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:25
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:3
|
||||
msgid "Set cue and fade points using the visual editor. The timestamps will be saved to the corresponding fields in the advanced playback settings."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:22
|
||||
msgid "Set Cue In"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:29
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:26
|
||||
msgid "Set Cue Out"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:39
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:36
|
||||
msgid "Set Fade In"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:43
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:40
|
||||
msgid "Set Fade Out"
|
||||
msgstr ""
|
||||
|
||||
|
@ -864,7 +891,7 @@ msgstr ""
|
|||
msgid "Set or clear playlists from the selected media"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:34
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:31
|
||||
msgid "Set Overlap"
|
||||
msgstr ""
|
||||
|
||||
|
@ -915,11 +942,11 @@ msgstr ""
|
|||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:162
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:171
|
||||
msgid "Specify the minute of every hour that this playlist should play."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:47
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:56
|
||||
msgid "Standard playlist, shuffles with other standard playlists based on weight."
|
||||
msgstr ""
|
||||
|
||||
|
@ -938,6 +965,10 @@ msgstr ""
|
|||
msgid "Start Time"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:70
|
||||
msgid "Start/end date cannot be used on playlists with advanced settings!"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:56
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:53
|
||||
msgid "Station Time Zone"
|
||||
|
@ -967,7 +998,7 @@ msgstr ""
|
|||
msgid "Streamer/DJ Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:139
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:141
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:133
|
||||
msgid "Sunday"
|
||||
msgstr ""
|
||||
|
@ -1013,13 +1044,13 @@ msgstr ""
|
|||
#: ./vue/station_media/form/MediaFormBasicInfo.vue:14
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:11
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:25
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:120
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:144
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:168
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:129
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:153
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:177
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:38
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:51
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:76
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:89
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:78
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:91
|
||||
#: ./vue/station_playlists/form/PlaylistFormSource.vue:86
|
||||
#: ./vue/station_playlists/form/PlaylistFormSource.vue:115
|
||||
#: ./vue/station_streamers/form/StreamerFormBasicInfo.vue:14
|
||||
|
@ -1041,11 +1072,11 @@ msgstr ""
|
|||
msgid "This playlist currently has no scheduled times. It will play at all times. To add a new scheduled time, click the button below."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:138
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:147
|
||||
msgid "This playlist will play every $x minutes, where $x is specified below."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:114
|
||||
#: ./vue/station_playlists/form/PlaylistFormBasicInfo.vue:123
|
||||
msgid "This playlist will play every $x songs, where $x is specified below."
|
||||
msgstr ""
|
||||
|
||||
|
@ -1060,12 +1091,13 @@ msgstr ""
|
|||
msgid "This streamer is not scheduled to play at any times."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:136
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:138
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:130
|
||||
msgid "Thursday"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationMedia.vue:145
|
||||
#: ./vue/StationOnDemand.vue:70
|
||||
#: ./vue/station_playlists/PlaylistReorderModal.vue:8
|
||||
#: ./vue/webcaster/settings.vue:108
|
||||
msgid "Title"
|
||||
|
@ -1080,7 +1112,7 @@ msgstr ""
|
|||
msgid "To set this schedule to run only within a certain date range, specify a start and end date."
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:134
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:136
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:128
|
||||
msgid "Tuesday"
|
||||
msgstr ""
|
||||
|
@ -1114,12 +1146,13 @@ msgstr ""
|
|||
msgid "View tracks in playlist"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:65
|
||||
#: ./vue/station_media/form/MediaFormWaveformEditor.vue:61
|
||||
msgid "Visual Cue Editor"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/InlinePlayer.vue:108
|
||||
#: ./vue/RadioPlayer.vue:317
|
||||
#: ./vue/InlinePlayer.vue:85
|
||||
#: ./vue/RadioPlayer.vue:294
|
||||
#: ./vue/components/Waveform.vue:107
|
||||
msgid "Volume"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1127,15 +1160,15 @@ msgstr ""
|
|||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/components/Waveform.vue:11
|
||||
#: ./vue/components/Waveform.vue:15
|
||||
msgid "Waveform Zoom"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:135
|
||||
#: ./vue/station_playlists/form/PlaylistFormSchedule.vue:137
|
||||
#: ./vue/station_streamers/form/StreamerFormSchedule.vue:129
|
||||
msgid "Wednesday"
|
||||
msgstr ""
|
||||
|
||||
#: ./vue/StationPlaylists.vue:186
|
||||
#: ./vue/StationPlaylists.vue:192
|
||||
msgid "Weight"
|
||||
msgstr ""
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
namespace App\Controller\Api\Stations\OnDemand;
|
||||
|
||||
use App\Entity;
|
||||
use App\Flysystem\Filesystem;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class DownloadAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $media_id,
|
||||
Entity\Repository\StationMediaRepository $mediaRepo,
|
||||
Filesystem $filesystem
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
// Verify that the station supports on-demand streaming.
|
||||
if (!$station->getEnableOnDemand()) {
|
||||
return $response->withStatus(403)
|
||||
->withJson(new Entity\Api\Error(403, __('This station does not support on-demand streaming.')));
|
||||
}
|
||||
|
||||
$media = $mediaRepo->findByUniqueId($media_id, $station);
|
||||
|
||||
if (!($media instanceof Entity\StationMedia)) {
|
||||
return $response->withStatus(404)
|
||||
->withJson(new Entity\Api\Error(404, __('File not found.')));
|
||||
}
|
||||
|
||||
$filePath = $media->getPathUri();
|
||||
$fs = $filesystem->getForStation($station);
|
||||
|
||||
set_time_limit(600);
|
||||
return $response->withFlysystemFile($fs, $filePath);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
namespace App\Controller\Api\Stations\OnDemand;
|
||||
|
||||
use App\ApiUtilities;
|
||||
use App\Doctrine\Paginator;
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ListAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
EntityManager $em,
|
||||
ApiUtilities $apiUtils
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
// Verify that the station supports on-demand streaming.
|
||||
if (!$station->getEnableOnDemand()) {
|
||||
return $response->withStatus(403)
|
||||
->withJson(new Entity\Api\Error(403, __('This station does not support on-demand streaming.')));
|
||||
}
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
|
||||
$qb->select('sm, s, spm, sp')
|
||||
->from(Entity\StationMedia::class, 'sm')
|
||||
->join('sm.song', 's')
|
||||
->leftJoin('sm.playlists', 'spm')
|
||||
->leftJoin('spm.playlist', 'sp')
|
||||
->where('sm.station_id = :station_id')
|
||||
->andWhere('sp.id IS NOT NULL')
|
||||
->andWhere('sp.is_enabled = 1')
|
||||
->andWhere('sp.include_in_on_demand = 1')
|
||||
->setParameter('station_id', $station->getId());
|
||||
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
if (!empty($params['sort'])) {
|
||||
$sort_fields = [
|
||||
'song_title' => 'sm.title',
|
||||
'song_artist' => 'sm.artist',
|
||||
'song_album' => 'sm.album',
|
||||
];
|
||||
|
||||
foreach ($params['sort'] as $sort_key => $sort_direction) {
|
||||
if (isset($sort_fields[$sort_key])) {
|
||||
$qb->addOrderBy($sort_fields[$sort_key], $sort_direction);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$qb->orderBy('sm.artist', 'ASC')
|
||||
->addOrderBy('sm.title', 'ASC');
|
||||
}
|
||||
|
||||
$search_phrase = trim($params['searchPhrase']);
|
||||
if (!empty($search_phrase)) {
|
||||
$qb->andWhere('(sm.title LIKE :query OR sm.artist LIKE :query OR sm.album LIKE :query)')
|
||||
->setParameter('query', '%' . $search_phrase . '%');
|
||||
}
|
||||
|
||||
$paginator = new Paginator($qb);
|
||||
$paginator->setFromRequest($request);
|
||||
|
||||
$isBootgrid = $paginator->isFromBootgrid();
|
||||
$router = $request->getRouter();
|
||||
|
||||
$paginator->setPostprocessor(function ($media) use ($station, $isBootgrid, $router, $apiUtils) {
|
||||
/** @var Entity\StationMedia $media */
|
||||
$row = new Entity\Api\StationOnDemand();
|
||||
|
||||
$row->track_id = $media->getUniqueId();
|
||||
$row->media = $media->api($apiUtils);
|
||||
$row->download_url = (string)$router->named('api:stations:ondemand:download', [
|
||||
'station_id' => $station->getId(),
|
||||
'media_id' => $media->getUniqueId(),
|
||||
]);
|
||||
|
||||
$row->resolveUrls($router->getBaseUrl());
|
||||
|
||||
if ($isBootgrid) {
|
||||
return Utilities::flattenArray($row, '_');
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $paginator->write($response);
|
||||
}
|
||||
}
|
|
@ -1,190 +0,0 @@
|
|||
<?php
|
||||
namespace App\Controller\Frontend;
|
||||
|
||||
use App\Entity;
|
||||
use App\Exception\StationNotFoundException;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use App\Radio\Remote\AdapterProxy;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class PublicController
|
||||
{
|
||||
public function indexAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
return $this->_getPublicPage($request, $response, 'frontend/public/index');
|
||||
}
|
||||
|
||||
protected function _getPublicPage(ServerRequest $request, Response $response, $template_name, $template_vars = [])
|
||||
{
|
||||
// Override system-wide iframe refusal
|
||||
$response = $response->withHeader('X-Frame-Options', '*');
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw new StationNotFoundException;
|
||||
}
|
||||
|
||||
$np = [
|
||||
'station' => [
|
||||
'listen_url' => '',
|
||||
'mounts' => [],
|
||||
'remotes' => [],
|
||||
],
|
||||
'now_playing' => [
|
||||
'song' => [
|
||||
'title' => __('Song Title'),
|
||||
'artist' => __('Song Artist'),
|
||||
'art' => '',
|
||||
],
|
||||
'playlist' => '',
|
||||
'is_request' => false,
|
||||
'duration' => 0,
|
||||
],
|
||||
'live' => [
|
||||
'is_live' => false,
|
||||
'streamer_name' => '',
|
||||
],
|
||||
'song_history' => [],
|
||||
];
|
||||
|
||||
$station_np = $station->getNowplaying();
|
||||
if ($station_np instanceof Entity\Api\NowPlaying) {
|
||||
$station_np->resolveUrls($request->getRouter()->getBaseUrl());
|
||||
$np = array_intersect_key($station_np->toArray(), $np) + $np;
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, $template_name, $template_vars + [
|
||||
'station' => $station,
|
||||
'nowplaying' => $np,
|
||||
]);
|
||||
}
|
||||
|
||||
public function embedAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
return $this->_getPublicPage($request, $response, 'frontend/public/embed');
|
||||
}
|
||||
|
||||
public function embedrequestsAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
return $this->_getPublicPage($request, $response, 'frontend/public/embedrequests');
|
||||
}
|
||||
|
||||
public function playlistAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
$station_id,
|
||||
$format = 'pls'
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
$streams = [];
|
||||
$stream_urls = [];
|
||||
|
||||
$fa = $request->getStationFrontend();
|
||||
foreach ($station->getMounts() as $mount) {
|
||||
/** @var Entity\StationMount $mount */
|
||||
if (!$mount->isVisibleOnPublicPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stream_url = $fa->getUrlForMount($station, $mount, null, false);
|
||||
|
||||
$stream_urls[] = $stream_url;
|
||||
$streams[] = [
|
||||
'name' => $station->getName() . ' - ' . $mount->getDisplayName(),
|
||||
'url' => $stream_url,
|
||||
];
|
||||
}
|
||||
|
||||
$remotes = $request->getStationRemotes();
|
||||
foreach ($remotes as $remote_proxy) {
|
||||
/** @var AdapterProxy $remote_proxy */
|
||||
$adapter = $remote_proxy->getAdapter();
|
||||
$remote = $remote_proxy->getRemote();
|
||||
|
||||
$stream_url = $adapter->getPublicUrl($remote);
|
||||
|
||||
$stream_urls[] = $stream_url;
|
||||
$streams[] = [
|
||||
'name' => $station->getName() . ' - ' . $remote->getDisplayName(),
|
||||
'url' => $stream_url,
|
||||
];
|
||||
}
|
||||
|
||||
$format = strtolower($format);
|
||||
switch ($format) {
|
||||
// M3U Playlist Format
|
||||
case 'm3u':
|
||||
$m3u_file = implode("\n", $stream_urls);
|
||||
|
||||
$response->getBody()->write($m3u_file);
|
||||
return $response
|
||||
->withHeader('Content-Type', 'audio/x-mpegurl')
|
||||
->withHeader('Content-Disposition', 'attachment; filename=' . $station->getShortName() . '.m3u');
|
||||
break;
|
||||
|
||||
// PLS Playlist Format
|
||||
case 'pls':
|
||||
default:
|
||||
$output = [
|
||||
'[playlist]',
|
||||
];
|
||||
|
||||
$i = 1;
|
||||
foreach ($streams as $stream) {
|
||||
$output[] = 'File' . $i . '=' . $stream['url'];
|
||||
$output[] = 'Title' . $i . '=' . $stream['name'];
|
||||
$output[] = 'Length' . $i . '=-1';
|
||||
$output[] = '';
|
||||
$i++;
|
||||
}
|
||||
|
||||
$output[] = 'NumberOfEntries=' . count($streams);
|
||||
$output[] = 'Version=2';
|
||||
|
||||
$response->getBody()->write(implode("\n", $output));
|
||||
return $response
|
||||
->withHeader('Content-Type', 'audio/x-scpls')
|
||||
->withHeader('Content-Disposition', 'attachment; filename=' . $station->getShortName() . '.pls');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function djAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
$station_id,
|
||||
$format = 'pls'
|
||||
): ResponseInterface {
|
||||
// Override system-wide iframe refusal
|
||||
$response = $response->withHeader('X-Frame-Options', '*');
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw new StationNotFoundException;
|
||||
}
|
||||
|
||||
if (!$station->getEnableStreamers()) {
|
||||
throw new StationUnsupportedException;
|
||||
}
|
||||
|
||||
$backend = $request->getStationBackend();
|
||||
|
||||
if (!($backend instanceof Liquidsoap)) {
|
||||
throw new StationUnsupportedException;
|
||||
}
|
||||
|
||||
$wss_url = (string)$backend->getWebStreamingUrl($station, $request->getRouter()->getBaseUrl());
|
||||
$wss_url = str_replace('wss://', '', $wss_url);
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'frontend/public/dj', [
|
||||
'station' => $station,
|
||||
'base_uri' => $wss_url,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
namespace App\Controller\Frontend\PublicPages;
|
||||
|
||||
use App\Exception\StationNotFoundException;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class OnDemandAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
EntityManager $em,
|
||||
bool $embed = false
|
||||
): ResponseInterface {
|
||||
// Override system-wide iframe refusal
|
||||
$response = $response->withHeader('X-Frame-Options', '*');
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw new StationNotFoundException;
|
||||
}
|
||||
|
||||
if (!$station->getEnableOnDemand()) {
|
||||
throw new StationUnsupportedException;
|
||||
}
|
||||
|
||||
// Get list of custom fields.
|
||||
$customFieldsRaw = $em->createQuery(/** @lang DQL */ 'SELECT cf.id, cf.short_name, cf.name FROM App\Entity\CustomField cf ORDER BY cf.name ASC')
|
||||
->getArrayResult();
|
||||
|
||||
$customFields = [];
|
||||
foreach ($customFieldsRaw as $row) {
|
||||
$customFields[] = [
|
||||
'display_key' => 'media_custom_' . $row['id'],
|
||||
'key' => $row['short_name'],
|
||||
'label' => $row['name'],
|
||||
];
|
||||
}
|
||||
|
||||
$templateName = ($embed)
|
||||
? 'frontend/public/ondemand_embed'
|
||||
: 'frontend/public/ondemand';
|
||||
|
||||
return $request->getView()->renderToResponse($response, $templateName, [
|
||||
'station' => $station,
|
||||
'custom_fields' => $customFields,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
namespace App\Controller\Frontend\PublicPages;
|
||||
|
||||
use App\Entity;
|
||||
use App\Exception\StationNotFoundException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class PlayerAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
bool $embed = false
|
||||
): ResponseInterface {
|
||||
// Override system-wide iframe refusal
|
||||
$response = $response->withHeader('X-Frame-Options', '*');
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw new StationNotFoundException;
|
||||
}
|
||||
|
||||
$np = [
|
||||
'station' => [
|
||||
'listen_url' => '',
|
||||
'mounts' => [],
|
||||
'remotes' => [],
|
||||
],
|
||||
'now_playing' => [
|
||||
'song' => [
|
||||
'title' => __('Song Title'),
|
||||
'artist' => __('Song Artist'),
|
||||
'art' => '',
|
||||
],
|
||||
'playlist' => '',
|
||||
'is_request' => false,
|
||||
'duration' => 0,
|
||||
],
|
||||
'live' => [
|
||||
'is_live' => false,
|
||||
'streamer_name' => '',
|
||||
],
|
||||
'song_history' => [],
|
||||
];
|
||||
|
||||
$station_np = $station->getNowplaying();
|
||||
if ($station_np instanceof Entity\Api\NowPlaying) {
|
||||
$station_np->resolveUrls($request->getRouter()->getBaseUrl());
|
||||
$np = array_intersect_key($station_np->toArray(), $np) + $np;
|
||||
}
|
||||
|
||||
$templateName = ($embed)
|
||||
? 'frontend/public/embed'
|
||||
: 'frontend/public/index';
|
||||
|
||||
return $request->getView()->renderToResponse($response, $templateName, [
|
||||
'station' => $station,
|
||||
'nowplaying' => $np,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
namespace App\Controller\Frontend\PublicPages;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class PlaylistAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
$station_id,
|
||||
$format = 'pls'
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
$streams = [];
|
||||
$stream_urls = [];
|
||||
|
||||
$fa = $request->getStationFrontend();
|
||||
foreach ($station->getMounts() as $mount) {
|
||||
/** @var Entity\StationMount $mount */
|
||||
if (!$mount->isVisibleOnPublicPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stream_url = $fa->getUrlForMount($station, $mount, null, false);
|
||||
|
||||
$stream_urls[] = $stream_url;
|
||||
$streams[] = [
|
||||
'name' => $station->getName() . ' - ' . $mount->getDisplayName(),
|
||||
'url' => $stream_url,
|
||||
];
|
||||
}
|
||||
|
||||
$remotes = $request->getStationRemotes();
|
||||
foreach ($remotes as $remote_proxy) {
|
||||
$adapter = $remote_proxy->getAdapter();
|
||||
$remote = $remote_proxy->getRemote();
|
||||
|
||||
$stream_url = $adapter->getPublicUrl($remote);
|
||||
|
||||
$stream_urls[] = $stream_url;
|
||||
$streams[] = [
|
||||
'name' => $station->getName() . ' - ' . $remote->getDisplayName(),
|
||||
'url' => $stream_url,
|
||||
];
|
||||
}
|
||||
|
||||
$format = strtolower($format);
|
||||
switch ($format) {
|
||||
// M3U Playlist Format
|
||||
case 'm3u':
|
||||
$m3u_file = implode("\n", $stream_urls);
|
||||
|
||||
$response->getBody()->write($m3u_file);
|
||||
return $response
|
||||
->withHeader('Content-Type', 'audio/x-mpegurl')
|
||||
->withHeader('Content-Disposition', 'attachment; filename=' . $station->getShortName() . '.m3u');
|
||||
break;
|
||||
|
||||
// PLS Playlist Format
|
||||
case 'pls':
|
||||
default:
|
||||
$output = [
|
||||
'[playlist]',
|
||||
];
|
||||
|
||||
$i = 1;
|
||||
foreach ($streams as $stream) {
|
||||
$output[] = 'File' . $i . '=' . $stream['url'];
|
||||
$output[] = 'Title' . $i . '=' . $stream['name'];
|
||||
$output[] = 'Length' . $i . '=-1';
|
||||
$output[] = '';
|
||||
$i++;
|
||||
}
|
||||
|
||||
$output[] = 'NumberOfEntries=' . count($streams);
|
||||
$output[] = 'Version=2';
|
||||
|
||||
$response->getBody()->write(implode("\n", $output));
|
||||
return $response
|
||||
->withHeader('Content-Type', 'audio/x-scpls')
|
||||
->withHeader('Content-Disposition', 'attachment; filename=' . $station->getShortName() . '.pls');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
namespace App\Controller\Frontend\PublicPages;
|
||||
|
||||
use App\Exception\StationNotFoundException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RequestsAction
|
||||
{
|
||||
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
// Override system-wide iframe refusal
|
||||
$response = $response->withHeader('X-Frame-Options', '*');
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw new StationNotFoundException;
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'frontend/public/embedrequests', [
|
||||
'station' => $station,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
namespace App\Controller\Frontend\PublicPages;
|
||||
|
||||
use App\Exception\StationNotFoundException;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Backend\Liquidsoap;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class WebDjAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
$station_id,
|
||||
$format = 'pls'
|
||||
): ResponseInterface {
|
||||
// Override system-wide iframe refusal
|
||||
$response = $response->withHeader('X-Frame-Options', '*');
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!$station->getEnablePublicPage()) {
|
||||
throw new StationNotFoundException;
|
||||
}
|
||||
|
||||
if (!$station->getEnableStreamers()) {
|
||||
throw new StationUnsupportedException;
|
||||
}
|
||||
|
||||
$backend = $request->getStationBackend();
|
||||
|
||||
if (!($backend instanceof Liquidsoap)) {
|
||||
throw new StationUnsupportedException;
|
||||
}
|
||||
|
||||
$wss_url = (string)$backend->getWebStreamingUrl($station, $request->getRouter()->getBaseUrl());
|
||||
$wss_url = str_replace('wss://', '', $wss_url);
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'frontend/public/dj', [
|
||||
'station' => $station,
|
||||
'base_uri' => $wss_url,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
namespace App\Entity\Api;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @OA\Schema(type="object", schema="Api_StationOnDemand")
|
||||
*/
|
||||
class StationOnDemand implements ResolvableUrlInterface
|
||||
{
|
||||
/**
|
||||
* Track ID unique identifier
|
||||
*
|
||||
* @OA\Property(example=1)
|
||||
* @var string
|
||||
*/
|
||||
public string $track_id;
|
||||
|
||||
/**
|
||||
* URL to download/play track.
|
||||
*
|
||||
* @OA\Property(example="/api/station/1/ondemand/download/1")
|
||||
* @var string
|
||||
*/
|
||||
public string $download_url;
|
||||
|
||||
/**
|
||||
* Song
|
||||
*
|
||||
* @OA\Property
|
||||
* @var Song
|
||||
*/
|
||||
public Song $media;
|
||||
|
||||
/**
|
||||
* Re-resolve any Uri instances to reflect base URL changes.
|
||||
*
|
||||
* @param UriInterface $base
|
||||
*/
|
||||
public function resolveUrls(UriInterface $base): void
|
||||
{
|
||||
$this->media->resolveUrls($base);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\Migration;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200514061004 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add "on-demand" options for stations and playlists.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql',
|
||||
'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE station ADD enable_on_demand TINYINT(1) NOT NULL');
|
||||
$this->addSql('ALTER TABLE station_playlists ADD include_in_on_demand TINYINT(1) NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql',
|
||||
'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE station DROP enable_on_demand');
|
||||
$this->addSql('ALTER TABLE station_playlists DROP include_in_on_demand');
|
||||
}
|
||||
}
|
|
@ -255,6 +255,14 @@ class Station
|
|||
*/
|
||||
protected $enable_public_page = true;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="enable_on_demand", type="boolean", nullable=false)
|
||||
*
|
||||
* @OA\Property(example=true)
|
||||
* @var bool Whether this station has a public "on-demand" streaming and download page.
|
||||
*/
|
||||
protected $enable_on_demand = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="needs_restart", type="boolean")
|
||||
*
|
||||
|
@ -844,6 +852,16 @@ class Station
|
|||
$this->enable_public_page = $enable_public_page;
|
||||
}
|
||||
|
||||
public function getEnableOnDemand(): bool
|
||||
{
|
||||
return $this->enable_on_demand;
|
||||
}
|
||||
|
||||
public function setEnableOnDemand(bool $enable_on_demand): void
|
||||
{
|
||||
$this->enable_on_demand = $enable_on_demand;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->is_enabled;
|
||||
|
|
|
@ -205,6 +205,15 @@ class StationPlaylist
|
|||
*/
|
||||
protected $include_in_requests = true;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="include_in_on_demand", type="boolean")
|
||||
*
|
||||
* @OA\Property(example=true)
|
||||
*
|
||||
* @var bool Whether this playlist's media is included in "on demand" download/streaming if enabled.
|
||||
*/
|
||||
protected $include_in_on_demand = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="include_in_automation", type="boolean")
|
||||
*
|
||||
|
@ -428,6 +437,16 @@ class StationPlaylist
|
|||
$this->include_in_requests = $include_in_requests;
|
||||
}
|
||||
|
||||
public function getIncludeInOnDemand(): bool
|
||||
{
|
||||
return $this->include_in_on_demand;
|
||||
}
|
||||
|
||||
public function setIncludeInOnDemand(bool $include_in_on_demand): void
|
||||
{
|
||||
$this->include_in_on_demand = $include_in_on_demand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this playlist can be used as a valid source of requestable media.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
$props = [
|
||||
'listUrl' => $router->fromHere('api:stations:ondemand:list'),
|
||||
'customFields' => $custom_fields,
|
||||
'stationName' => $station->getName(),
|
||||
]
|
||||
?>
|
||||
var station_on_demand;
|
||||
|
||||
$(function () {
|
||||
station_on_demand = new Vue({
|
||||
el: '#station_on_demand',
|
||||
render: function (createElement) {
|
||||
return createElement(StationOnDemand.default, {
|
||||
props: <?=json_encode($props) ?>
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
$this->layout('minimal', [
|
||||
'page_class' => 'ondemand station-' . $station->getShortName(),
|
||||
'title' => 'On-Demand Media - ' . $this->e($station->getName()),
|
||||
'hide_footer' => true,
|
||||
]);
|
||||
|
||||
/** @var \App\Assets $assets */
|
||||
$assets
|
||||
->load('vue')
|
||||
->load('fancybox')
|
||||
->load('station_on_demand')
|
||||
->addInlineJs($this->fetch('frontend/public/ondemand.js', [
|
||||
'station' => $station,
|
||||
'custom_fields' => $custom_fields,
|
||||
]));
|
||||
?>
|
||||
<section id="content" role="main" style="height: 100vh;">
|
||||
<div class="container pt-5 pb-5">
|
||||
<div id="station_on_demand"></div>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
$this->layout('minimal', [
|
||||
'page_class' => 'embed ondemand station-' . $station->getShortName(),
|
||||
'title' => 'On-Demand Media - ' . $this->e($station->getName()),
|
||||
'hide_footer' => true,
|
||||
]);
|
||||
|
||||
/** @var \App\Assets $assets */
|
||||
$assets
|
||||
->load('vue')
|
||||
->load('station_on_demand')
|
||||
->addInlineJs($this->fetch('frontend/public/ondemand.js', [
|
||||
'station' => $station,
|
||||
'custom_fields' => $custom_fields,
|
||||
]));
|
||||
?>
|
||||
|
||||
<div id="station_on_demand"></div>
|
|
@ -14,62 +14,92 @@
|
|||
<?php if ($station->getEnablePublicPage()): ?>
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h3 class="card-title">
|
||||
<?=__('Public Pages') ?>
|
||||
<small class="badge badge-pill badge-success"><?=__('Enabled') ?></small>
|
||||
<?=__('Public Pages')?>
|
||||
<small class="badge badge-pill badge-success"><?=__('Enabled')?></small>
|
||||
</h3>
|
||||
</div>
|
||||
<table class="table table-striped table-responsive-md mb-0">
|
||||
<tr>
|
||||
<td style="width: 30%;"><?=__('Public Page') ?></td>
|
||||
<td style="width: 30%;"><?=__('Public Page')?></td>
|
||||
<td style="width: 70%;">
|
||||
<?=$this->link($router->named('public:index', ['station_id' => $station->getShortName()], [], true)) ?>
|
||||
<?=$this->link($router->named('public:index', ['station_id' => $station->getShortName()], [],
|
||||
true))?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ($backend::supportsStreamers() && $station->getEnableStreamers()): ?>
|
||||
<tr>
|
||||
<td><?=__('Web DJ') ?></td>
|
||||
<td>
|
||||
<?=$this->link($router->named('public:dj', ['station_id' => $station->getShortName()], [], true)) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<td><?=__('Player Embed Code') ?></td>
|
||||
<td><?=__('Player Embed Code')?></td>
|
||||
<td class="form-field">
|
||||
<textarea id="player_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embed', ['station_id' => $station->getShortName()], [], true) ?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 150px; border: 0;"></iframe></textarea>
|
||||
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#player_embed_url"><i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard') ?></button>
|
||||
<textarea id="player_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:index',
|
||||
['station_id' => $station->getShortName(), 'embed' => 'embed'], [],
|
||||
true)?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 150px; border: 0;"></iframe></textarea>
|
||||
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#player_embed_url">
|
||||
<i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard')?></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ($backend::supportsRequests() && $station->getEnableRequests()): ?>
|
||||
<tr>
|
||||
<td><?=__('Request Embed Code') ?></td>
|
||||
<td class="form-field">
|
||||
<textarea id="request_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embedrequests', ['station_id' => $station->getShortName()], [], true) ?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 850px; border: 0;"></iframe></textarea>
|
||||
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#request_embed_url"><i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard') ?></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($backend::supportsStreamers() && $station->getEnableStreamers()): ?>
|
||||
<tr>
|
||||
<td><?=__('Web DJ')?></td>
|
||||
<td>
|
||||
<?=$this->link($router->named('public:dj', ['station_id' => $station->getShortName()], [],
|
||||
true))?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($station->getEnableOnDemand()): ?>
|
||||
<tr>
|
||||
<td><?=__('On-Demand Media')?></td>
|
||||
<td>
|
||||
<?=$this->link($router->named('public:ondemand', ['station_id' => $station->getShortName()], [],
|
||||
true))?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><?=__('On-Demand Embed Code')?></td>
|
||||
<td>
|
||||
<textarea id="ondemand_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:ondemand',
|
||||
['station_id' => $station->getShortName(), 'embed' => 'embed'], [],
|
||||
true)?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 400px; border: 0;"></iframe></textarea>
|
||||
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#ondemand_embed_url">
|
||||
<i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard')?></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($backend::supportsRequests() && $station->getEnableRequests()): ?>
|
||||
<tr>
|
||||
<td><?=__('Request Embed Code')?></td>
|
||||
<td class="form-field">
|
||||
<textarea id="request_embed_url" class="full-width form-control text-preformatted" spellcheck="false" style="height: 70px;"><iframe src="<?=$router->named('public:embedrequests',
|
||||
['station_id' => $station->getShortName()], [],
|
||||
true)?>" frameborder="0" allowtransparency="true" style="width: 100%; min-height: 850px; border: 0;"></iframe></textarea>
|
||||
<button class="btn btn-copy btn-link btn-xs" data-clipboard-target="#request_embed_url">
|
||||
<i class="material-icons sm">file_copy</i> <?=__('Copy to Clipboard')?></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station->getId())): ?>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-danger" data-confirm-title=<?=$this->escapeJs(__('Disable public pages?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'public', 'csrf' => $csrf]) ?>">
|
||||
<a class="btn btn-outline-danger" data-confirm-title=<?=$this->escapeJs(__('Disable public pages?'))?> href="<?=$router->fromHere('stations:profile:toggle',
|
||||
['feature' => 'public', 'csrf' => $csrf])?>">
|
||||
<i class="material-icons" aria-hidden="true">close</i>
|
||||
<?=__('Disable') ?>
|
||||
<?=__('Disable')?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h3 class="card-title">
|
||||
<?=__('Public Pages') ?>
|
||||
<small class="badge badge-pill badge-danger"><?=__('Disabled') ?></small>
|
||||
<?=__('Public Pages')?>
|
||||
<small class="badge badge-pill badge-danger"><?=__('Disabled')?></small>
|
||||
</h3>
|
||||
</div>
|
||||
<?php if ($acl->userAllowed($user, \App\Acl::STATION_PROFILE, $station->getId())): ?>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-success" data-confirm-title=<?=$this->escapeJs(__('Enable public pages?')) ?> href="<?=$router->fromHere('stations:profile:toggle', ['feature' => 'public', 'csrf' => $csrf]) ?>">
|
||||
<a class="btn btn-outline-success" data-confirm-title=<?=$this->escapeJs(__('Enable public pages?'))?> href="<?=$router->fromHere('stations:profile:toggle',
|
||||
['feature' => 'public', 'csrf' => $csrf])?>">
|
||||
<i class="material-icons" aria-hidden="true">check</i>
|
||||
<?=__('Enable') ?>
|
||||
<?=__('Enable')?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"dist/app.js": "dist/app-e71c721caa.js",
|
||||
"dist/bootgrid.js": "dist/bootgrid-dbc21837bc.js",
|
||||
"dist/dark.css": "dist/dark-2eb39cde96.css",
|
||||
"dist/inline_player.js": "dist/inline_player-eed4875c5a.js",
|
||||
"dist/inline_player.js": "dist/inline_player-7df0d22bee.js",
|
||||
"dist/lib/autosize/autosize.min.js": "dist/lib/autosize/autosize-ad0656589d.min.js",
|
||||
"dist/lib/bootgrid/jquery.bootgrid.min.css": "dist/lib/bootgrid/jquery-14a6b139e8.bootgrid.min.css",
|
||||
"dist/lib/bootgrid/jquery.bootgrid.updated.js": "dist/lib/bootgrid/jquery-09b807f309.bootgrid.updated.js",
|
||||
|
@ -49,9 +49,10 @@
|
|||
"dist/lib/zxcvbn/zxcvbn.js": "dist/lib/zxcvbn/zxcvbn-9cf6916dc0.js",
|
||||
"dist/light.css": "dist/light-b6b5ab39b6.css",
|
||||
"dist/material.js": "dist/material-df68dbf23f.js",
|
||||
"dist/radio_player.js": "dist/radio_player-f88abea4e8.js",
|
||||
"dist/radio_player.js": "dist/radio_player-562bd0b7db.js",
|
||||
"dist/station_media.js": "dist/station_media-0d81c6e9d1.js",
|
||||
"dist/station_playlists.js": "dist/station_playlists-c1c36c2497.js",
|
||||
"dist/station_on_demand.js": "dist/station_on_demand-77728edf82.js",
|
||||
"dist/station_playlists.js": "dist/station_playlists-dc79da8967.js",
|
||||
"dist/station_streamers.js": "dist/station_streamers-2e160bdd93.js",
|
||||
"dist/vue_gettext.js": "dist/vue_gettext-2d25ba9686.js",
|
||||
"dist/webcaster.js": "dist/webcaster-09a0e0221a.js",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue