diff --git a/Fonts/Futura.ttf b/Fonts/Futura.ttf new file mode 100644 index 0000000..7d08b83 Binary files /dev/null and b/Fonts/Futura.ttf differ diff --git a/Fonts/Ubuntu.ttf b/Fonts/Ubuntu.ttf new file mode 100644 index 0000000..f98a2da Binary files /dev/null and b/Fonts/Ubuntu.ttf differ diff --git a/LICENSE b/LICENSE index cde4ac6..133993b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,63 @@ -This is free and unencumbered software released into the public domain. +COMMUNIST DIGITAL MANIFEST LICENSE (CDM) v1.0 -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. +PREAMBLE +Digital resources constitute collective human heritage. This license guarantees freedom for individual creators while ensuring collective accountability for commercial exploitation. Through these terms, humanity asserts sovereignty over its digital means of production. -In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. +ARTICLE 1: DEFINITIONS +1.1 "The Commons" refers to the software and associated materials governed by this license. +1.2 "Laborer" denotes any non-commercial user, including individuals and donation-funded projects. +1.3 "Capital" signifies any entity generating revenue beyond voluntary donations through use of the Commons. +1.4 "Derivative Work" means any modification, extension, or adaptation serving third parties. +1.5 "Public Deployment" indicates any implementation accessible beyond the deployer's immediate control. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +ARTICLE 2: UNIVERSAL GRANTS +Permission is hereby granted to all persons to use, reproduce, modify, and distribute the Commons without restriction, provided the subsequent conditions are met according to the nature of deployment. -For more information, please refer to +ARTICLE 3: CAPITAL OBLIGATIONS +Capital deploying Derivative Works must satisfy these requirements: + +3.1 Source Liberation: +Complete corresponding source code for all Derivative Works must be made available, including build scripts, installation instructions, and modification records. + +3.2 Commons Attribution: +A visible acknowledgment must appear in user-accessible interfaces stating: "This product incorporates the [Commons Name] digital commons. Support community-owned technology at [Project URL]." + +3.3 Data Sovereignty: +Mechanisms for one-click export of user data must be implemented using open formats (JSON, CSV, or ActivityPub), enabling migration to compatible systems without functional degradation. + +ARTICLE 4: PUBLIC DEPLOYMENT REQUIREMENTS +All Public Deployments regardless of funding must comply with: + +4.1 Asset Transparency: +Editable source formats must accompany non-branding assets, including documentation (Markdown/ODT), graphics (SVG/PSD), and configurations (YAML/JSON). + +4.2 Interoperability: +Networked deployments must implement standardized federation protocols ensuring equal access across compatible instances. + +ARTICLE 5: PROHIBITIONS + +5.1 Trademark Protection: +Use of project names or logos implying official affiliation is prohibited. Modified branding elements are permitted only when visually distinct and explicitly identified as unofficial. + +5.2 Contributor Warranty: +Contributors affirm they possess necessary rights for submitted materials. Infringement liability rests exclusively with the contributor. + +ARTICLE 6: ENFORCEMENT + +6.1 Violation Remedies: +Capital failing attribution obligations forfeits license rights. Public deployments concealing assets must release source materials within thirty (30) days. Trademark violations warrant immediate takedown notices. + +6.2 Cure Period: +Violators retain license rights for thirty (30) days following formal notification of non-compliance. + +ARTICLE 7: LABORER EXEMPTION +Laborers are exempt from Articles 3-5. They may freely modify branding, disable features, and create private derivatives without obligation. + +ARTICLE 8: LICENSE EVOLUTION +Future versions require majority approval from active contributors. Existing deployments may operate under preceding terms. + +ARTICLE 9: DISCLAIMERS +THE COMMONS IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. CONTRIBUTORS SHALL NOT BE LIABLE FOR DAMAGES ARISING FROM ITS USE. THIS LICENSE DOES NOT GRANT TRADEMARK RIGHTS BEYOND ARTICLE 5.1. + +IMPLEMENTATION +This document constitutes the complete license terms. Retain this unmodified text as "LICENSE.md" in your repository root. \ No newline at end of file diff --git a/README.md b/README.md index 5df8da5..61ac090 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Radio -The Frontend UI of the [ CiF ] Web-Radio \ No newline at end of file +This is the Code of the [ CiF ] Radio Web-UI. The Backend is powered by LibreTime and IceCast2. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..c5307a9 --- /dev/null +++ b/index.html @@ -0,0 +1,55 @@ + + + + + + + + + [ CiF ] Radio + + +
+
+

