StudioClock.svelte 13.3 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

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

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

35
36
	promise = fetchApi(queryCurrent);

37

38
	/* When component is mounted to the DOM */
David Trattnig's avatar
David Trattnig committed
39
40
41
	onMount(() => {
		const interval = setInterval(() => {
			time = new Date();
42
			timeLeft -= 1;
43
44
45
46
47
48
49
50
51
52
			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;				
53
			}
54
						
David Trattnig's avatar
David Trattnig committed
55
56
57
58
		}, 1000);

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

62
63
64
65
66
67
68
69

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


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

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

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

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

95

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

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

		/* Set currently loaded data */
104
105
106
107
108
109
110
111
112
		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;
			}
113
			
114
115
116
117
118
119
120
121
			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;
			}
122
123
124
125
		}
		return "";
	}

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

	/* 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;
	}


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

David Trattnig's avatar
David Trattnig committed
166
167
168
169
170
171
172
173
174
175
176
	/* Display the type of a track */
	function displayType(track) {
		let tracktype = "";
		if (track != null) {
			if (track.track_type == 2)	
				tracktype = "STREAM";
			else if (track.track_type == 3) 
				tracktype = "LIVE STUDIO";
		}
		return tracktype;
	}
177

178
	/* Format the time */
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
	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 "";
	}

194

195
	/* Display the name of a show */
David Trattnig's avatar
David Trattnig committed
196
	function displayShowName(schedule) {
197
		let name = ""
David Trattnig's avatar
David Trattnig committed
198
		if (schedule == null || schedule.show_name == null) {
199
			name = '<span class="error">'+nonextschedule+'</span>';
200
		} else {
David Trattnig's avatar
David Trattnig committed
201
			name = schedule.show_name;
202
		}
203
204
205
		return name;
	}

206

207
	/* Display the schedule of a show */
208
209
210
211
212
213
214
215
216
217
218
219
220
	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'
				});
221
				str = "" + scheduleStart;
222
223
224
225
226
227
228
			}
			if (schedule.schedule_end != null) {
				scheduleEnd = new Date(Date.parse(schedule.schedule_end));
				scheduleEnd = scheduleEnd.toLocaleTimeString(navigator.language, {
					hour: '2-digit',
					minute:'2-digit'
				});
229
				str = str + " - " + scheduleEnd + "";
230
			} else {
231
				str += "";
232
233
234
235
			}

		}
		return str;
236
	}
237
	
238
	/* Check if the given track is currently playing */
239
	function isActive(entry, currentTrack) {
David Trattnig's avatar
David Trattnig committed
240
		if (currentTrack != null && entry.track_num == currentTrack.track_num) {
241
242
243
244
245
			return true;
		}
		return false;
	}

246

247
	/* Hack to load external CSS into the Web Component */
248
249
250
251
252
253
254
	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);
	}
255
	
256

257
258
259
260
	/* Scrolls to the track currently playing */
	function scrollToActiveTrack() {
		if (currentTrackElement != null)
			currentTrackElement.scrollIntoView();
261
	}
262

David Trattnig's avatar
David Trattnig committed
263
264
265
</script>

<style>
266

David Trattnig's avatar
David Trattnig committed
267
268
	* {
		font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
269
270
	}

271
272
	#studio-clock {
		width: calc(100% - 200px);
273
		height: calc(100% - 500px);
David Trattnig's avatar
David Trattnig committed
274
		margin: 0 50px 50px 50px;
275
276
277
278
279
280
281
282
283
284
285
286
287
		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
288
		margin-top: 220px;
289
		padding: 25px 25px 25px 50px;
290
291
	}

David Trattnig's avatar
David Trattnig committed
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
	#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%);
	}

318
319
320
321
322
323
324
	#current-schedule {
		border: 2px solid #333;
		margin: 20px 20px 40px 20px;
		background-color: #111;		
		height: 100%;
	}

325
326
327
328
329
	#current-schedule,
	#next-schedule {
		margin: 0 0 40px 20px;
	}

330
331
332
333
	#next-schedule {
		background-color:rgb(24, 24, 24);
		margin-right: 20px;
		padding: 12px;
David Trattnig's avatar
David Trattnig committed
334
		text-align: center;		
335
336
337
	}

	#current-schedule .schedule-title {
338
		text-align: center;
339
340
341
342
343
344
345
346
347
		height: 100px;		
	}

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

350
351
352
353
354
	#next-schedule .schedule-title {
		color: gray !important;
		font-size: 2em;
	}

355
	#playlist {
356
		height: calc(100% - 155px);
357
358
		overflow-y: auto;
		scroll-behavior: smooth; 
359
		padding: 10px;
360
		display: flex;
361
362
	   	align-items: center;
		border-top: 1px solid #333;;
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
	}

	#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);
383
384
	}

385
386
	#playlist ol {
		margin-left: 33px;
387
		height: 95%;
388
389
	}

390
	.playlist-entry {
David Trattnig's avatar
David Trattnig committed
391
		font-size: 1.9em;
392
		padding-left: 53px;
David Trattnig's avatar
David Trattnig committed
393
		padding-bottom: 23px;
394
		color: #AAA;
395
396
	}

397
	#current-track * {
David Trattnig's avatar
David Trattnig committed
398
		font-size: 1.5em;
399
400
	}

