Compare commits
10 Commits
liquidsoap
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fd1cbc0e60 | |||
| 4fe1e88220 | |||
| 8239a21c43 | |||
| 7d938f05e7 | |||
| b874da3a9c | |||
| 778437da35 | |||
| 8ca5deebb2 | |||
| f397ec62ed | |||
| cc5ffc0133 | |||
| 1a5453a714 |
18
Dockerfile
18
Dockerfile
@@ -5,7 +5,11 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y ffmpeg liquidsoap
|
RUN apt-get install -y ffmpeg liquidsoap
|
||||||
|
|
||||||
WORKDIR /config
|
RUN curl -fsSL https://deno.land/install.sh | sh
|
||||||
|
ENV DENO_INSTALL="/$HOME/.deno"
|
||||||
|
ENV PATH="$DENO_INSTALL/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
RUN adduser zambla
|
RUN adduser zambla
|
||||||
|
|
||||||
@@ -13,19 +17,13 @@ COPY radio.liq /opt/radio.liq
|
|||||||
COPY yt_sync.py /opt
|
COPY yt_sync.py /opt
|
||||||
COPY ultrasync.sh /opt
|
COPY ultrasync.sh /opt
|
||||||
COPY je_te_met_en_pls.py /opt
|
COPY je_te_met_en_pls.py /opt
|
||||||
|
COPY met_extractor.py /opt
|
||||||
|
COPY www /var/www
|
||||||
|
|
||||||
RUN curl -fsSL https://deno.land/install.sh | sh
|
RUN chmod +x /opt/yt_sync.py /opt/ultrasync.sh /opt/je_te_met_en_pls.py /opt/met_extractor.py
|
||||||
ENV DENO_INSTALL="/$HOME/.deno"
|
|
||||||
ENV PATH="$DENO_INSTALL/bin:$PATH"
|
|
||||||
|
|
||||||
RUN chmod +x /opt/yt_sync.py /opt/ultrasync.sh /opt/je_te_met_en_pls.py
|
|
||||||
|
|
||||||
RUN mkdir -p /songs /jingles /air-support /var/log/liquidsoap
|
RUN mkdir -p /songs /jingles /air-support /var/log/liquidsoap
|
||||||
|
|
||||||
RUN chown -R zambla:zambla /config
|
|
||||||
RUN chown -R zambla:zambla /opt
|
|
||||||
RUN chown -R zambla:zambla /songs
|
|
||||||
RUN chown -R zambla:zambla /jingles
|
|
||||||
RUN chown -R zambla:zambla /var/log/liquidsoap
|
RUN chown -R zambla:zambla /var/log/liquidsoap
|
||||||
|
|
||||||
ADD air-support /air-support
|
ADD air-support /air-support
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ Then visit http://localhost:8000 to listen!
|
|||||||
- **Liquidsoap** - Audio stream generation and HTTP server
|
- **Liquidsoap** - Audio stream generation and HTTP server
|
||||||
- **yt-dlp** - YouTube playlist downloading
|
- **yt-dlp** - YouTube playlist downloading
|
||||||
- **FFmpeg** - Audio format conversion
|
- **FFmpeg** - Audio format conversion
|
||||||
- **Python 3.9** - Automation scripts
|
- **Python 3.14** - Automation scripts
|
||||||
|
|||||||
1
air-support/.gitignore
vendored
Normal file
1
air-support/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.pls
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
HOSTNAME=${HOSTNAME:=localhost}
|
||||||
|
PORT=${PORT:=8000}
|
||||||
|
|
||||||
export HOSTNAME=${HOSTNAME:=localhost}
|
touch /songs/playlist.pls /jingles/playlist.pls
|
||||||
export PORT=${PORT:=8000}
|
|
||||||
export MAX_LISTENERS=${MAX_LISTENERS:=30}
|
|
||||||
export ADMIN_USER=${ADMIN_USER:=admin}
|
|
||||||
export ADMIN_PASSWORD=${ADMIN_PASSWORD:=admin}
|
|
||||||
|
|
||||||
runuser -l zambla 'touch /songs/playlist.pls /jingles/playlist.pls'
|
# fallback
|
||||||
|
python /opt/je_te_met_en_pls.py /air-support
|
||||||
|
python /opt/je_te_met_en_pls.py /songs
|
||||||
|
python /opt/je_te_met_en_pls.py /jingles
|
||||||
|
|
||||||
# Start background sync process
|
# Start background sync process
|
||||||
/opt/ultrasync.sh &
|
/opt/ultrasync.sh &
|
||||||
|
|
||||||
# fallback
|
|
||||||
runuser -l zambla '/opt/je_te_met_en_pls.py /air-support /air-support/playlist.pls'
|
|
||||||
# Run Liquidsoap as zambla user
|
# Run Liquidsoap as zambla user
|
||||||
runuser -l zambla -c 'liquidsoap /opt/radio.liq'
|
runuser -l zambla -c "HOSTNAME=$HOSTNAME PORT=$PORT liquidsoap /opt/radio.liq"
|
||||||
|
|||||||
@@ -1,13 +1,47 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import glob, os, sys
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def generate_pls(directory: str, output_file: str = None) -> None:
|
def get_song_length(file: Path) -> float:
|
||||||
|
ret = subprocess.run(
|
||||||
|
[
|
||||||
|
"ffprobe",
|
||||||
|
"-i",
|
||||||
|
str(file.absolute()),
|
||||||
|
"-show_entries",
|
||||||
|
"format=duration",
|
||||||
|
"-v",
|
||||||
|
"quiet",
|
||||||
|
"-of",
|
||||||
|
"csv=p=0",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
if ret.returncode != 0:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
return float(ret.stdout)
|
||||||
|
except ValueError:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def generate_playlist(
|
||||||
|
directory: Path,
|
||||||
|
pls_file: Path | None = None,
|
||||||
|
json_file: Path | None = None,
|
||||||
|
) -> None:
|
||||||
"""Generate a .pls playlist file from all audio files in a directory.
|
"""Generate a .pls playlist file from all audio files in a directory.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
directory: Path to directory containing audio files
|
directory: Path to directory containing audio files
|
||||||
output_file: Path to output .pls file (default: directory/playlist.pls)
|
pls_file: Path to output .pls file (default: directory/playlist.pls)
|
||||||
|
json_file: Path to output .json file (default: directory/playlist.json)
|
||||||
"""
|
"""
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
print(f"Error: Directory '{directory}' does not exist")
|
print(f"Error: Directory '{directory}' does not exist")
|
||||||
@@ -30,41 +64,64 @@ def generate_pls(directory: str, output_file: str = None) -> None:
|
|||||||
audio_files.sort()
|
audio_files.sort()
|
||||||
|
|
||||||
# Determine output file path
|
# Determine output file path
|
||||||
if output_file is None:
|
if pls_file is None:
|
||||||
output_file = os.path.join(directory, "playlist.pls")
|
pls_file = directory / "playlist.pls"
|
||||||
|
|
||||||
# Generate .pls content
|
if json_file is None:
|
||||||
|
json_file = directory / "playlist.json"
|
||||||
|
|
||||||
|
json_content = {}
|
||||||
|
|
||||||
|
# Generate output content
|
||||||
pls_content = ["[playlist]"]
|
pls_content = ["[playlist]"]
|
||||||
pls_content.append(f"NumberOfEntries={len(audio_files)}")
|
pls_content.append(f"NumberOfEntries={len(audio_files)}")
|
||||||
pls_content.append("")
|
pls_content.append("")
|
||||||
|
|
||||||
for idx, file_path in enumerate(audio_files, start=1):
|
for idx, path in enumerate(audio_files, start=1):
|
||||||
# Get absolute path
|
file = Path(path)
|
||||||
abs_path = os.path.abspath(file_path)
|
|
||||||
filename = os.path.basename(file_path)
|
|
||||||
|
|
||||||
pls_content.append(f"File{idx}={abs_path}")
|
title = file.stem.replace('_', ' ')
|
||||||
pls_content.append(f"Title{idx}={filename}")
|
duration = get_song_length(file)
|
||||||
pls_content.append(f"Length{idx}=-1")
|
|
||||||
|
pls_content.append(f"File{idx}={file.absolute()}")
|
||||||
|
pls_content.append(f"Title{idx}={title}")
|
||||||
|
pls_content.append(f"Length{idx}={duration}")
|
||||||
pls_content.append("")
|
pls_content.append("")
|
||||||
|
|
||||||
|
json_content[str(file.absolute())] = {
|
||||||
|
"index": idx,
|
||||||
|
"file": str(file.absolute()),
|
||||||
|
"title": title,
|
||||||
|
"duration": duration,
|
||||||
|
}
|
||||||
|
|
||||||
pls_content.append("Version=2")
|
pls_content.append("Version=2")
|
||||||
|
|
||||||
# Write to file
|
# Write to file
|
||||||
with open(output_file, "w") as f:
|
with open(pls_file, "w") as f:
|
||||||
f.write("\n".join(pls_content))
|
f.write("\n".join(pls_content))
|
||||||
|
|
||||||
print(f"Generated '{output_file}' with {len(audio_files)} entries")
|
with open(json_file, "w") as f:
|
||||||
|
json.dump(json_content, f)
|
||||||
|
|
||||||
|
print(f"Generated '{pls_file}' with {len(audio_files)} entries")
|
||||||
|
print(f"Generated '{json_file}' with {len(audio_files)} entries")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: je_te_met_en_pls.py <directory> [output_file.pls]")
|
print(
|
||||||
|
"Usage: je_te_met_en_pls.py <directory> [output_file.pls] [output_file.json]"
|
||||||
|
)
|
||||||
print("Example: je_te_met_en_pls.py /songs")
|
print("Example: je_te_met_en_pls.py /songs")
|
||||||
print("Example: je_te_met_en_pls.py /songs /tmp/my_playlist.pls")
|
print("Example: je_te_met_en_pls.py /songs /tmp/my_playlist.pls")
|
||||||
|
print(
|
||||||
|
"Example: je_te_met_en_pls.py /songs /tmp/my_playlist.pls /tmp/my_playlist.json"
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
directory = sys.argv[1]
|
directory = sys.argv[1]
|
||||||
output_file = sys.argv[2] if len(sys.argv) > 2 else None
|
pls_file = Path(sys.argv[2]) if len(sys.argv) > 2 else None
|
||||||
|
json_file = Path(sys.argv[3]) if len(sys.argv) > 3 else None
|
||||||
|
|
||||||
generate_pls(directory, output_file)
|
generate_playlist(Path(directory), pls_file, json_file)
|
||||||
|
|||||||
14
met_extractor.py
Executable file
14
met_extractor.py
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(
|
||||||
|
"Usage: met_etractor.py <playlist.json> <key>"
|
||||||
|
)
|
||||||
|
print("Example: met_extractor.py /tmp/my_playlist.json key")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||||
|
met = json.load(f)
|
||||||
|
print(json.dumps(met.get(sys.argv[2], {})))
|
||||||
117
radio.liq
117
radio.liq
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/liquidsoap
|
#!/usr/bin/liquidsoap
|
||||||
|
|
||||||
# Radio Bullshit - Liquidsoap Configuration
|
# Radio Bullshit - Liquidsoap Configuration
|
||||||
# Modern open-source streaming with automatic song/jingle alternation
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
@@ -11,6 +10,10 @@
|
|||||||
hostname = environment.get(default="localhost", "HOSTNAME")
|
hostname = environment.get(default="localhost", "HOSTNAME")
|
||||||
port = int_of_string(environment.get(default="8000", "PORT"))
|
port = int_of_string(environment.get(default="8000", "PORT"))
|
||||||
|
|
||||||
|
folder_songs = "/songs"
|
||||||
|
folder_jingles = "/jingles"
|
||||||
|
folder_air_support = "/air-support"
|
||||||
|
|
||||||
# Log configuration
|
# Log configuration
|
||||||
log.file.path := "/var/log/liquidsoap/radio-bullshit.log"
|
log.file.path := "/var/log/liquidsoap/radio-bullshit.log"
|
||||||
log.level := 3
|
log.level := 3
|
||||||
@@ -24,7 +27,7 @@ songs = playlist(
|
|||||||
mode="randomize",
|
mode="randomize",
|
||||||
reload=60,
|
reload=60,
|
||||||
reload_mode="watch",
|
reload_mode="watch",
|
||||||
"/songs/playlist.pls"
|
"#{folder_songs}/playlist.pls"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Jingles playlist
|
# Jingles playlist
|
||||||
@@ -32,23 +35,62 @@ jingles = playlist(
|
|||||||
mode="randomize",
|
mode="randomize",
|
||||||
reload=60,
|
reload=60,
|
||||||
reload_mode="watch",
|
reload_mode="watch",
|
||||||
"/jingles/playlist.pls"
|
"#{folder_jingles}/playlist.pls"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Air support (fallback audio when nothing else is available)
|
# Air support (fallback audio when nothing else is available)
|
||||||
# air_support = playlist(
|
air_support = mksafe(
|
||||||
# mode="randomize",
|
playlist(
|
||||||
# reload_mode="watch",
|
mode="randomize",
|
||||||
# "/air-support/playlist.pls"
|
reload_mode="watch",
|
||||||
# )
|
"#{folder_air_support}/playlist.pls"
|
||||||
air_support = single("/air-support/Airplane_Sound_Effect.ogg")
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# METADATA UPDATE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def enrich_metadata(folder, m)
|
||||||
|
entry = process.read("/opt/met_extractor.py #{folder}/playlist.json '#{metadata.filename(m)}'")
|
||||||
|
let json.parse (entry :
|
||||||
|
{
|
||||||
|
index: int?,
|
||||||
|
file: string?,
|
||||||
|
title: string?,
|
||||||
|
duration: float?
|
||||||
|
}
|
||||||
|
) = entry
|
||||||
|
[("title", "#{entry.title}")]
|
||||||
|
end
|
||||||
|
|
||||||
|
songs = map_metadata(
|
||||||
|
fun (m) -> begin
|
||||||
|
enrich_metadata(folder_songs, m)
|
||||||
|
end,
|
||||||
|
songs
|
||||||
|
)
|
||||||
|
|
||||||
|
jingles = map_metadata(
|
||||||
|
fun (m) -> begin
|
||||||
|
enrich_metadata(folder_jingles, m)
|
||||||
|
end,
|
||||||
|
jingles
|
||||||
|
)
|
||||||
|
|
||||||
|
air_support = map_metadata(
|
||||||
|
fun (m) -> begin
|
||||||
|
enrich_metadata(folder_air_support, m)
|
||||||
|
end,
|
||||||
|
air_support
|
||||||
|
)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ALTERNATING LOGIC
|
# ALTERNATING LOGIC
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Alternate between songs and jingles: play 1 jingle, then 4 songs
|
# Alternate between songs and jingles: play 1 jingle, then 4 songs
|
||||||
# This creates a pattern: jingle, song, song, song, song, jingle, ...
|
# This creates a pattern: jingle, song, jingle, ...
|
||||||
radio = rotate(
|
radio = rotate(
|
||||||
weights=[1, 1],
|
weights=[1, 1],
|
||||||
[jingles, songs]
|
[jingles, songs]
|
||||||
@@ -80,34 +122,26 @@ output.harbor(
|
|||||||
# Enable built-in HTTP server for stream and status
|
# Enable built-in HTTP server for stream and status
|
||||||
settings.harbor.bind_addrs := ["0.0.0.0"]
|
settings.harbor.bind_addrs := ["0.0.0.0"]
|
||||||
|
|
||||||
# Add a simple HTML page
|
# Add static folder
|
||||||
|
harbor.http.static(
|
||||||
|
port=port,
|
||||||
|
path="/static/",
|
||||||
|
content_type=
|
||||||
|
fun (path) -> begin
|
||||||
|
file.mime.magic(path)
|
||||||
|
end,
|
||||||
|
"/var/www/static"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add index file
|
||||||
harbor.http.register(
|
harbor.http.register(
|
||||||
port=port,
|
port=port,
|
||||||
method="GET",
|
method="GET",
|
||||||
"/",
|
"/",
|
||||||
fun (_, response) -> begin
|
fun (_, response) -> begin
|
||||||
html = "<!DOCTYPE html>
|
response.html(
|
||||||
<html>
|
file.contents("/var/www/index.html")
|
||||||
<head>
|
)
|
||||||
<title>Radio Bullshit</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #1a1a1a; color: #fff; }
|
|
||||||
h1 { color: #ff6b6b; }
|
|
||||||
audio { margin: 20px; }
|
|
||||||
a { color: #4ecdc4; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Radio Bullshit</h1>
|
|
||||||
<p>J'ai une superbe opportunité de travail</p>
|
|
||||||
<audio controls autoplay>
|
|
||||||
<source src=\"/radio-bullshit\" type=\"audio/mpeg\">
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>
|
|
||||||
<p><a href=\"/radio-bullshit.m3u\">Download M3U Playlist</a></p>
|
|
||||||
</body>
|
|
||||||
</html>"
|
|
||||||
response.html(html)
|
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,14 +158,13 @@ harbor.http.register(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Status endpoint (JSON)
|
# Status endpoint (JSON)
|
||||||
# harbor.http.register(
|
harbor.http.register(
|
||||||
# port=port,
|
port=port,
|
||||||
# method="GET",
|
method="GET",
|
||||||
# "/status.json",
|
"/status.json",
|
||||||
# fun (_, response) -> begin
|
fun (_, response) -> begin
|
||||||
# status = '{"status":"online","stream":"Radio Bullshit","mount":"/radio-bullshit"}'
|
response.json(radio.last_metadata() ?? [("status", "down")])
|
||||||
# response.json(parse_json(status))
|
end
|
||||||
# end
|
)
|
||||||
# )
|
|
||||||
|
|
||||||
log("Radio Bullshit is now streaming on http://#{hostname}:#{port}/radio-bullshit")
|
log("Radio Bullshit is now streaming on http://#{hostname}:#{port}/radio-bullshit")
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
pip3 install -U yt-dlp
|
pip3 install -U yt-dlp
|
||||||
runuser -l zambla -c '/opt/je_te_met_en_pls.py /songs /songs/playlist.pls' || true
|
/opt/yt_sync.py || true
|
||||||
runuser -l zambla -c '/opt/je_te_met_en_pls.py /jingles /jingles/playlist.pls' || true
|
|
||||||
runuser -l zambla -c '/opt/yt_sync.py' || true
|
/opt/je_te_met_en_pls.py /songs || true
|
||||||
|
/opt/je_te_met_en_pls.py /jingles || true
|
||||||
sleep 6h
|
sleep 6h
|
||||||
done
|
done
|
||||||
|
|||||||
76
www/index.html
Normal file
76
www/index.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
x-data="{
|
||||||
|
metadata: { status: 'down' },
|
||||||
|
isTitleOverflow: false
|
||||||
|
}"
|
||||||
|
x-init="setInterval(
|
||||||
|
async () => {
|
||||||
|
metadata = await getMetadata()
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
)
|
||||||
|
$watch('metadata.title', async (value, oldValue) => {
|
||||||
|
if (value == oldValue){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// First, reset overflow for proper recalculation
|
||||||
|
isTitleOverflow = false
|
||||||
|
await $nextTick()
|
||||||
|
// Now check for overflow
|
||||||
|
isTitleOverflow = $refs.scrollTitle.offsetWidth < $refs.scrollTitle.scrollWidth
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<head>
|
||||||
|
<script defer src="/static/alpine.min.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title x-text="'Radio bullshit, la radio du paradis ! Now playing: ' + metadata.title"></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
class="radio-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="track-title"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
x-text="metadata.title ?? '𝐍𝐨𝐰 𝐥𝐨𝐚𝐝𝐢𝐧𝐠. . .'"
|
||||||
|
x-ref="scrollTitle"
|
||||||
|
:class="{
|
||||||
|
overflow: isTitleOverflow
|
||||||
|
}"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<audio :title="metadata.title" controls autoplay>
|
||||||
|
<source src="/radio-bullshit" type="audio/mpeg" preload="none" >
|
||||||
|
Ton navigateur ne supporte pas l'audio HTML5. Pas de Zambla pour toi !
|
||||||
|
</audio>
|
||||||
|
<p>
|
||||||
|
<ul class="radio-links">
|
||||||
|
<li>
|
||||||
|
<a href="/radio-bullshit" target="_blank">
|
||||||
|
🎵 VLC Stream
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/radio-bullshit.m3u">
|
||||||
|
📄 M3U file
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
const getMetadata = async () => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
await (await fetch("/status.json")).json()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
5
www/static/alpine.min.js
vendored
Normal file
5
www/static/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
78
www/static/style.css
Normal file
78
www/static/style.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/* Reset minimal */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0e0e0e;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title p.overflow {
|
||||||
|
padding-left: 100%;
|
||||||
|
animation: scroll-title 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title:hover p.overflow {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll-title {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-150%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-links {
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-links li {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-links a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-links a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ if __name__ == "__main__":
|
|||||||
ydl_parameters = {
|
ydl_parameters = {
|
||||||
"format": "bestaudio/best",
|
"format": "bestaudio/best",
|
||||||
"postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "vorbis"}],
|
"postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "vorbis"}],
|
||||||
"outtmpl": f"{songs_folder}/%(title)s.godwin",
|
"outtmpl": f"{songs_folder}/%(title)s",
|
||||||
"ignoreerrors": True,
|
"ignoreerrors": True,
|
||||||
"restrictfilenames": True,
|
"restrictfilenames": True,
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Radio Bullshit Jingles
|
# Radio Bullshit Jingles
|
||||||
## change output path for Radio Bullshit jingles
|
## change output path for Radio Bullshit jingles
|
||||||
ydl_parameters["outtmpl"] = f"{jingles_folder}/%(title)s.godwin"
|
ydl_parameters["outtmpl"] = f"{jingles_folder}/%(title)s"
|
||||||
## download
|
## download
|
||||||
ydl = yt_dlp.YoutubeDL(ydl_parameters)
|
ydl = yt_dlp.YoutubeDL(ydl_parameters)
|
||||||
ydl.extract_info(
|
ydl.extract_info(
|
||||||
|
|||||||
Reference in New Issue
Block a user