aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.css26
-rw-r--r--src/App.js467
-rw-r--r--src/App.test.js9
-rw-r--r--src/StreamInfo.js62
-rw-r--r--src/index.css13
-rw-r--r--src/index.js12
-rw-r--r--src/serviceWorker.js141
-rw-r--r--src/setupTests.js5
8 files changed, 735 insertions, 0 deletions
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..1b1cca9
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,26 @@
+body {
+ background-color: #373b41;
+}
+
+.dropdownLabel {
+ display: block;
+ margin: 0 0 0.28571429rem 0;
+ color: rgba(0, 0, 0, 0.87);
+ font-size: 0.92857143em;
+ font-weight: 700;
+ text-transform: none;
+}
+
+#historyContainer {
+ min-height: 6em;
+ max-height: 6em;
+ overflow-y: scroll;
+ overflow-wrap: break-word;
+}
+
+#chatContainer {
+ min-height: 25em;
+ max-height: 25em;
+ overflow-y: scroll;
+ overflow-wrap: break-word;
+}
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 0000000..3260705
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,467 @@
+import React, { Component } from "react";
+import { render } from "react-dom";
+import {
+ Container,
+ Header,
+ Icon,
+ Grid,
+ Segment,
+ List,
+ Input,
+ Message,
+ Dropdown
+} from "semantic-ui-react";
+import "fomantic-ui-css/semantic.min.css";
+import { w3cwebsocket as W3CWebSocket } from "websocket";
+import Moment from "react-moment";
+import "moment/locale/es";
+import "./App.css";
+import StreamInfo from "./StreamInfo";
+
+const client = new W3CWebSocket("wss://radio.bienvenidoainternet.org/daemon/");
+const chatColors = [
+ "grey",
+ "red",
+ "pink",
+ "orange",
+ "yellow",
+ "green",
+ "teal",
+ "blue",
+ "purple",
+ "black"
+];
+class App extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ ready: false,
+ response: {},
+ history: [],
+ chat: [],
+ chatMessage: "",
+ infoUpdate: {},
+ currentSong: "",
+ streamsAvailable: [],
+ multipleSources: false,
+ selectedStream: 0,
+ chatUserCount: 0,
+ isChatOnline: false
+ };
+ this.handleOnChange = this.handleOnChange.bind(this);
+ this.sendMessage = this.sendMessage.bind(this);
+ this.streamCount = 0;
+ }
+
+ checkStream(result) {
+ const prevCount = this.streamCount;
+ if (result.source !== undefined) {
+ if (Array.isArray(result)) {
+ this.streamCount = result.length;
+ } else {
+ this.streamCount = 1;
+ }
+ } else {
+ this.streamCount = 0;
+ }
+ return prevCount === this.streamCount;
+ }
+
+ addMessage(message, color, own = false, sender = "") {
+ var ts = new Date();
+ this.setState(
+ prevState => ({
+ chat: [
+ ...prevState.chat,
+ { content: message, color: color, own, timestamp: ts, author: sender }
+ ],
+ chatMessage: ""
+ }),
+ () => {
+ let chatContainer = document.getElementById("chatContainer");
+ if (chatContainer != null) {
+ chatContainer.scrollTop = chatContainer.scrollHeight;
+ }
+ }
+ );
+ }
+
+ log(object) {
+ if (process.env.NODE_ENV === "development") {
+ console.log(object);
+ }
+ }
+
+ refreshPlayer() {
+ let audio = document.getElementById("player");
+ if (audio !== null) {
+ audio.pause();
+ audio.load();
+ audio.play();
+ }
+ }
+
+ addToHistory(song) {
+ this.setState(prevState => ({
+ history: [...prevState.history, song]
+ }));
+ let historyContainer = document.getElementById("historyContainer");
+ if (historyContainer != null) {
+ historyContainer.scrollTop = historyContainer.scrollHeight;
+ }
+ }
+
+ sendMessage() {
+ const { chatMessage } = this.state;
+ client.send("MSG:1:" + chatMessage);
+ this.addMessage(chatMessage, "black", true);
+ this.setState({ chatMessage: "" });
+ }
+
+ componentWillMount() {
+ client.onopen = () => {
+ //console.info("[Chat] Connected to BaiTV Chat!");
+ this.setState({ isChatOnline: true });
+ };
+ client.onmessage = message => {
+ //console.info("[Chat] Message: " + message.data);
+ this.processChatMessage(message.data);
+ };
+ client.onerror = () => {
+ this.setState({ isChatOnline: false });
+ this.addMessage("Ha ocurrido un error conectándose al chat.", "red");
+ //console.error("[Chat] Ha ocurrido un error");
+ };
+ }
+
+ translateColon(msg) {
+ return msg ? msg.replace(/%3A/g, ":") : "";
+ }
+
+ processChatMessage(line) {
+ console.log("<< " + line);
+ const args = line.split(":");
+ switch (args[0]) {
+ case "MSG":
+ this.addMessage(this.translateColon(args[2]), chatColors[args[1]]);
+ break;
+ case "FMSG":
+ this.addMessage(
+ this.translateColon(args[3]),
+ chatColors[args[2]],
+ false,
+ args[1]
+ );
+ break;
+ case "WELCOME":
+ this.addMessage("Conectado al chat, " + args[1], "green");
+ break;
+ case "COUNT":
+ this.setState({ chatUserCount: args[1] });
+ break;
+ case "FLOOD":
+ this.addMessage(
+ "Error: Estas enviando mensajes demasido rápido!",
+ "red"
+ );
+ break;
+ default:
+ console.error("[Chat] Unsupported command " + args[0]);
+ break;
+ }
+ }
+
+ handleOnChange(e, data) {
+ this.setState({ chatMessage: data.value });
+ }
+
+ compareSongs() {
+ const {
+ currentSong,
+ multipleSources,
+ selectedStream,
+ response
+ } = this.state;
+ const currentStream = multipleSources
+ ? response.source[selectedStream]
+ : response.source;
+
+ if (`${currentStream.artist} - ${currentStream.title}` !== currentSong) {
+ this.addToHistory(currentSong);
+ this.setState({
+ currentSong: `${currentStream.artist} - ${currentStream.title}`
+ });
+ }
+ }
+
+ updateInfo() {
+ const {
+ currentSong,
+ multipleSources,
+ selectedStream,
+ response
+ } = this.state;
+ const currentStream = multipleSources
+ ? response.source[selectedStream]
+ : response.source;
+ if (currentSong === "") {
+ this.setState({
+ currentSong: `${currentStream.artist} - ${currentStream.title}`
+ });
+ }
+
+ fetch("https://bienvenidoainternet.org:8443/status-json.xsl")
+ .then(response => response.json())
+ .then(result => {
+ this.log(result);
+ this.checkStream(result);
+ const source = result.icestats.source;
+ if (source !== undefined) {
+ let multipleStreams = Array.isArray(source);
+ if (!multipleStreams && Array.isArray(this.state.response.source)) {
+ this.setState(
+ {
+ response: result.icestats,
+ streamsAvailable: [],
+ multipleSources: multipleStreams,
+ selectedStream: 0
+ },
+ () => this.compareSongs()
+ );
+ this.refreshPlayer();
+ } else {
+ let streams = [];
+ if (Array.isArray(source)) {
+ source.map((stream, index) =>
+ streams.push({
+ key: index,
+ value: index,
+ text: `[${stream.listeners}] ${stream.server_name} - ${stream.server_description}`
+ })
+ );
+ }
+ this.setState(
+ {
+ response: result.icestats,
+ streamsAvailable: streams,
+ multipleSources: multipleStreams
+ },
+ () => this.compareSongs()
+ );
+ }
+ } else {
+ // No hay streams disponibles
+ this.setState(
+ {
+ response: result.icestats,
+ multipleSources: true,
+ streamsAvailable: {},
+ selectedStream: 0
+ },
+ () => this.compareSongs()
+ );
+ }
+ });
+ }
+
+ componentDidMount() {
+ fetch("https://bienvenidoainternet.org:8443/status-json.xsl")
+ .then(response => response.json())
+ .then(result =>
+ this.setState(
+ {
+ response: result.icestats,
+ infoUpdate: setInterval(() => this.updateInfo(), 10000)
+ },
+ () => {
+ const { source } = this.state.response;
+ if (source !== undefined && Array.isArray(source)) {
+ let streams = [];
+ source.map((stream, index) =>
+ streams.push({
+ key: index,
+ value: index,
+ text: `[${stream.listeners}] ${stream.server_name} - ${stream.server_description}`
+ })
+ );
+ console.log(streams);
+ this.setState({
+ multipleSources: true,
+ streamsAvailable: streams,
+ ready: true
+ });
+ } else if (source !== undefined) {
+ this.setState({
+ multipleSources: false,
+ selectedStream: 0,
+ ready: true
+ });
+ }
+ }
+ )
+ );
+ }
+
+ render() {
+ const {
+ ready,
+ response,
+ history,
+ chat,
+ chatMessage,
+ streamsAvailable,
+ multipleSources,
+ selectedStream,
+ chatUserCount,
+ isChatOnline
+ } = this.state;
+ const isOnline = response.source !== undefined;
+
+ if (!ready) {
+ return "Loading";
+ }
+
+ const currentStream = multipleSources
+ ? response.source[selectedStream]
+ : response.source;
+
+ const currentStreamURL = currentStream.listenurl.replace(
+ "http://bienvenidoainternet.org:8000",
+ "https://bienvenidoainternet.org:8443"
+ );
+
+ return (
+ <Container style={{ paddingTop: "50px" }}>
+ <Segment style={{ padding: "2em" }}>
+ <Grid stackable>
+ <Grid.Row columns={2}>
+ <Grid.Column>
+ <Header as="h2">
+ <Icon name="broadcast tower" />
+ <Header.Content>
+ BaiRadio
+ <Header.Subheader>
+ {ready ? response.server_id : "Sin información"}
+ </Header.Subheader>
+ </Header.Content>
+ </Header>
+ </Grid.Column>
+
+ <Grid.Column floated="right">
+ <audio
+ xmlns="http://www.w3.org/1999/xhtml"
+ controls="controls"
+ preload="none"
+ autoPlay
+ id="player"
+ >
+ <source src={currentStreamURL} type="application/ogg" />
+ </audio>
+ </Grid.Column>
+ </Grid.Row>
+
+ <Grid.Row divided columns={2}>
+ <Grid.Column width={6}>
+ {isOnline ? (
+ <>
+ {multipleSources && (
+ <>
+ <label className="dropdownLabel">
+ Transmisiones disponibles:
+ </label>
+ <Dropdown
+ defaultValue={0}
+ fluid
+ selection
+ options={streamsAvailable}
+ onChange={(e, d) =>
+ this.setState({ selectedStream: d.value }, () => {
+ let audio = document.getElementById("player");
+ if (audio !== null) {
+ audio.pause();
+ audio.load();
+ audio.play();
+ }
+ })
+ }
+ />
+ </>
+ )}
+
+ <Header as="h3">Información de la transmisión</Header>
+ <StreamInfo stream={currentStream} />
+ </>
+ ) : (
+ <Message negative>
+ <Message.Header>Oops</Message.Header>
+ Actualmente no hay nadie transmitiendo en Bai Radio
+ </Message>
+ )}
+
+ <Header as="h3">Historial de canciones</Header>
+ <div id="historyContainer">
+ <List>
+ {history.map((song, i) => (
+ <List.Item key={i}>
+ <List.Icon name="music" />
+ <List.Content>{song}</List.Content>
+ </List.Item>
+ ))}
+ </List>
+ </div>
+ </Grid.Column>
+ <Grid.Column width={10}>
+ <Header as="h3">
+ Chat
+ <Header.Subheader>{`Usuarios en el chat: ${chatUserCount}`}</Header.Subheader>
+ </Header>
+ <div id="chatContainer">
+ <List>
+ {chat.map((message, i) => (
+ <List.Item key={i}>
+ [<Moment format="HH:mm:ss" date={message.timestamp} />]{" "}
+ <span className={`ui text ${message.color}`}>
+ {message.content}
+ </span>
+ </List.Item>
+ ))}
+ </List>
+ </div>
+ <Input
+ fluid
+ size="mini"
+ action={{
+ content: "Enviar",
+ onClick: () => {
+ this.sendMessage();
+ },
+ disabled: !isChatOnline
+ }}
+ placeholder=""
+ value={chatMessage}
+ onChange={this.handleOnChange}
+ style={{ marginTop: "0.75em" }}
+ maxLength={128}
+ onKeyUp={e => {
+ let char = e.which || e.keyCode;
+ if (char === 13) {
+ this.sendMessage();
+ }
+ }}
+ disabled={!isChatOnline}
+ />
+ </Grid.Column>
+ </Grid.Row>
+ </Grid>
+ </Segment>
+ <Container textAlign="center">
+ <span className="ui text grey">
+ Bienvenido a Internet 2010 - 2020
+ </span>
+ </Container>
+ </Container>
+ );
+ }
+}
+
+export default App;
diff --git a/src/App.test.js b/src/App.test.js
new file mode 100644
index 0000000..4db7ebc
--- /dev/null
+++ b/src/App.test.js
@@ -0,0 +1,9 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+ const { getByText } = render(<App />);
+ const linkElement = getByText(/learn react/i);
+ expect(linkElement).toBeInTheDocument();
+});
diff --git a/src/StreamInfo.js b/src/StreamInfo.js
new file mode 100644
index 0000000..fd88150
--- /dev/null
+++ b/src/StreamInfo.js
@@ -0,0 +1,62 @@
+import React from "react";
+import { List } from "semantic-ui-react";
+import Moment from "react-moment";
+import "moment/locale/es";
+
+const StreamInfo = props => {
+ const { stream } = props;
+ return (
+ <>
+ <List>
+ <List.Item>
+ <List.Icon name="users" />
+ <List.Content>
+ <List.Header>{`Usuarios escuchando: ${stream.listeners}`}</List.Header>
+ </List.Content>
+ </List.Item>
+ <List.Item>
+ <List.Icon name="clock" />
+ <List.Content>
+ <List.Header>
+ Transmitiendo{" "}
+ <Moment date={stream.stream_start} fromNow locale="es" />
+ </List.Header>
+ </List.Content>
+ </List.Item>
+ <List.Item>
+ <List.Icon name="info" verticalAlign="middle" />
+ <List.Content>
+ <List.Header>{stream.server_name}</List.Header>
+ {stream.server_description}
+ </List.Content>
+ </List.Item>
+ <List.Item>
+ <List.Icon name="file audio outline" verticalAlign="middle" />
+ <List.Content>
+ <List.Header>
+ {stream.audio_channels === 2 ? "Stereo" : "Mono"}
+ </List.Header>
+ {`Bitrate: ${stream["ice-bitrate"]} kbps, ${stream["ice-samplerate"]} Hz`}
+ </List.Content>
+ </List.Item>
+ <List.Item>
+ <List.Icon name="sound" verticalAlign="middle" />
+ <List.Content>
+ <List.Header>Canción actual</List.Header>
+ {`${stream.artist} - ${stream.title}`}
+ </List.Content>
+ </List.Item>
+ </List>
+ <a
+ href={`${stream.listenurl.replace(
+ "http://bienvenidoainternet.org:8000",
+ "https://bienvenidoainternet.org:8443"
+ )}.m3u`}
+ >
+ Descargar archivo m3u
+ </a>
+ </>
+ );
+};
+
+export default StreamInfo;
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..ec2585e
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..87d1be5
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import * as serviceWorker from './serviceWorker';
+
+ReactDOM.render(<App />, document.getElementById('root'));
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: https://bit.ly/CRA-PWA
+serviceWorker.unregister();
diff --git a/src/serviceWorker.js b/src/serviceWorker.js
new file mode 100644
index 0000000..c4838eb
--- /dev/null
+++ b/src/serviceWorker.js
@@ -0,0 +1,141 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ )
+);
+
+export function register(config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA'
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl, config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl, config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' }
+ })
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf('javascript') === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log(
+ 'No internet connection found. App is running in offline mode.'
+ );
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready
+ .then(registration => {
+ registration.unregister();
+ })
+ .catch(error => {
+ console.error(error.message);
+ });
+ }
+}
diff --git a/src/setupTests.js b/src/setupTests.js
new file mode 100644
index 0000000..74b1a27
--- /dev/null
+++ b/src/setupTests.js
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom/extend-expect';