StudioClock.svelte 12.5 KB
Newer Older
1
<svelte:options tag="aura-clock"/>
2
<main bind:this={rootElement}></main>
David Trattnig's avatar
David Trattnig committed
3
<script>
4
	import { onMount, afterUpdate } from 'svelte';
David Trattnig's avatar
David Trattnig committed
5
6
7
8
9
10
11
12
13
14

	export let css = globalConfig.CSS;
	export let api =  globalConfig.API_URL;
	export let name = globalConfig.NAME;
	export let logo = globalConfig.LOGO;
	export let logosize = globalConfig.LOGO_SIZE;
	export let nocurrentschedule = globalConfig.NO_CURRENT_SCHEDULE;
	export let nonextschedule = globalConfig.NO_NEXT_SCHEDULE;
	export let unknowntitle = globalConfig.UNKNOWN_TITLE;
	export let playoffset = globalConfig.PLAY_OFFSET;
15
16

	let time = new Date(); 
17
	let queryCurrent = "clock";
18
	let rootElement;
19
20
21
22
	let promise;
	let prevClockData = null;
	let clockData = null;
	let currentTrackElement = null;
23
	let timeLeft;
24
25
26
	let scheduleTimeLeft = 0;
	let reloadTime = 10;
	let reloadWait = 0;
David Trattnig's avatar
David Trattnig committed
27
28
29
30
31
32
33

	// these automatically update when `time`
	// changes, because of the `$:` prefix
	$: hours = time.getHours();
	$: minutes = time.getMinutes();
	$: seconds = time.getSeconds();

34
35
	promise = fetchApi(queryCurrent);

36

37
	/* When component is mounted to the DOM */
David Trattnig's avatar
David Trattnig committed
38
39
40
	onMount(() => {
		const interval = setInterval(() => {
			time = new Date();
41
			timeLeft -= 1;
42
43
44
45
46
47
48
49
50
51
			scheduleTimeLeft -= 1;
		
			/* End of track or end of schedule - load new data */
			if (timeLeft <= 0 || scheduleTimeLeft <= 0) {
				/* For some seconds refresh every second, to work around API timing delays */
				if (timeLeft <= 0 && timeLeft >= -3 || scheduleTimeLeft <= 0 && scheduleTimeLeft >= -3 || reloadWait == 0) {
					promise = fetchApi(queryCurrent);
					reloadWait = reloadTime;
				}
				reloadWait -= 1;				
52
			}
53
						
David Trattnig's avatar
David Trattnig committed
54
55
56
57
		}, 1000);

		return () => {
			clearInterval(interval);
58
		};		
David Trattnig's avatar
David Trattnig committed
59
	});
60

61
62
63
64
65
66
67
68

	/* Called after the component has been updated */
	afterUpdate(async () => {
		scrollToActiveTrack();
	});


	/* Load clock data from the API */
69
	async function fetchApi(query) {
70
		let response;
71
72
		let data;

73
		try {
74
			response = await fetch(api+query);
75
		} catch {
76
			throw new Error(`Cannot connect to Engine API at "${api}"!`);
77
78
		}

79
80
81
82
83
84
		try {
			data = await response.json();
		} catch(e) {
			console.log("Error while converting response to JSON!", e);
			throw new Error(response.statusText);
		}
85
86
87
88

		if (response.ok) {
			return data;
		} else {
89
90
91
92
93
			console.log("Error:", data);
			throw new Error(data.message);
		}
	}

94

95
	/* Initialize the component */
96
	function initComponent(value) {
97
98
99
100
101
102

		/* Load external CSS */
		if (css != null)
			loadExternalCss(rootElement, css);

		/* Set currently loaded data */
103
104
105
106
107
108
109
110
111
		if (value != null) {
			clockData = value;
			console.log("Current Data", value);

			if (value.current_track != null) {
				let t = time - Date.parse(value.current_track.track_start);
				t = parseInt(t/1000);
				timeLeft = value.current_track.track_duration - t - playoffset;
			}
112
			
113
114
115
116
117
118
119
120
			if (value.current_schedule != null) {
				let schedule_end = Date.parse(value.current_schedule.schedule_end);
				schedule_end = parseInt(schedule_end/1000);
				scheduleTimeLeft = schedule_end - time;
			} else {
				/* Decrease time left in any case to avoid reloading too often */
				scheduleTimeLeft -= 1;
			}
121
122
123
124
		}
		return "";
	}

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

	/* Checks if there's an existing, valid schedule */
	function hasValidSchedule(value) {
		if (value.current_schedule != null) {
			if (value.current_schedule.schedule_end != null) {
				let schedule_end = Date.parse(value.current_schedule.schedule_end);
				let diff = schedule_end - time;
				if (diff >= 0)
					return true;
			}
		}
		return false;
	}


	/* Checks if there is an existing valid playlist */
	function hasValidPlaylist(value) {
		if (hasValidSchedule(value))
			if (value.current_playlist != null)
				return true;
		return false;
	}