David Trattnig's avatar
David Trattnig committed
401
402
403
404
405
	.track-duration {
		border-radius: 25px;		
		padding: 5px 33px;		  
		border: 1px solid gray;
	}
406
	.track-time-left {
407
		margin: 25px 50px;
David Trattnig's avatar
David Trattnig committed
408
409
410
		border-radius: 25px;		
		padding: 5px 33px;		  
		border: 1px solid gray;
411
412
	}

413
	.is-active {
414
415
		color: rgb(43, 241, 36);
		font-weight: bold;
416
417
418
		padding-left: 0;
	}

David Trattnig's avatar
David Trattnig committed
419
420
421
422
423
424
425
426
427
428
429
	.track-type {
		border-radius: 25px;
  		background: #73AD21;
		color: white;
		padding: 5px 33px;
	}

	.track-type:empty {
		display: none;
	}

430
431
	.is-active .track-title::before {
		content: "\00a0\00a0▶\00a0\00a0\00a0";
432
		font-size: larger; 
433
		color: rgb(43, 241, 36);
434
435
	}

436
437
438
439
440
441
442
	.is-active .track-time-left {
		color: rgb(43, 241, 36);
		background-color: #222;
		padding: 5px 15px;
	}

	.error {
David Trattnig's avatar
David Trattnig committed
443
		font-size: 0.8em;
444
445
446
447
448
449
450
		color:red;
		height:100%;
		display : flex;
		align-items : center;
		justify-content: center;
	}

David Trattnig's avatar
David Trattnig committed
451
452
453
454
455
	svg {
		width: 100%;
	}

	.clock-face {
456
457
		stroke: rgb(66, 66, 66);
		fill: black;
David Trattnig's avatar
David Trattnig committed
458
459
460
	}

	.minor {
461
		stroke: rgb(132, 132, 132);
David Trattnig's avatar
David Trattnig committed
462
463
464
465
		stroke-width: 0.5;
	}

	.major {
466
		stroke: rgb(162, 162, 162);
David Trattnig's avatar
David Trattnig committed
467
468
469
470
		stroke-width: 1;
	}

	.hour {
471
		stroke: rgba(255, 255, 255, 0.705);
David Trattnig's avatar
David Trattnig committed
472
473
474
	}

	.minute {
475
		stroke: rgba(255, 255, 255, 0.705);
David Trattnig's avatar
David Trattnig committed
476
477
478
479
480
481
	}

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

David Trattnig's avatar
David Trattnig committed
482
483
484
485
486
	footer {
		width: 100%;
		text-align: center;
		font-size: 0.8em;
		color: gray;
487
		opacity: 0.5;
David Trattnig's avatar
David Trattnig committed
488
	}
489
490
491
492
493
494
495

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


David Trattnig's avatar
David Trattnig committed
496
497
</style>

498
499


500
501
<div id="studio-clock">
	<div id="left-column" class="column">
David Trattnig's avatar
David Trattnig committed
502
503
504
505
506
507
508
509

		<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>

510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
		<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
539

540
			<!-- minute hand -->
David Trattnig's avatar
David Trattnig committed
541
			<line
542
543
544
545
				class='minute'
				y1='4'
				y2='-30'
				transform='rotate({6 * minutes + seconds / 10})'
David Trattnig's avatar
David Trattnig committed
546
			/>
547
548
549
550
551
552
553
554
555
556
557

			<!-- 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">

558
		{#await promise}
559
560
561
562
			<div class="spinner-border mt-5" role="status">
				<span class="sr-only">Loading...</span>
			</div>
		{:then value}
563
			{initComponent(value)}
564
565
						

566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
				<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>
David Trattnig's avatar
David Trattnig committed
586
587
									<span class="track-type">{displayType(entry)}</span>
									<span class="track-time-left">{formatTime(timeLeft)}</span>
588
589
590
591
592
593
								</li>

								{:else}

								<li class="playlist-entry">
									<span class="track-title">{displayTitle(entry)}</span>
David Trattnig's avatar
David Trattnig committed
594
595
									<span class="track-type">{displayType(entry)}</span>
									<span class="track-duration">{formatTime(entry.track_duration)}</span>
596
597
598
599
600
601
								</li>	

								{/if}

							{/each}
							</ol>
602
						{:else}
603
604
605
							{#if value.current_track}
								<div id="current-track" class="is-active">
									<h2>
David Trattnig's avatar
David Trattnig committed
606
607
										<span class="track-title">{displayTitle(value.current_track)}</span>
										<span class="track-type">{displayType(value.current_track)}</span>
608
609
610
611
										<span class="track-time-left">{formatTime(timeLeft)}</span>
									</h2>
								</div>
							{/if}								
612
						{/if}
613
614
615
					</div>

				</div> 
616
617


618

619
620
621
622
623
			{#if value.current_schedule}
				<div id="next-schedule">
					<h3 class="schedule-title">{@html displayShowName(value.next_schedule)} {displayShowSchedule(value)}</h3>
				</div>		
			{/if}				
624
		{:catch error}
625
			<div class="error"><p>{error}</p></div>
626
		{/await}
627

628
629

		<footer>
David Trattnig's avatar
David Trattnig committed
630
			<a href="https://gitlab.servus.at/aura/engine-clock">Engine Clock v{version}</a> is fuelled by <a href="https://gitlab.servus.at/aura/engine">AURA Engine</a>
631
		</footer>
632
633
	</div>

David Trattnig's avatar
David Trattnig committed
634
635
</div>