[ CiF ] Radio

+

The Sound of Socialism

+
+ +
+
+ +
+
+
+ Placeholder image + Current track cover + Temporary cover +
+
+ +
+
Not Connected
+
[ CiF ] Radio
+
+
+ +
+
+
+
+ Min + Max +
+
+
Volume: 0%
+
+ +
+

You're listening together with -- Comrades

+
+
+
+
+
+ + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..cc84eb2 --- /dev/null +++ b/script.js @@ -0,0 +1,297 @@ +document.addEventListener('DOMContentLoaded', function() { + const songTitle = document.getElementById('songTitle'); + const artist = document.getElementById('artist'); + const statusElement = document.getElementById('status'); + const listenersElement = document.getElementById('listeners'); + const bitrateElement = document.getElementById('bitrate'); + const uptimeElement = document.getElementById('uptime'); + + const audio = new Audio('https://broadcast.cif.su/main'); + audio.crossOrigin = 'anonymous'; + let isPlaying = false; + let metadataInterval; + let statsInterval; + let currentShow = ''; + + // Volume Knob Variables + var knobPositionX; + var knobPositionY; + var mouseX; + var mouseY; + var knobCenterX; + var knobCenterY; + var adjacentSide; + var oppositeSide; + var currentRadiansAngle; + var getRadiansInDegrees; + var finalAngleInDegrees; + var volumeSetting = 0; // Set initial volume to 0% + var tickHighlightPosition; + var startingTickAngle = -135; + var tickContainer = document.getElementById("tickContainer"); + var volumeKnob = document.getElementById("knob"); + var boundingRectangle = volumeKnob.getBoundingClientRect(); + + // Set initial volume + audio.volume = volumeSetting / 100; + + // Format time (seconds to HH:MM:SS) + function formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + return [ + hours.toString().padStart(2, '0'), + minutes.toString().padStart(2, '0'), + secs.toString().padStart(2, '0') + ].join(':'); + } + + // Fetch Icecast stats + async function fetchStats() { + try { + const response = await fetch('https://broadcast.cif.su/status-json.xsl'); + if (!response.ok) throw new Error('Network response was not ok'); + + const data = await response.json(); + const source = data.icestats.source; + + if (source) { + // Update stats + listenersElement.textContent = source.listeners || '0'; + + if (source.server_start_iso8601) { + const startTime = new Date(source.server_start_iso8601); + const uptimeSeconds = Math.floor((new Date() - startTime) / 1000); + uptimeElement.textContent = formatTime(uptimeSeconds); + } + } + } catch (error) { + console.error('Error fetching stats:', error); + } + } + + // Fetch Libretime current track info + async function fetchCurrentTrack() { + try { + const response = await fetch('https://fm.cif.su/api/live-info'); + if (!response.ok) throw new Error('Network response was not ok'); + + const data = await response.json(); + + if (data.current && data.current.metadata) { + const track = data.current.metadata; + + // Update song info + songTitle.textContent = track.track_title || 'Unknown Track'; + artist.textContent = track.artist_name || 'Unknown Artist'; + + // Update show info if available + if (data.current.show && data.current.show.name !== currentShow) { + currentShow = data.current.show.name; + statusElement.textContent = isPlaying ? 'Playing' : `Current Show: ${currentShow}`; + } + } + } catch (error) { + console.error('Error fetching current track:', error); + // Fallback to Icecast metadata if Libretime API fails + fetchIcecastMetadata(); + } + } + + // Fallback to Icecast metadata if needed + async function fetchIcecastMetadata() { + try { + const response = await fetch('https://broadcast.cif.su/status-json.xsl'); + if (!response.ok) throw new Error('Network response was not ok'); + + const data = await response.json(); + const source = data.icestats.source; + + if (source && source.title) { + const titleParts = source.title.split(' - '); + if (titleParts.length > 1) { + artist.textContent = titleParts[0]; + songTitle.textContent = titleParts.slice(1).join(' - '); + } else { + songTitle.textContent = source.title; + } + } + } catch (error) { + console.error('Error fetching Icecast metadata:', error); + } + } + + // Volume Knob Functions + function initVolumeKnob() { + // Calculate initial rotation angle based on initial volume (0%) + const initialAngle = (volumeSetting / 100) * 270; + volumeKnob.style.transform = "rotate(" + initialAngle + "deg)"; + + // Calculate how many ticks to highlight (0% of 27 ticks is 0) + tickHighlightPosition = Math.round((volumeSetting * 2.7) / 10); + createTicks(27, tickHighlightPosition); + + volumeKnob.addEventListener(getMouseDown(), onMouseDown); + document.addEventListener(getMouseUp(), onMouseUp); + } + + //on mouse button down + function onMouseDown() { + document.addEventListener(getMouseMove(), onMouseMove); //start drag + } + + //on mouse button release + function onMouseUp() { + document.removeEventListener(getMouseMove(), onMouseMove); //stop drag + } + + //compute mouse angle relative to center of volume knob + function onMouseMove(event) { + knobPositionX = boundingRectangle.left; + knobPositionY = boundingRectangle.top; + + if(detectMobile() == "desktop") { + mouseX = event.pageX; + mouseY = event.pageY; + } else { + mouseX = event.touches[0].pageX; + mouseY = event.touches[0].pageY; + } + + knobCenterX = boundingRectangle.width / 2 + knobPositionX; + knobCenterY = boundingRectangle.height / 2 + knobPositionY; + + adjacentSide = knobCenterX - mouseX; + oppositeSide = knobCenterY - mouseY; + + currentRadiansAngle = Math.atan2(adjacentSide, oppositeSide); + + getRadiansInDegrees = currentRadiansAngle * 180 / Math.PI; + + finalAngleInDegrees = -(getRadiansInDegrees - 135); + + if(finalAngleInDegrees >= 0 && finalAngleInDegrees <= 270) { + volumeKnob.style.transform = "rotate(" + finalAngleInDegrees + "deg)"; + volumeSetting = Math.floor(finalAngleInDegrees / (270 / 100)); + tickHighlightPosition = Math.round((volumeSetting * 2.7) / 10); + createTicks(27, tickHighlightPosition); + + // Handle play/pause based on volume setting + if (volumeSetting > 0 && !isPlaying) { + audio.play() + .then(() => { + statusElement.textContent = 'Playing'; + isPlaying = true; + // Don't clear the metadata interval when pausing + }) + .catch(error => { + console.error('Error playing stream:', error); + statusElement.textContent = 'Error: ' + error.message; + }); + } else if (volumeSetting === 0 && isPlaying) { + audio.pause(); + statusElement.textContent = currentShow ? `Current Show: ${currentShow}` : 'Paused'; + isPlaying = false; + // Don't clear the metadata interval when pausing + } + + audio.volume = volumeSetting / 100; + document.getElementById("volumeValue").textContent = volumeSetting + "%"; + } + } + + //dynamically create volume knob "ticks" + function createTicks(numTicks, highlightNumTicks) { + while(tickContainer.firstChild) { + tickContainer.removeChild(tickContainer.firstChild); + } + + for(var i=0;i