149
	/* Display the title of a track */
150
151
	function displayTitle(track) {
		if (track != null) {
152
			let artist = "";
153
154
			let title = unknowntitle;
			if (track.track_artist != null && track.track_artist != "")
David Trattnig's avatar
David Trattnig committed
155
				artist = track.track_artist + " - ";
156
157
158
			if (track.track_title != null && track.track_title != "")
				title = track.track_title;
			return artist + title;
159
160
161
162
		}
		return "";
	}

163

164
	/* Format the time */
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
	function formatTime(seconds) {
		if (seconds != null && Number.isInteger(seconds)) {
			let d = new Date(null);
			d.setSeconds(seconds);

			let s;
			if (seconds > 3600)
				s = d.toISOString().substr(11, 8);
			else
				s = d.toISOString().substr(14, 5);
			return s;
		}
		return "";
	}

180

181
	/* Display the name of a show */
David Trattnig's avatar
David Trattnig committed
182
	function displayShowName(schedule) {
183
		let name = ""
David Trattnig's avatar
David Trattnig committed
184
		if (schedule == null || schedule.show_name == null) {
185
			name = '<span class="error">'+nonextschedule+'</span>';
186
		} else {
David Trattnig's avatar
David Trattnig committed
187
			name = schedule.show_name;
188
		}
189
190
191
		return name;
	}

192

193
	/* Display the schedule of a show */
194
195
196
197
198
199
200
201
202
203
204
205
206
	function displayShowSchedule(schedule) {
		let str = "";

		if (schedule != null && schedule.schedule_start != null) {
			let scheduleStart = ""
			let scheduleEnd = "";

			if (schedule.schedule_start != null) {
				let scheduleStart = new Date(Date.parse(schedule.schedule_start));
				scheduleStart = scheduleStart.toLocaleTimeString(navigator.language, {
					hour: '2-digit',
					minute:'2-digit'
				});
207
				str = "" + scheduleStart;
208
209
210
211
212
213
214
			}
			if (schedule.schedule_end != null) {
				scheduleEnd = new Date(Date.parse(schedule.schedule_end));
				scheduleEnd = scheduleEnd.toLocaleTimeString(navigator.language, {
					hour: '2-digit',
					minute:'2-digit'
				});
215
				str = str + " - " + scheduleEnd + "";
216
			} else {
217
				str += "";
218
219
220
221
			}

		}
		return str;
222
	}
223
	
224
	/* Check if the given track is currently playing */
225
	function isActive(entry, currentTrack) {
David Trattnig's avatar
David Trattnig committed
226
		if (currentTrack != null && entry.track_num == currentTrack.track_num) {
227
228
229
230
231
			return true;
		}
		return false;
	}

232

233
	/* Hack to load external CSS into the Web Component */
234
235
236
237
238
239
240
	function loadExternalCss(root, file) {
		let element = document.createElement("link");
		element.setAttribute("rel", "stylesheet");
		element.setAttribute("type", "text/css");
		element.setAttribute("href", file);
		root.appendChild(element);
	}
241
	
242

243
244
245
246
	/* Scrolls to the track currently playing */
	function scrollToActiveTrack() {
		if (currentTrackElement != null)
			currentTrackElement.scrollIntoView();
247
	}
248

David Trattnig's avatar
David Trattnig committed
249
250
251
</script>

<style>
252

David Trattnig's avatar
David Trattnig committed
253
254
	* {
		font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
255
256
	}

257
258
	#studio-clock {
		width: calc(100% - 200px);
259
		height: calc(100% - 500px);
David Trattnig's avatar
David Trattnig committed
260
		margin: 0 50px 50px 50px;
261
262
263
264
265
266
267
268
269
270
271
272
273
		display: -webkit-flex;
		display: -ms-flexbox;
		display: flex;
		flex-direction: row;
	}

	#left-column {
		width: 30%;
		padding: 25px;
	}

	#right-column {
		width: 70%;
David Trattnig's avatar
David Trattnig committed
274
		margin-top: 220px;
275
		padding: 25px 25px 25px 50px;
