This commit is contained in:
11
Dockerfile
11
Dockerfile
@@ -5,6 +5,10 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update
|
||||
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 /
|
||||
|
||||
RUN adduser zambla
|
||||
@@ -13,13 +17,10 @@ COPY radio.liq /opt/radio.liq
|
||||
COPY yt_sync.py /opt
|
||||
COPY ultrasync.sh /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
|
||||
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 chmod +x /opt/yt_sync.py /opt/ultrasync.sh /opt/je_te_met_en_pls.py /opt/met_extractor.py
|
||||
|
||||
RUN mkdir -p /songs /jingles /air-support /var/log/liquidsoap
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import glob
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -30,12 +31,17 @@ def get_song_length(file: Path) -> float:
|
||||
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.
|
||||
|
||||
Args:
|
||||
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):
|
||||
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()
|
||||
|
||||
# Determine output file path
|
||||
if output_file is None:
|
||||
output_file = os.path.join(directory, "playlist.pls")
|
||||
if pls_file is None:
|
||||
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.append(f"NumberOfEntries={len(audio_files)}")
|
||||
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):
|
||||
file = Path(path)
|
||||
|
||||
title = file.stem.replace('_', ' ')
|
||||
duration = get_song_length(file)
|
||||
|
||||
pls_content.append(f"File{idx}={file.absolute()}")
|
||||
pls_content.append(f"Title{idx}={file.stem.replace('_', ' ')}")
|
||||
pls_content.append(f"Length{idx}={get_song_length(file)}")
|
||||
pls_content.append(f"Title{idx}={title}")
|
||||
pls_content.append(f"Length{idx}={duration}")
|
||||
pls_content.append("")
|
||||
|
||||
json_content[str(file.absolute())] = {
|
||||
"index": idx,
|
||||
"file": str(file.absolute()),
|
||||
"title": title,
|
||||
"duration": duration,
|
||||
}
|
||||
|
||||
pls_content.append("Version=2")
|
||||
|
||||
# Write to file
|
||||
with open(output_file, "w") as f:
|
||||
with open(pls_file, "w") as f:
|
||||
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 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 /tmp/my_playlist.pls")
|
||||
print(
|
||||
"Example: je_te_met_en_pls.py /songs /tmp/my_playlist.pls /tmp/my_playlist.json"
|
||||
)
|
||||
sys.exit(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], {})))
|
||||
67
radio.liq
67
radio.liq
@@ -10,6 +10,10 @@
|
||||
hostname = environment.get(default="localhost", "HOSTNAME")
|
||||
port = int_of_string(environment.get(default="8000", "PORT"))
|
||||
|
||||
folder_songs = "/songs"
|
||||
folder_jingles = "/jingles"
|
||||
folder_air_support = "/air-support"
|
||||
|
||||
# Log configuration
|
||||
log.file.path := "/var/log/liquidsoap/radio-bullshit.log"
|
||||
log.level := 3
|
||||
@@ -23,7 +27,7 @@ songs = playlist(
|
||||
mode="randomize",
|
||||
reload=60,
|
||||
reload_mode="watch",
|
||||
"/songs/playlist.pls"
|
||||
"#{folder_songs}/playlist.pls"
|
||||
)
|
||||
|
||||
# Jingles playlist
|
||||
@@ -31,7 +35,7 @@ jingles = playlist(
|
||||
mode="randomize",
|
||||
reload=60,
|
||||
reload_mode="watch",
|
||||
"/jingles/playlist.pls"
|
||||
"#{folder_jingles}/playlist.pls"
|
||||
)
|
||||
|
||||
# Air support (fallback audio when nothing else is available)
|
||||
@@ -39,16 +43,54 @@ air_support = mksafe(
|
||||
playlist(
|
||||
mode="randomize",
|
||||
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
|
||||
# ============================================================================
|
||||
|
||||
# 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(
|
||||
weights=[1, 1],
|
||||
[jingles, songs]
|
||||
@@ -112,14 +154,13 @@ harbor.http.register(
|
||||
)
|
||||
|
||||
# Status endpoint (JSON)
|
||||
# harbor.http.register(
|
||||
# port=port,
|
||||
# method="GET",
|
||||
# "/status.json",
|
||||
# fun (_, response) -> begin
|
||||
# status = '{"status":"online","stream":"Radio Bullshit","mount":"/radio-bullshit"}'
|
||||
# response.json(parse_json(status))
|
||||
# end
|
||||
# )
|
||||
harbor.http.register(
|
||||
port=port,
|
||||
method="GET",
|
||||
"/status.json",
|
||||
fun (_, response) -> begin
|
||||
response.json(radio.last_metadata() ?? [("status", "down")])
|
||||
end
|
||||
)
|
||||
|
||||
log("Radio Bullshit is now streaming on http://#{hostname}:#{port}/radio-bullshit")
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script defer src="/static/alpine.min.js"></script>
|
||||
<meta charset="utf-8">
|
||||
<title>Radio bullshit, la radio du paradis !</title>
|
||||
<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>
|
||||
<audio controls autoplay>
|
||||
<source src="/radio-bullshit" type="audio/mpeg" preload="none" >
|
||||
</audio>
|
||||
|
||||
<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>
|
||||
<source src="/radio-bullshit" type="audio/mpeg" preload="none" >
|
||||
</audio>
|
||||
</div>
|
||||
<p>
|
||||
<ul>
|
||||
<li><a href="/radio-bullshit">VLC Stream</a></li>
|
||||
@@ -19,4 +31,11 @@
|
||||
</p>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
const get_metadata = 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
Reference in New Issue
Block a user