Display current music
All checks were successful
ci / deploy (push) Successful in 2m30s

This commit is contained in:
2026-01-21 00:37:15 +01:00
parent f397ec62ed
commit 8ca5deebb2
6 changed files with 145 additions and 34 deletions

View File

@@ -5,6 +5,10 @@ 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
RUN curl -fsSL https://deno.land/install.sh | sh
ENV DENO_INSTALL="/$HOME/.deno"
ENV PATH="$DENO_INSTALL/bin:$PATH"
WORKDIR / WORKDIR /
RUN adduser zambla RUN adduser zambla
@@ -13,13 +17,10 @@ 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 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

View File

@@ -3,6 +3,7 @@ import glob
import os import os
import sys import sys
import subprocess import subprocess
import json
from pathlib import Path from pathlib import Path
@@ -30,12 +31,17 @@ def get_song_length(file: Path) -> float:
return -1 return -1
def generate_pls(directory: str, output_file: str | None = None) -> None: 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")
@@ -58,10 +64,15 @@ def generate_pls(directory: str, output_file: str | None = 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("")
@@ -69,28 +80,48 @@ def generate_pls(directory: str, output_file: str | None = None) -> None:
for idx, path in enumerate(audio_files, start=1): for idx, path in enumerate(audio_files, start=1):
file = Path(path) file = Path(path)
title = file.stem.replace('_', ' ')
duration = get_song_length(file)
pls_content.append(f"File{idx}={file.absolute()}") pls_content.append(f"File{idx}={file.absolute()}")
pls_content.append(f"Title{idx}={file.stem.replace('_', ' ')}") pls_content.append(f"Title{idx}={title}")
pls_content.append(f"Length{idx}={get_song_length(file)}") 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
View 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], {})))

View File

@@ -10,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
@@ -23,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
@@ -31,7 +35,7 @@ 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)
@@ -39,16 +43,54 @@ air_support = mksafe(
playlist( playlist(
mode="randomize", mode="randomize",
reload_mode="watch", reload_mode="watch",
"/air-support/playlist.pls" "#{folder_air_support}/playlist.pls"
) )
) )
# ============================================================================
# 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]
@@ -112,14 +154,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")

View File

@@ -1,16 +1,28 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script defer src="/static/alpine.min.js"></script>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Radio bullshit, la radio du paradis !</title> <title>Radio bullshit, la radio du paradis !</title>
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon">
<link rel="icon" href="/static/favicon.ico" type="image/x-icon"> <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
</head> </head>
<body> <body>
<div
x-data="{ metadata: { status: 'down' } }"
x-init="setInterval(
async () => {
metadata = await get_metadata()
},
1000,
)
"
>
<p x-show="metadata.status == 'ready'" x-text="metadata.title"></p>
<audio controls autoplay> <audio controls autoplay>
<source src="/radio-bullshit" type="audio/mpeg" preload="none" > <source src="/radio-bullshit" type="audio/mpeg" preload="none" >
</audio> </audio>
</div>
<p> <p>
<ul> <ul>
<li><a href="/radio-bullshit">VLC Stream</a></li> <li><a href="/radio-bullshit">VLC Stream</a></li>
@@ -19,4 +31,11 @@
</p> </p>
</body> </body>
<script>
const get_metadata = async () => {
return Object.fromEntries(
await (await fetch("/status.json")).json()
)
}
</script>
</html> </html>

5
www/static/alpine.min.js vendored Normal file

File diff suppressed because one or more lines are too long