276
277
	}

David Trattnig's avatar
David Trattnig committed
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
	#station-header {
		width: 100%;
		height: 220px;
		padding: 0 0 130px 0;
	}

	#station-wrapper {
		text-align: center;
	}

	#station-name {
		margin: 0;
    	font-size: 3em;
		line-height: 60px;
		color: #CCC;	
		font-variant: all-small-caps;
	}

	#station-logo {
		align-content: left;
		text-align: right;
		margin: 0 0 0 0;
		opacity: 0.88;
		filter: invert(100%);
	}

304
305
306
307
308
309
310
	#current-schedule {
		border: 2px solid #333;
		margin: 20px 20px 40px 20px;
		background-color: #111;		
		height: 100%;
	}

311
312
313
314
315
	#current-schedule,
	#next-schedule {
		margin: 0 0 40px 20px;
	}

316
317
318
319
320
321
322
	#next-schedule {
		background-color:rgb(24, 24, 24);
		margin-right: 20px;
		padding: 12px;
	}

	#current-schedule .schedule-title {
323
		text-align: center;
324
325
326
327
328
329
330
331
332
		height: 100px;		
	}

	#current-schedule .schedule-title h1 {
		color: #ccc;
		font-size: 2.8em;		
		position: relative;
		top: 30%;
		transform: translateY(-50%);		
333
	}
334

335
336
337
338
339
	#next-schedule .schedule-title {
		color: gray !important;
		font-size: 2em;
	}

340
	#playlist {
341
		height: calc(100% - 155px);
342
343
		overflow-y: auto;
		scroll-behavior: smooth; 
344
		padding: 10px;
345
		display: flex;
346
347
	   	align-items: center;
		border-top: 1px solid #333;;
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
	}

	#playlist::-webkit-scrollbar-track
	{
		border-radius: 10px;
		-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
		background-color: rgb(77, 73, 73);
	}

	#playlist::-webkit-scrollbar
	{
		width: 12px;
		background-color: rgb(0, 0, 0);
	}

	#playlist::-webkit-scrollbar-thumb
	{
		border-radius: 10px;
		-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
		background-color: rgb(34, 32, 32);
368
369
	}

370
371
	#playlist ol {
		margin-left: 33px;
372
		height: 95%;
373
374
	}

375
	.playlist-entry {
David Trattnig's avatar
David Trattnig committed
376
		font-size: 1.9em;
377
		padding-left: 53px;
378
		padding-bottom: 13px;
379
		color: #AAA;
380
381
	}

382
	#current-track * {
David Trattnig's avatar
David Trattnig committed
383
		font-size: 1.5em;
384
385
	}

386
	.track-time-left {
387
		margin: 25px 50px;
388
389
	}

390
	.is-active {
391
392
		color: rgb(43, 241, 36);
		font-weight: bold;
393
394
395
		padding-left: 0;
	}

396
397
	.is-active .track-title::before {
		content: "\00a0\00a0▶\00a0\00a0\00a0";
398
		font-size: larger; 
399
		color: rgb(43, 241, 36);
400
401
	}

402
403
404
405
406
407
408
	.is-active .track-time-left {
		color: rgb(43, 241, 36);
		background-color: #222;
		padding: 5px 15px;
	}

	.error {
David Trattnig's avatar
David Trattnig committed
409
		font-size: 0.8em;
410
411
412
413
414
415
416
		color:red;
		height:100%;
		display : flex;
		align-items : center;
		justify-content: center;
	}

David Trattnig's avatar
David Trattnig committed
417
418
419
420
421
	svg {
		width: 100%;
	}

	.clock-face {
422
423
		stroke: rgb(66, 66, 66);
		fill: black;
David Trattnig's avatar
David Trattnig committed
424
425
426
	}

	.minor {
427
		stroke: rgb(132, 132, 132);
David Trattnig's avatar
David Trattnig committed
428
429
430
431
		stroke-width: 0.5;
	}

	.major {
432
		stroke: rgb(162, 162, 162);
David Trattnig's avatar
David Trattnig committed
433
434
435
436
		stroke-width: 1;
	}

	.hour {
437
		stroke: rgba(255, 255, 255, 0.705);
David Trattnig's avatar
David Trattnig committed
438
439
440
	}

	.minute {
441
		stroke: rgba(255, 255, 255, 0.705);
David Trattnig's avatar
David Trattnig committed
442
443
444
445
446
447
	}

	.second, .second-counterweight {
		stroke: rgb(180,0,0);
	}

David Trattnig's avatar
David Trattnig committed
448
449
450
451
452
	footer {
		width: 100%;
		text-align: center;
		font-size: 0.8em;
		color: gray;
453
		opacity: 0.5;
David Trattnig's avatar
David Trattnig committed
454
	}
455
456
457
458
459
460
461

	footer a {
		color: gray;
		text-decoration: underline;
	}


David Trattnig's avatar
David Trattnig committed
462
463
</style>

464
465


466
467
<div id="studio-clock">
	<div id="left-column" class="column">
David Trattnig's avatar
David Trattnig committed
468
469
470
471
472
473
474
475

		<div id="station-header">
			<div id="station-wrapper">
				<img id="station-logo" src="{logo}" style="width:{logosize}" alt="Radio Station" align="center" />
				<h1 id="station-name">{name}</h1>
			</div>		
		</div>

476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
		<svg viewBox='-50 -50 100 100'>
			<circle class='clock-face' r='48'/>

			<!-- markers -->
			{#each [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] as minute}
				<line
					class='major'
					y1='35'
					y2='45'
					transform='rotate({30 * minute})'
				/>

				{#each [1, 2, 3, 4] as offset}
					<line
						class='minor'
						y1='42'
						y2='45'
						transform='rotate({6 * (minute + offset)})'
					/>
				{/each}
			{/each}

			<!-- hour hand -->
			<line
				class='hour'
				y1='2'
				y2='-20'
				transform='rotate({30 * hours + minutes / 2})'
			/>
David Trattnig's avatar
David Trattnig committed
505

506
			<!-- minute hand -->
David Trattnig's avatar
David Trattnig committed
507
			<line
508
509
510
511
				class='minute'
				y1='4'
				y2='-30'
				transform='rotate({6 * minutes + seconds / 10})'
David Trattnig's avatar
David Trattnig committed
512
			/>
513
514
515
516
517
518
519
520
521
522
523

			<!-- second hand -->
			<g transform='rotate({6 * seconds})'>
				<line class='second' y1='10' y2='-38'/>
				<line class='second-counterweight' y1='10' y2='2'/>
			</g>
		</svg>
	</div>

	<div id="right-column" class="column">

524
		{#await promise}
525
526
527
528
			<div class="spinner-border mt-5" role="status">
				<span class="sr-only">Loading...</span>
			</div>
		{:then value}
529
			{initComponent(value)}
530
531
						

532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
				<div id="current-schedule">
					<div class="schedule-title">
						<h1>
						{#if hasValidSchedule(value)}						
								{@html displayShowName(value.current_schedule)} {displayShowSchedule(value.current_schedule)}
							{:else}
								<span class="error">{nocurrentschedule}</span>
							{/if}						
						</h1>
					</div>
					<div id="playlist">

						{#if hasValidPlaylist(value)}
							<ol>
							{#each value.current_playlist.entries as entry, index}
								{#if isActive(entry, value.current_track)}
								
								<li id="current-playlist-entry" class="playlist-entry is-active" bind:this={currentTrackElement}>
									<!-- <span class="play-icon">&#9654;</span> -->
									<span class="track-title">{displayTitle(entry)}</span>
									<span class="track-time-left">({formatTime(timeLeft)})</span>
								</li>

								{:else}

								<li class="playlist-entry">
									<span class="track-title">{displayTitle(entry)}</span>
									<span class="track-duration">({formatTime(entry.track_duration)})</span>
								</li>	

								{/if}

							{/each}
							</ol>
566
						{:else}
567
568
569
570
571
572
573
574
							{#if value.current_track}
								<div id="current-track" class="is-active">
									<h2>
										<span class="track-title">{displayTitle(value.track)}</span>
										<span class="track-time-left">{formatTime(timeLeft)}</span>
									</h2>
								</div>
							{/if}								
575
						{/if}
576
577
578
					</div>

				</div> 
579
580


581

582
583
584
585
586
			{#if value.current_schedule}
				<div id="next-schedule">
					<h3 class="schedule-title">{@html displayShowName(value.next_schedule)} {displayShowSchedule(value)}</h3>
				</div>		
			{/if}				
587
		{:catch error}
588
			<div class="error"><p>{error}</p></div>
589
		{/await}
590

591
592
593
594

		<footer>
			<a href="https://gitlab.servus.at/aura/engine-clock">Engine Clock</a> is fuelled by <a href="https://gitlab.servus.at/aura/engine">AURA Engine</a>
		</footer>
595
596
	</div>

David Trattnig's avatar
David Trattnig committed
597
598
</div>