nothing@nowhere - 2021-07-11

Lainon Radio Setup

In this tutorial we're going to setup a web radio for lainons on a debian server VM, figuring out how barrucadu installed lainon.life by looking at his github repository.

Initial Setup

First let's clone the repository into /srv


root@lain:/srv# apt update -y ; apt upgrade -y ; apt install git net-utils vim -y
root@lain:/srv# cd /srv
root@lain:/srv# git clone https://github.com/barrucadu/lainonlife

Once that's done we start to install the required dependencies:


root@lain:/srv# apt install curl mpd mpc alsa-utils nginx -y

We're going to start with the nginx configuration:


root@lain:/srv# ls
lainonlife
root@lain:/srv# cd lainonlife/examples/
root@lain:/srv/lainonlife/examples# ls -l
total 12
-rw-r--r-- 1 root root 1944 Jul 11 22:11 icecast.xml
-rw-r--r-- 1 root root 1136 Jul 11 22:11 mpd-cyberia.conf
-rw-r--r-- 1 root root 1161 Jul 11 22:11 nginx.conf


root@lain:/srv/lainonlife/examples# cp nginx.conf /etc/nginx/sites-available/lainon.conf
root@lain:/srv/lainonlife/examples# cd /etc/nginx/sites-available/
root@lain:/etc/nginx/sites-available# rm default
root@lain:/etc/nginx/sites-available# rm ../sites-enabled/default
root@lain:/etc/nginx/sites-available# vim lain.conf

root@lain:/etc/nginx/sites-available# cat lain.conf
  server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name lain.void.yt;
    root /srv/http;

    proxy_set_header        Host $host;
    proxy_set_header        X-Real-IP $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto $scheme;
    proxy_set_header        X-Forwarded-Host $host;
    proxy_set_header        X-Forwarded-Server $host;
    proxy_set_header        Accept-Encoding "";

    # this is needed to let people ajax stuff from other sites.
    add_header 'Access-Control-Allow-Origin' '*';

    # this is very important!  if you don't set it, nginx will buffer
    # the proxied audio stream to disk and eat all your space!
    proxy_max_temp_file_size 0;

    location / {
      try_files $uri $uri/ @script;
    }

    # icecast
    location /radio/ {
      proxy_pass http://localhost:8000/;
    }

    # grafana
    #location /graphs/ {
    #  proxy_pass http://localhost:8001/;
    #}

    # backend
    location @script {
      proxy_pass http://localhost:8002;
    }
  }

root@lain:/etc/nginx/sites-available# ln -s /etc/nginx/sites-available/lain.conf /etc/nginx/sites-enabled/
root@lain:/etc/nginx/sites-available# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

So right here in the nginx config i changed the server_name to match the DNS i will give it, and i commented out the grafana block because i won't use it. Although we do need the @script backend and icecast. So now from here let's move on to icecast, we're going to clone it in /srv too:


root@lain:/etc/nginx/sites-available# cd /srv

root@lain:/srv/Icecast-Server# apt install vorbis-tools libvorbis-dev libxml2 libxml2-dev libxslt1.1 libxslt1-dev curl pkg-config dh-autoreconf -y

root@lain:/srv/Icecast-Server# source ~/.bashrc

root@lain:/srv# git clone https://github.com/xiph/Icecast-Server --recursive

root@lain:/srv# cd Icecast-Server/

root@lain:/srv/Icecast-Server# ./autogen.sh

root@lain:/srv/Icecast-Server# chmod +x configure

root@lain:/srv/Icecast-Server# ./configure

[...]


config.status: executing libtool commands

Icecast configuration
---------------------
Version        : 2.4.99.2
cURL           : no
TLS (openSSL)  : no

Format/Codec support:
  Ogg          : yes
  Theora       : no
  Speex        : no

Features:
  YP support   : no
  Client tests : no

Development logging: no

root@lain:/srv/Icecast-Server# make
root@lain:/srv/Icecast-Server# make install

Once it's done we should be able to get the icecast binary added to our PATH, and a sample config file in /usr/local/etc/:


root@lain:/srv/Icecast-Server# which icecast
/usr/local/bin/icecast

root@lain:/srv/Icecast-Server# ls /usr/local/etc
icecast.xml

Although barrucadu also left a sample config for us to use, so let's test it after we modify it to match our Icecast install's directory paths for /var/log/icecast, /srv/IcecastServer/admin/, and /srv/Icecast-Server/web/:


root@lain:/srv/Icecast-Server# mkdir /var/log/icecast
root@lain:/srv/Icecast-Server# ls -l /var/log/icecast /srv/Icecast-Server/admin/ /srv/Icecast-Server/web/
/srv/Icecast-Server/admin/:
total 88
-rw-r--r-- 1 root root   834 Jul 11 22:36 error-html.xsl
-rw-r--r-- 1 root root   650 Jul 11 22:36 error-plaintext.xsl
drwxr-xr-x 2 root root  4096 Jul 11 22:36 includes
-rw-r--r-- 1 root root  1735 Jul 11 22:36 listclients.xsl
-rw-r--r-- 1 root root  2431 Jul 11 22:36 listmounts.xsl
-rw-r--r-- 1 root root 16245 Jul 11 22:55 Makefile
-rw-r--r-- 1 root root   495 Jul 11 22:36 Makefile.am
-rw-r--r-- 1 root root 16338 Jul 11 22:48 Makefile.in
-rw-r--r-- 1 root root  2556 Jul 11 22:36 manageauth.xsl
-rw-r--r-- 1 root root  1686 Jul 11 22:36 moveclients.xsl
-rw-r--r-- 1 root root   714 Jul 11 22:36 response.xsl
-rw-r--r-- 1 root root  4541 Jul 11 22:36 stats.xsl
-rw-r--r-- 1 root root  1227 Jul 11 22:36 updatemetadata.xsl
-rw-r--r-- 1 root root  2322 Jul 11 22:36 vclt.xsl
-rw-r--r-- 1 root root  2981 Jul 11 22:36 xspf.xsl

/srv/Icecast-Server/web/:
total 108
drwxr-xr-x 4 root root  4096 Jul 11 22:36 assets
-rw-r--r-- 1 root root  1432 Jul 11 22:36 auth.xsl
-rw-r--r-- 1 root root  1406 Jul 11 22:36 favicon.ico
-rw-r--r-- 1 root root  9124 Jul 11 22:36 icecast.png
-rw-r--r-- 1 root root  2228 Jul 11 22:36 key.png
-rw-r--r-- 1 root root 16426 Jul 11 22:55 Makefile
-rw-r--r-- 1 root root   811 Jul 11 22:36 Makefile.am
-rw-r--r-- 1 root root 16525 Jul 11 22:48 Makefile.in
-rw-r--r-- 1 root root  1632 Jul 11 22:36 server_version.xsl
-rw-r--r-- 1 root root  2009 Jul 11 22:36 status-json.xsl
-rw-r--r-- 1 root root  5387 Jul 11 22:36 status.xsl
-rw-r--r-- 1 root root  3879 Jul 11 22:36 style.css
-rw-r--r-- 1 root root  2042 Jul 11 22:36 tunein.png
-rw-r--r-- 1 root root  9450 Jul 11 22:36 xml2json.xslt

/var/log/icecast:
total 0
	
root@lain:/srv/Icecast-Server# cd conf/
root@lain:/srv/Icecast-Server/conf# cp /srv/lainonlife/examples/icecast.xml .

root@lain:/srv/Icecast-Server/conf# vim icecast.xml

Here i basically changed the hostname, the admin password and the icecast directory paths we mentionned earlier:

Below those you can also see that we will need to setup the mpd mountpoints later on. We now change the ownership of /var/log/icecast to nobody:nogroup


root@lain:/srv/Icecast-Server/conf# ls -lash icecast.xml
4.0K -rw-r--r-- 1 root root 1.9K Jul 11 23:20 icecast.xml

root@lain:/srv/Icecast-Server/conf# chown -R nobody:nogroup /var/log/icecast/
root@lain:/srv/Icecast-Server/conf# ls -lash /var/log
total 760K
4.0K drwxr-xr-x 10 root   root    4.0K Jul 11 23:04 .
4.0K drwxr-xr-x 12 root   root    4.0K Jul 11 22:13 ..
 28K -rw-r--r--  1 root   root     24K Jul 11 22:45 alternatives.log
4.0K drwxr-xr-x  2 root   root    4.0K Jul 11 23:22 apt
4.0K -rw-r-----  1 root   adm     3.8K Jul 11 23:17 auth.log
   0 -rw-rw----  1 root   utmp       0 Jul 11 21:41 btmp
4.0K drwxr-xr-x  2 root   root    4.0K Jul 11 21:52 cups
 36K -rw-r-----  1 root   adm      30K Jul 11 23:24 daemon.log
 12K -rw-r-----  1 root   adm     9.3K Jul 11 21:52 debug
408K -rw-r--r--  1 root   root    403K Jul 11 23:22 dpkg.log
8.0K -rw-r--r--  1 root   root     32K Jul 11 22:13 faillog
4.0K -rw-r--r--  1 root   root    2.1K Jul 11 22:13 fontconfig.log
4.0K drwxr-xr-x  3 root   root    4.0K Jul 11 21:48 hp
4.0K drwxr-xr-x  2 nobody nogroup 4.0K Jul 11 23:04 icecast
4.0K drwxr-xr-x  3 root   root    4.0K Jul 11 21:52 installer
 60K -rw-r-----  1 root   adm      60K Jul 11 21:59 kern.log
 12K -rw-rw-r--  1 root   utmp    286K Jul 11 22:13 lastlog
 52K -rw-r-----  1 root   adm      51K Jul 11 21:59 messages
4.0K drwxr-xr-x  2 mpd    audio   4.0K Jul 11 22:13 mpd
4.0K drwxr-xr-x  2 root   adm     4.0K Jul 11 22:28 nginx
4.0K drwx------  2 root   root    4.0K Jul 11 21:52 private
 92K -rw-r-----  1 root   adm      90K Jul 11 23:24 syslog
4.0K -rw-rw-r--  1 root   utmp    3.0K Jul 11 21:59 wtmp

root@lain:/srv/Icecast-Server/conf# icecast -c /srv/Icecast-Server/conf/icecast.xml
[2021-07-11  23:24:33] WARN CONFIG/_parse_root Warning, <location> not configured, using default value "Earth".
[2021-07-11  23:24:33] WARN CONFIG/_parse_root Warning, <admin> contact not configured, using default value "icemaster@localhost". This breaks YP directory listings. YP directory support will be disabled.
Changed groupid to 65534.
Changed supplementary groups based on user: nobody.
Changed userid to 65534.	


We can check that our icecast server is running on port 8000:

Now that's done, exit out of icecast with CTRL+C and let's configure mpd using barrucadu's sample config file:


root@lain:/srv/Icecast-Server/conf# cd ../..
root@lain:/srv# cd lainonlife/
root@lain:/srv/lainonlife# cd examples/
root@lain:/srv/lainonlife/examples# vim mpd-cyberia.conf	

First we setup the root directory for our music files in /srv/radio/music/cyberia/ and the /srv/radio/data/cyberia/playlists directory:


root@lain:/srv# mkdir -p /srv/radio/data/cyberia/playlists
root@lain:/srv# mkdir -p /srv/radio/music/cyberia/
root@lain:/srv/radio# tree .
.
├── data
│   └── cyberia
│       └── playlists
└── music
    └── cyberia

root@lain:/srv# mkdir -p  ~/.config/mpd/

root@lain:/srv# kill $(pidof mpd)
root@lain:/srv# cp /srv/lainonlife/examples/mpd-cyberia.conf ~/.config/mpd/mpd.conf
root@lain:/srv# mpd
	

Now basically we will put our music files into /srv/radio/cyberia/, and we can make playlists of it using ncmpcpp i will do it from my local machine since my debian VM is in the same local network as i am :


[ 10.10.14.8/23 ] [ /dev/pts/30 ] [~]
→ vim ~/.config/ncmpcpp/config	

[ 10.10.14.8/23 ] [ /dev/pts/30 ] [~]
→ cat ~/.config/ncmpcpp/config
##
# Files
# mpd_music_dir = "~/Music"
 lyrics_directory  = ~/.ncmpcpp/lyrics
 ncmpcpp_directory  = ~/.ncmpcpp

# mpd_host = "localhost"
 mpd_host = "10.0.0.201"
 mpd_port = "6601"


 mpd_connection_timeout = "5"
 mpd_crossfade_time = "5"

 # Playlist
 playlist_disable_highlight_delay = "0"
 playlist_display_mode = "columns"
 playlist_show_remaining_time = "yes"

 browser_display_mode = "columns"
 autocenter_mode = "yes"
 fancy_scrolling = "yes"
 follow_now_playing_lyrics = "yes"
 display_screens_numbers_on_start = "yes"
 ignore_leading_the = "yes"
 lyrics_database = "1"
 song_columns_list_format = "(10)[blue]{l} (30)[green]{a} (30)[magenta]{b} (50)[yellow]{t}"
 colors_enabled = "yes"
 main_window_color = "white"
 main_window_highlight_color =  "blue"
 header_window_color = "cyan"
 volume_color = "red"
 progressbar_color = "cyan"
 statusbar_color = "white"
 active_column_color = "cyan"
 active_window_border = "blue"

alternative_header_first_line_format = "$0$aqqu$/a {$7%a - $9}{$5%t$9}|{$8%f$9} $0$atqq$/a$9"
alternative_header_second_line_format = "{{$6%b$9}{ [$6%y$9]}}|{%D}"
song_list_format = "{$3%n │ $9}{$7%a - $9}{$5%t$9}|{$8%f$9}$R{$6 │ %b$9}{$3 │ %l$9}"
user_interface = "alternative"
#user_interface =                    "classic"
default_place_to_search_in = "database"


# visualizer
#visualizer_fifo_path = "/tmp/mpd.fifo"
#visualizer_output_name = "my_fifo"
##visualizer_sync_interval = "12"
##visualizer_type = "wave" (spectrum/wave)
#visualizer_type = "spectrum" (spectrum/wave)
#visualizer_in_stereo = "yes"
#visualizer_look = "+|"


## Navigation ##
cyclic_scrolling = "yes"
header_text_scrolling = "yes"
jump_to_now_playing_song_at_start = "yes"
lines_scrolled = "2"

## Other ##
system_encoding = "utf-8"
regular_expressions = "extended"



## Selected tracks ##
selected_item_prefix = "* "
discard_colors_if_item_is_selected = "no"

## Seeking ##
incremental_seeking = "yes"
seek_time = "1"

## Visivility ##
header_visibility = "yes"
statusbar_visibility = "yes"
titles_visibility = "yes"


progressbar_look =  "=>-"
#progressbar_boldness = "yes"
progressbar_elapsed_color = "white"

now_playing_prefix = "> "
song_status_format = " $2%a $4⟫$3⟫ $8%t $4⟫$3⟫ $5%b "
autocenter_mode = "yes"
centered_cursor = "yes"

# Misc
display_bitrate = "yes"
# enable_window_title = "no"
follow_now_playing_lyrics = "yes"
ignore_leading_the = "yes"
empty_tag_marker = ""

Now if we want to connec to the mpd server with ncmpcpp we need the mpd server to be bound to it's local ip address instead of it's localhost address, so let's change it and restart it:


root@lain:/srv/radio# vim ~/.config/mpd/mpd.conf	

root@lain:/srv/radio# cat ~/.config/mpd/mpd.conf
music_directory     "/srv/radio/music/cyberia"
playlist_directory  "/srv/radio/data/cyberia/playlists"
db_file             "/srv/radio/data/cyberia/db"
state_file          "/srv/radio/data/cyberia/state"
sticker_file        "/srv/radio/data/cyberia/sticker.sql"
log_file            "/srv/radio/data/syslog"
#bind_to_address     "127.0.0.1"
bind_to_address     "10.0.0.201"
port                "6601"

[...]

root@lain:/srv/radio# mpd
Jul 12 00:13 : hybrid_dsd: The Hybrid DSD decoder is disabled because it was not explicitly enabled
Jul 12 00:13 : exception: Input plugin 'tidal' is unavailable: No Tidal application token configured
Jul 12 00:13 : exception: Input plugin 'qobuz' is unavailable: No Qobuz app_id configured

root@lain:/srv/radio# pidof mpd
23898

You can ignore the the stderr from mpd, mpd is anyway launched as you can see from the pidof command. Now that's done we can connect to it with ncmpcpp:


[ 10.10.14.8/23 ] [ /dev/pts/30 ] [~]
→ nmap -sCV -p 6601 10.0.0.201
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-12 00:19 CEST
Nmap scan report for 10.0.0.201
Host is up (0.0047s latency).

PORT     STATE SERVICE VERSION
6601/tcp open  mpd     Music Player Daemon 0.21.4

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 0.71 seconds

[ 10.10.14.8/23 ] [ /dev/pts/30 ] [~]
→ ncmpcpp -h 10.0.0.201 -p 6601
Reading configuration from /home/nothing/.config/ncmpcpp/config...

Now obviously there are no music files to play yet, so we need to add them to the server's /srv/radio/music/cyberia/ directory:




root@lain:/srv/radio/music/cyberia# apt purge youtube-dl -y #youtube-dl is severly outdated on debian's repositories
root@lain:/srv/radio/music/cyberia# apt install python3-pip -y
root@lain:/srv/radio/music/cyberia# pip3 install youtube-dl
root@lain:/srv/radio/music/cyberia# bash
root@lain:/srv/radio/music/cyberia# youtube-dl --version
2021.06.06

root@lain:/srv/radio/music/cyberia# youtube-dl -x https://www.youtube.com/playlist?list=PLMR9WY6VV-0c0bv7MsYjudCxx0X1cXWBQ --audio-format mp3
[youtube:tab] PLMR9WY6VV-0c0bv7MsYjudCxx0X1cXWBQ: Downloading webpage
[download] Downloading playlist: Serial Experiments Lain OST
[youtube:tab] playlist Serial Experiments Lain OST: Downloading 14 videos
[download] Downloading video 1 of 14
[youtube] AsPLBQfZQ04: Downloading webpage
[download] Destination: Serial Experiment Lain -  Infanity world-AsPLBQfZQ04.webm
[download]   5.3% of 4.98MiB at 54.61KiB/s ETA 01:28

[...]

[download] Downloading video 14 of 14
[youtube] 5dbi4N6NGn4: Downloading webpage
[download] Destination: Serial Experiments Lain - Cyberia Theme-5dbi4N6NGn4.webm
[download] 100% of 2.57MiB in 00:37
[ffmpeg] Destination: Serial Experiments Lain - Cyberia Theme-5dbi4N6NGn4.mp3
Deleting original file Serial Experiments Lain - Cyberia Theme-5dbi4N6NGn4.webm (pass -k to keep)
[download] Finished downloading playlist: Serial Experiments Lain OST
	
root@lain:/srv/radio/music/cyberia# ls -lash
total 62M
4.0K drwxr-xr-x 2 root root 4.0K Jul 12 00:55  .
4.0K drwxr-xr-x 3 root root 4.0K Jul 12 00:04  ..
2.7M -rw-r--r-- 1 root root 2.7M Jun  9  2017 "Lain's Theme - Lain-o-cwuTmqz8c.mp3"
5.7M -rw-r--r-- 1 root root 5.7M Dec 15  2019 'Serial Experiment Lain - Antidepressant 044-KaOsmUDMdaE.mp3'
4.7M -rw-r--r-- 1 root root 4.7M Dec 24  2019 'Serial Experiment Lain - Cloudy with occasional rain-1TunrW7dRr0.mp3'
4.8M -rw-r--r-- 1 root root 4.8M Dec  9  2019 'Serial Experiment Lain - Duvet (cyberia remix)-juDqDMlTfUg.mp3'
1.5M -rw-r--r-- 1 root root 1.5M Mar 18  2015 'Serial Experiment Lain -  Duvet (tv version)-EnEaNaqGMqU.mp3'
5.0M -rw-r--r-- 1 root root 5.0M Oct 15  2017 'Serial Experiment Lain -  Infanity world-AsPLBQfZQ04.mp3'
4.6M -rw-r--r-- 1 root root 4.6M Jan 23  2020 'Serial Experiment Lain - Invisible file-32IlMQ8Bs6w.mp3'
4.7M -rw-r--r-- 1 root root 4.7M Dec  9  2019 'Serial Experiment Lain - Island in video cassette-CCdyzUPrLpM.mp3'
7.1M -rw-r--r-- 1 root root 7.1M Jul  9  2014 'Serial Experiment Lain -  k.i.d.s-TvCJnW46ISo.mp3'
6.1M -rw-r--r-- 1 root root 6.1M Dec 16  2019 'Serial Experiment Lain - Prayer-yyPMWWjzMG8.mp3'
4.0M -rw-r--r-- 1 root root 4.0M Dec 25  2019 'Serial Experiment Lain - Professed intention and real-D4G97Xsc8PA.mp3'
3.8M -rw-r--r-- 1 root root 3.8M Jan 23  2020 'Serial Experiment Lain - Psychedelic farm-XEGz6CJnY04.mp3'
4.7M -rw-r--r-- 1 root root 4.7M Dec 17  2019 'Serial Experiment Lain - Speed-zFwQFdAsGGA.mp3'
2.7M -rw-r--r-- 1 root root 2.7M Nov  7  2018 'Serial Experiments Lain - Cyberia Theme-5dbi4N6NGn4.mp3'

Now that's done, let's go back into ncmpcpp to make our playlist:


[ 10.10.14.8/23 ] [ /dev/pts/30 ] [~]
→ ncmpcpp -h 10.0.0.201 -p 6601
	
#once in ncmpcpp
#press 1 to get to the playlist tab
#press a to add 
#if you can't connect to it, then use ncmpcpp on the remote host:

root@lain:~# mkdir ~/.config/ncmpcpp
root@lain:~# which ncmpcpp
/usr/bin/ncmpcpp
root@lain:~# vim ~/.config/ncmpcpp/config

#make sure that you have changed the 10.0.0.201 part to 127.0.0.1
#same goes for mpd:
#also make mpd use alsa for now:

root@lain:~# cat ~/.config/mpd/mpd.conf
music_directory     "/srv/radio/music/cyberia"
playlist_directory  "/srv/radio/data/cyberia/playlists"
db_file             "/srv/radio/data/cyberia/db"
state_file          "/srv/radio/data/cyberia/state"
sticker_file        "/srv/radio/data/cyberia/sticker.sql"
log_file            "/srv/radio/data/syslog"
bind_to_address     "127.0.0.1"
#bind_to_address     "10.0.0.201"
port                "6601"

audio_output {
  name        "[mpd] cyberia (ogg)"
  description "classic lainchan radio: electronic, chiptune, weeb"
  type        "shout"
  encoder     "vorbis"
  host        "localhost"
  port        "8000"
  mount       "/mpd-cyberia.ogg"
  user        "source"
  password    "password for icecast"
  quality     "3"
  format      "44100:16:2"
  always_on   "yes"
}

audio_output {
  name        "[mpd] cyberia (mp3)"
  description "classic lainchan radio: electronic, chiptune, weeb"
  type        "shout"
  encoder     "lame"
  host        "localhost"
  port        "8000"
  mount       "/mpd-cyberia.mp3"
  user        "source"
  password    "password for icecast"
  quality     "3"
  format      "44100:16:2"
  always_on   "yes"
}

audio_output {
  type "null"
  name "null"
}

 audio_output {
     type  "alsa"
     name  "alsa audio"
     mixer_type      "software"
 }

audio_output {
    type                    "fifo"
    name                    "my_fifo"
    path                    "/tmp/mpd.fifo"
    format                  "44100:16:2"
}


root@lain:~# mpd
Jul 12 08:29 : hybrid_dsd: The Hybrid DSD decoder is disabled because it was not explicitly enabled
Jul 12 08:29 : exception: Input plugin 'tidal' is unavailable: No Tidal application token configured
Jul 12 08:29 : exception: Input plugin 'qobuz' is unavailable: No Qobuz app_id configured
root@lain:~# pidof mpd
29091

root@lain:~# netstat -alntup | grep 6601
tcp        0      0 127.0.0.1:6601          0.0.0.0:*               LISTEN      29091/mpd

root@lain:~# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press 2 to go to the browsing tab and
#press u to update the mpd database
#select the songs you want to add to the playlist with INSERT
#then once you selected them press a to add to a playlist
#hit 'New playlist', choose the name you want for it:


#press 5 to go to the playlist editor tab to check the result: 

#now here you see we have a small cyberia playlist.
#press 8 to go to the visualizer to verify that it's working properly: 

#now press 7 to go to the Outputs tab to enable the ogg and mp3 outputs:

#We're going to need those for icecast. Now that's done we can exit out of ncmpcpp and mpd:
#mpd is going to run in the background, so we can always return to it with # ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press q to exit ncmpcpp

We can verify our cyberia ogg output exists on port 8000 by launching icecast and using it to verify it:


root@lain:~# icecast -c /srv/Icecast-Server/conf/icecast.xml
[2021-07-12  09:25:43] WARN CONFIG/_parse_root Warning,  not configured, using default value "Earth".
[2021-07-12  09:25:43] WARN CONFIG/_parse_root Warning,  contact not configured, using default value "icemaster@localhost". This breaks YP directory listings. YP directory support will be disabled.
Changed groupid to 65534.
Changed supplementary groups based on user: nobody.
Changed userid to 65534.

We can log into icecast using the admin:P@SSW0RD_FOR_ADM1N credentials we used earlier in the config:

Now we need to fix the fact that icecast doesn't detect our mountpoints:

First we fix the passwords in the mpd and icecast config files:


root@lain:~# vim ~/.config/mpd/mpd.conf
root@lain:~# cat ~/.config/mpd/mpd.conf
music_directory     "/srv/radio/music/cyberia"
playlist_directory  "/srv/radio/data/cyberia/playlists"
db_file             "/srv/radio/data/cyberia/db"
state_file          "/srv/radio/data/cyberia/state"
sticker_file        "/srv/radio/data/cyberia/sticker.sql"
log_file            "/srv/radio/data/syslog"
bind_to_address     "127.0.0.1"
#bind_to_address     "10.0.0.201"
port                "6601"

audio_output {
  name        "[mpd] cyberia (ogg)"
  description "classic lainchan radio: electronic, chiptune, weeb"
  type        "shout"
  encoder     "vorbis"
  host        "localhost"
  port        "8000"
  mount       "/mpd-cyberia.ogg"
  user        "source"
  password    "P@SSW0RD"
  quality     "3"
  format      "44100:16:2"
  always_on   "yes"
}

audio_output {
  name        "[mpd] cyberia (mp3)"
  description "classic lainchan radio: electronic, chiptune, weeb"
  type        "shout"
  encoder     "lame"
  host        "localhost"
  port        "8000"
  mount       "/mpd-cyberia.mp3"
  user        "source"
  password    "P@SSW0RD"
  quality     "3"
  format      "44100:16:2"
  always_on   "yes"
}
[...]

root@lain:~# vim  /srv/Icecast-Server/conf/icecast.xml


Next we restart mpd and icecast:


root@lain:~# systemctl restart mpd
root@lain:~# icecast -c /srv/Icecast-Server/conf/icecast.xml	

Now icecast doesn't pick up our ogg stream, so let's try out the mp3 one:


root@lain:~# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config


And as you can see on icecast's admin page we see that the mp3 output does get picked up:

You can also see it appear on icecast's public /status.xsl page:

And we can play the direct .m3u link to our VLC player:

And it works! now let's make sure that mpd is running on loop while shuffling the songs, from ncmpcpp you can press r for repeat mode, and z for random mode. After that's working, we need to setup the website itself.


root@lain:/srv/lainonlife# mkdir /srv/http/
root@lain:/srv/lainonlife# cp config.json.example config.json
root@lain:/srv/lainonlife# vim config.json
root@lain:/srv/lainonlife# cat config.json
{ "channels":
  {  "cyberia":    { "mpd_host": "localhost", "mpd_port": 6601, "description": "classic lainchan radio: electronic, chiptune, weeb" }
  }

, "template":
  { "default_channel": "cyberia"
  , "icecast_status_url": "/radio/status-json.xsl"
  , "icecast_stream_url_base": "https://lain.void.yt/"
  , "server_cost": 10.00
  , "currency_symbol": "€"
  }
}
	

For now we only have one channel, but later on we can always add more. Now let's edit build.sh to use our new config json file:


root@lain:/srv/lainonlife#cd frontend 
root@lain:/srv/lainonlife/frontend# vim build.sh
root@lain:/srv/lainonlife/frontend# cat build.sh
#!/usr/bin/env bash

if [ -z "$1" ]; then
    #./build.py ../config.json.example
    ./build.py ../config.json.example
else
    ./build.py "$1"
fi
	

Now that's done we can build the frontend assets:


apt install python3-pip python-pip virtualenv -y
root@lain:/srv/lainonlife/frontend# pip install jinja2
root@lain:/srv/lainonlife/frontend# pip3 install jinja2
root@lain:/srv/lainonlife/frontend# which virtualenv
/usr/bin/virtualenv

root@lain:/srv/lainonlife/frontend# ./run.sh ../config.json
	
root@lain:/srv/lainonlife/frontend# ./run.sh ../config.json
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
Requirement already satisfied: jinja2 in ./__venv__/lib/python2.7/site-packages (from -r requirements.txt (line 1)) (2.11.3)
Requirement already satisfied: MarkupSafe>=0.23 in ./__venv__/lib/python2.7/site-packages (from jinja2->-r requirements.txt (line 1)) (1.1.1)

Now that's done, we can see our frontend assets in the _site directory:


root@lain:/srv/lainonlife/frontend# cd _site/
root@lain:/srv/lainonlife/frontend/_site# ls -lash
total 300K
4.0K drwxr-xr-x  9 root root 4.0K Jul 12 10:16 .
4.0K drwxr-xr-x 10 root root 4.0K Jul 12 10:16 ..
4.0K drwxr-xr-x  2 root root 4.0K Jul 12 10:16 404
4.0K -rw-r--r--  1 root root 1.2K Jul 12 10:16 404.html
 52K -rw-r--r--  1 root root  52K Jul 12 10:16 android-chrome-192x192.png
 88K -rw-r--r--  1 root root  88K Jul 12 10:16 android-chrome-256x256.png
 44K -rw-r--r--  1 root root  41K Jul 12 10:16 apple-touch-icon.png
4.0K drwxr-xr-x  2 root root 4.0K Jul 12 10:16 backgrounds
4.0K -rw-r--r--  1 root root  246 Jul 12 10:16 browserconfig.xml
4.0K drwxr-xr-x  2 root root 4.0K Jul 12 10:16 bumps
4.0K drwxr-xr-x  3 root root 4.0K Jul 12 10:16 css
4.0K -rw-r--r--  1 root root 1.2K Jul 12 10:16 donate.html
4.0K -rw-r--r--  1 root root 1.4K Jul 12 10:16 favicon-16x16.png
4.0K -rw-r--r--  1 root root 2.9K Jul 12 10:16 favicon-32x32.png
 16K -rw-r--r--  1 root root  15K Jul 12 10:16 favicon.ico
4.0K drwxr-xr-x  2 root root 4.0K Jul 12 10:16 file-list
8.0K -rw-r--r--  1 root root 4.3K Jul 12 10:16 index.html
4.0K drwxr-xr-x  2 root root 4.0K Jul 12 10:16 js
4.0K -rw-r--r--  1 root root  414 Jul 12 10:16 manifest.json
 28K -rw-r--r--  1 root root  26K Jul 12 10:16 mstile-150x150.png
4.0K -rw-r--r--  1 root root 1.8K Jul 12 10:16 safari-pinned-tab.svg
4.0K drwxr-xr-x  2 root root 4.0K Jul 12 10:16 upload
	

And it's all here! so let's copy it to the root of our webserver at /srv/http/


root@lain:/srv/lainonlife/frontend/_site# cd ..
root@lain:/srv/lainonlife/frontend# cp -r _site/* /srv/http/
	

now that's done, we need to run the backend bashscript called run.sh and we need to pass it a few environment variables as we run it:


root@lain:/srv/lainonlife/frontend# cd ..
root@lain:/srv/lainonlife# cd backend/
root@lain:/srv/lainonlife/backend# ls -lash
total 36K
4.0K drwxr-xr-x 2 root root 4.0K Jul 12 10:00 .
4.0K drwxr-xr-x 9 root root 4.0K Jul 12 10:05 ..
4.0K -rwxr-xr-x 1 root root 1.5K Jul 11 22:11 backend.py
4.0K -rw-r--r-- 1 root root   21 Jul 11 22:11 .gitignore
4.0K -rw-r--r-- 1 root root   58 Jul 11 22:11 requirements.txt
4.0K -rwxr-xr-x 1 root root  205 Jul 11 22:11 run.sh
8.0K -rw-r--r-- 1 root root 4.1K Jul 11 22:11 stream.py
4.0K -rw-r--r-- 1 root root 3.2K Jul 11 22:11 web.py
	
root@lain:/srv/lainonlife/backend# CONFIG=../config.json HTTP_DIR=/srv/http PORT=5000 ./run.sh

Basically here it's going to (try to) install the required python dependencies, if it fails like for me you can install the missing dependencies:



[...]

Collecting MarkupSafe>=0.23
  Using cached MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl (24 kB)
Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, Flask, python-mpd2, pytz, tzlocal, futures, funcsigs, six, apscheduler, chardet, certifi, urllib3, idna, requests
Successfully installed Flask-1.1.4 Jinja2-2.11.3 MarkupSafe-1.1.1 Werkzeug-1.0.1 apscheduler-3.7.0 certifi-2021.5.30 chardet-4.0.0 click-7.1.2 funcsigs-1.0.2 futures-3.3.0 idna-2.10 itsdangerous-1.1.0 python-mpd2-1.1.0 pytz-2021.1 requests-2.25.1 six-1.16.0 tzlocal-2.1 urllib3-1.26.6
Traceback (most recent call last):
  File "backend.py", line 7, in module>
    import stream as stream
  File "/srv/lainonlife/backend/stream.py", line 1, in module>
    from apscheduler.schedulers.background import BackgroundScheduler
ModuleNotFoundError: No module named 'apscheduler'
	
root@lain:/srv/lainonlife/backend# pip3 install apscheduler ; pip install apscheduler
root@lain:/srv/lainonlife/backend#  pip install --upgrade pip setuptools
root@lain:/srv/lainonlife/backend#  pip3 install --upgrade pip setuptools
root@lain:/srv/lainonlife/backend#  pip install --upgrade virtualenv
root@lain:/srv/lainonlife/backend# apt install python-mpd python3-mpd
root@lain:/srv/lainonlife/backend# pip install flask ; pip3 install flask

root@lain:/srv/lainonlife/backend# CONFIG=../config.json HTTP_DIR=/srv/http PORT=5000 ./run.sh
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
Requirement already satisfied: Flask>=0.12.0 in ./__venv__/lib/python2.7/site-packages (from -r requirements.txt (line 1)) (1.1.4)

[...]

Requirement already satisfied: MarkupSafe>=0.23 in ./__venv__/lib/python2.7/site-packages (from Jinja2 3.0,>=2.10.1->Flask>=0.12.0->-r requirements.txt (line 1)) (1.1.1)
 * Serving Flask app 'web' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
error talking to prometheus: HTTPConnectionPool(host='localhost', port=9090): Max retries exceeded with url: /api/v1/query?query=sum%28listeners%29+by+%28channel%29 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f82323eabe0>: Failed to establish a new connection: [Errno 111] Connection refused'))

Now it seems we forgot to install prometheus on port 9090. So let's install it following a tutorial i already made here, obviously use an up to date release version of prometheus from their release pages here to this date, the latest version is 2-28.1:


root@lain:/tmp# mkdir /srv/prometheus
root@lain:/tmp# cd /srv/prometheus
root@lain:/srv/prometheus# apt update -y ; apt upgrade -y
root@lain:/srv/prometheus# wget https://github.com/prometheus/prometheus/releases/download/v2.28.1/prometheus-2.28.1.linux-amd64.tar.gz
	
root@lain:/srv/prometheus# tar -xvzf prometheus-2.28.1.linux-amd64.tar.gz
root@lain:/srv/prometheus# cd prometheus-2.28.1.linux-amd64

root@lain:/srv/prometheus/prometheus-2.28.1.linux-amd64# useradd -rs /bin/false prometheus
root@lain:/srv/prometheus/prometheus-2.28.1.linux-amd64# cp prometheus promtool /usr/local/bin/
root@lain:/srv/prometheus/prometheus-2.28.1.linux-amd64# chown prometheus:prometheus /usr/local/bin/prometheus

root@lain:/srv/prometheus/prometheus-2.28.1.linux-amd64# mkdir /etc/prometheus
root@lain:/srv/prometheus/prometheus-2.28.1.linux-amd64# cp -R consoles/ console_libraries/ prometheus.yml /etc/prometheus/

root@lain:/srv/prometheus/prometheus-2.28.1.linux-amd64# cd /lib/systemd/system
root@lain:/lib/systemd/system# wget https://ech1.github.io/blog/servers/progra/p.service -O /lib/systemd/system/prometheus.service


root@lain:/etc/prometheus# mkdir /data/prometheus -p
root@lain:/etc/prometheus# chown -R prometheus:prometheus /data/prometheus /etc/prometheus/*

root@lain:/lib/systemd/system# systemctl enable --now prometheus
root@lain:/lib/systemd/system# systemctl enable --now prometheus

root@lain:/etc/prometheus# systemctl status prometheus
● prometheus.service - Prometheus
   Loaded: loaded (/lib/systemd/system/prometheus.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-07-12 11:01:40 CEST; 1min 9s ago
 Main PID: 545 (prometheus)
    Tasks: 9 (limit: 4700)
   Memory: 29.8M
   CGroup: /system.slice/prometheus.service
           └─545 /usr/local/bin/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/data/prometheus --web.console.templates=/etc/prometheus/consoles --web.console.libraries=/etc/prometheus/

Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.623Z caller=head.go:780 component=tsdb msg="Replaying on-disk memory mappable chunks if any"
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.624Z caller=head.go:794 component=tsdb msg="On-disk memory mappable chunks replay completed" duration=26.9µs
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.624Z caller=head.go:800 component=tsdb msg="Replaying WAL, this may take a while"
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.627Z caller=head.go:854 component=tsdb msg="WAL segment loaded" segment=0 maxSegment=0
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.629Z caller=head.go:860 component=tsdb msg="WAL replay completed" checkpoint_replay_duration=143.441µs wal_replay_duration=3.876793ms total_re
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.632Z caller=main.go:851 fs_type=EXT4_SUPER_MAGIC
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.633Z caller=main.go:854 msg="TSDB started"
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.633Z caller=main.go:981 msg="Loading configuration file" filename=/etc/prometheus/prometheus.yml
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.636Z caller=main.go:1012 msg="Completed loading of configuration file" filename=/etc/prometheus/prometheus.yml totalDuration=2.953804ms remote
Jul 12 11:01:40 lain prometheus[545]: level=info ts=2021-07-12T09:01:40.636Z caller=main.go:796 msg="Server is ready to receive web requests."

And from here we can check if prometheus is running on the correct port:

Now that prometheus is running on port 9090 we can also setup grafana but we commented it out earlier. Let's run run.sh to see if it works this time:


root@lain:/etc/prometheus# cd /srv/lainonlife/backend/

root@lain:/srv/lainonlife/backend# CONFIG=../config.json HTTP_DIR=/srv/http PORT=5000 ./run.sh
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.
Requirement already satisfied: Flask>=0.12.0 in ./__venv__/lib/python2.7/site-packages (from -r requirements.txt (line 1)) (1.1.4)

[...]

Requirement already satisfied: MarkupSafe>=0.23 in ./__venv__/lib/python2.7/site-packages (from Jinja2<3.0,>=2.10.1->Flask>=0.12.0->-r requirements.txt (line 1)) (1.1.1)
 * Serving Flask app 'web' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

This time around we don't have the missing prometheus errors, so let's check if the server is running properly, and let's restart it to run on port 8002 since we told nginx we would use it for the @script block:


root@lain:/srv/lainonlife/backend# CONFIG=../config.json HTTP_DIR=/srv/http PORT=8002 ./run.sh
DEPRECATION: Python 2.7 reached the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 is no longer maintained. pip 21.0 will drop support for Python 2.7 in January 2021. More details about Python 2 support in pip can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support pip 21.0 will remove support for this functionality.

Requirement already satisfied: MarkupSafe>=0.23 in ./__venv__/lib/python2.7/site-packages (from Jinja2<3.0,>=2.10.1->Flask>=0.12.0->-r requirements.txt (line 1)) (1.1.1)
 * Serving Flask app 'web' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:8002/ (Press CTRL+C to quit)
	

Now let's check if it works properly in the browser on port 80:

We got some progress! However we need to correct a few things, first the background:


 * Serving Flask app 'web' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:8002/ (Press CTRL+C to quit)
127.0.0.1 - - [12/Jul/2021 11:12:55] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:13:10] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:13:25] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:13:40] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:13:55] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:14:09] "GET /css/font-awesome.min.css HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:14:09] "GET /background HTTP/1.0" 404 -
127.0.0.1 - - [12/Jul/2021 11:14:09] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:14:24] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:14:39] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:14:53] "GET /css/font-awesome.min.css HTTP/1.0" 304 -
127.0.0.1 - - [12/Jul/2021 11:14:53] "GET /background HTTP/1.0" 404 -
127.0.0.1 - - [12/Jul/2021 11:14:53] "GET /playlist/cyberia.json HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 11:15:08] "GET /playlist/cyberia.json HTTP/1.0" 200 -
	

Let's upload a few backgrounds into /srv/http/backgrounds/:


root@lain:/srv/http# cd backgrounds/
root@lain:/srv/http/backgrounds# ls -l
total 4
-rw-r--r-- 1 root root 68 Jul 12 10:18 index.html
	
root@lain:/srv/http/backgrounds# wget https://wallpapercave.com/wp/wp6600388.jpg

root@lain:/srv/http/backgrounds# wget https://blog.void.yt/wallpapers/joi1.png

root@lain:/srv/http/backgrounds# wget https://blog.void.yt/wallpapers/wallpaper_blue_original.png

Now that we got some backgrounds we also need the missing /css/font-awesome.min.css in the root of the webdirectory:


root@lain:/srv/lainonlife/# cd /srv/http/css/
root@lain:/srv/http/css# wget https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css
	

Now we get some missing icons, however this is probably related to the fact that we're not accessing our lainradio server from the correct domain name lain.void.yt:

Now i intend to run this lain radio behind a reverse nginx proxy, so i'll set it up from main debian node:


root@home:~# curl ifconfig.me ; echo; ping lain.void.yt
86.243.158.34
PING void.yt (86.243.158.34) 56(84) bytes of data.
	

First we get the DNS to point to the right IP, i used a CNAME to point at my root domain's public IP, but you can also use an A record to specify which IP the subdomain / domain name should point to. Now on my main debian node i use acme.sh to get letsencrypt TLS 1.3 certificates, so let's get them after i make the reverse proxy nginx config:


root@home:/var/www/void.yt/config# cd /etc/nginx/sites-available/
root@home:/etc/nginx/sites-available# vim lain.void.yt.conf
	
root@home:/etc/nginx/sites-available# cat lain.void.yt.conf
upstream lainbackend {
        server 10.0.0.201:80;
}

server {
        listen 80;
        listen [::]:80;
        server_name lain.void.yt;
        return 301 https://$server_name$request_uri;
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name lain.void.yt;

        ssl_certificate /root/.acme.sh/lain.void.yt/fullchain.cer;
        ssl_trusted_certificate /root/.acme.sh/lain.void.yt/lain.void.yt.cer;
        ssl_certificate_key /root/.acme.sh/lain.void.yt/lain.void.yt.key;

        ssl_protocols TLSv1.3 TLSv1.2;
        ssl_ciphers 'TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;
        ssl_session_tickets off;
        ssl_ecdh_curve auto;
        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 80.67.188.188 80.67.169.40 valid=300s;
        resolver_timeout 10s;

        add_header X-XSS-Protection "1; mode=block"; #Cross-site scripting
        add_header X-Frame-Options "SAMEORIGIN" always; #clickjacking
        add_header X-Content-Type-Options nosniff; #MIME-type sniffing
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";

        location / {
                proxy_pass http://lainbackend;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "Upgrade";
        }
}

You can tweak it however you like it, then we enable the site and get the letsencrypt certificates:


root@home:/etc/nginx/sites-available# ln -s /etc/nginx/sites-available/lain.void.yt.conf /etc/nginx/sites-enabled/
root@home:/etc/nginx/sites-available# nginx -t
nginx: [emerg] BIO_new_file("/root/.acme.sh/lain.void.yt/fullchain.cer") failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/root/.acme.sh/lain.void.yt/fullchain.cer','r') error:2006D080:BIO routines:BIO_new_file:no such file)
nginx: configuration file /etc/nginx/nginx.conf test failed
root@home:/etc/nginx/sites-available# systemctl stop nginx
root@home:/etc/nginx/sites-available# acme.sh --issue --standalone -d lain.void.yt -k 4096

[...]

[Mon 12 Jul 2021 12:08:38 PM CEST] Your cert is in  /root/.acme.sh/lain.void.yt/lain.void.yt.cer
[Mon 12 Jul 2021 12:08:38 PM CEST] Your cert key is in  /root/.acme.sh/lain.void.yt/lain.void.yt.key
[Mon 12 Jul 2021 12:08:38 PM CEST] The intermediate CA cert is in  /root/.acme.sh/lain.void.yt/ca.cer
[Mon 12 Jul 2021 12:08:38 PM CEST] And the full chain certs is there:  /root/.acme.sh/lain.void.yt/fullchain.cer
	

Once that's done we can reactivate nginx and see the result:


root@home:/etc/nginx/sites-available# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

root@home:/etc/nginx/sites-available# systemctl start nginx

Now we access our webservice and we see that the domain has been verified by letsencrypt, but we're still missing the song names: so let's take a look into the /srv/lainonlife/scripts/ directory:



root@lain:/srv/lainonlife/scripts# ls -lash
total 24K
4.0K drwxr-xr-x 2 root root 4.0K Jul 12 12:23 .
4.0K drwxr-xr-x 9 root root 4.0K Jul 12 10:05 ..
4.0K -rwxr-xr-x 1 root root 2.3K Jul 11 22:11 album_times.py
4.0K -rwxr-xr-x 1 root root  943 Jul 11 22:11 file-list.sh
8.0K -rwxr-xr-x 1 root root 4.5K Jul 11 22:11 schedule.py
	

Here we have some scripts we should run. An easy one is file-list.sh:


root@lain:/srv/lainonlife/scripts# ./file-list.sh

That script is there so we can get a list of files to use:

Next we need to figure out schedule.py:


root@lain:/srv/lainonlife/scripts# pip3 install docopt
root@lain:/srv/lainonlife/scripts# python3 schedule.py --host=127.0.0.1 6601

Now if this schedule.py script errors out, that's because it's looking for the Lainchan Radio Transitions album, so to make this script work, we need the aforementionned album containing some songs, so let's create it with tracks we put into /srv/radio/music/cyberia/transitions/:


root@lain:/srv/radio/music/cyberia# mkdir transitions
root@lain:/srv/radio/music/cyberia/transitions# apt install ffmpeg
	
root@lain:/srv/radio/music/cyberia/transitions# youtube-dl -x https://www.youtube.com/playlist?list=OLAK5uy_kBzNHyOb4LcpwQI-uca1LApynboNkEvAo --audio-format mp3
[youtube:tab] OLAK5uy_kBzNHyOb4LcpwQI-uca1LApynboNkEvAo: Downloading webpage
[download] Downloading playlist: Coloris
[youtube:tab] playlist Coloris: Downloading 15 videos
[download] Downloading video 1 of 15
[youtube] YTY7UV6GpkM: Downloading webpage
[download] Coloris-YTY7UV6GpkM.webm has already been downloaded
[download] 100% of 2.57MiB
[ffmpeg] Destination: Coloris-YTY7UV6GpkM.mp3

[...]


[youtube] 75yOApYoc6Y: Downloading webpage
[download] Destination: Traveling By Night-75yOApYoc6Y.webm
[download] 100% of 2.59MiB in 00:00
[ffmpeg] Destination: Traveling By Night-75yOApYoc6Y.mp3
Deleting original file Traveling By Night-75yOApYoc6Y.webm (pass -k to keep)
[download] Finished downloading playlist: Coloris

Now that we have our transition tracks let's add them to the correct album via ncmpcpp:


root@lain:/srv/radio/music/cyberia/transitions# cd ..
root@lain:/srv/radio/music/cyberia# tree .
.
├── Lain's Theme - Lain-o-cwuTmqz8c.mp3
├── Serial Experiment Lain - Antidepressant 044-KaOsmUDMdaE.mp3
├── Serial Experiment Lain - Cloudy with occasional rain-1TunrW7dRr0.mp3
├── Serial Experiment Lain - Duvet (cyberia remix)-juDqDMlTfUg.mp3
├── Serial Experiment Lain -  Duvet (tv version)-EnEaNaqGMqU.mp3
├── Serial Experiment Lain -  Infanity world-AsPLBQfZQ04.mp3
├── Serial Experiment Lain - Invisible file-32IlMQ8Bs6w.mp3
├── Serial Experiment Lain - Island in video cassette-CCdyzUPrLpM.mp3
├── Serial Experiment Lain -  k.i.d.s-TvCJnW46ISo.mp3
├── Serial Experiment Lain - Prayer-yyPMWWjzMG8.mp3
├── Serial Experiment Lain - Professed intention and real-D4G97Xsc8PA.mp3
├── Serial Experiment Lain - Psychedelic farm-XEGz6CJnY04.mp3
├── Serial Experiment Lain - Speed-zFwQFdAsGGA.mp3
├── Serial Experiments Lain - Cyberia Theme-5dbi4N6NGn4.mp3
└── transitions
    ├── Autumn In Space-nWqcLWxZOH0.mp3
    ├── Circuit Lover-zduKG8YUKxM.mp3
    ├── Coloris-YTY7UV6GpkM.mp3
    ├── Destination Luna4-VGaQwDylJPM.mp3
    ├── Distort Into Me-_zLINqJYH0U.mp3
    ├── Fuse-MhHshcp5tQs.mp3
    ├── Gum-tTGBo0Fmf4A.mp3
    ├── In Time-8PmEsQY5rq4.mp3
    ├── Monochrome-uFNppANT-Dk.mp3
    ├── Orbit-SHFmUt1Jit4.mp3
    ├── Reality-C0ybGDNZos8.mp3
    ├── Together-ohViqbZZnpE.mp3
    ├── Tokyo Nights-5fXc8aoF2eU.mp3
    ├── Touch and Go-gxfyax1IPdk.mp3
    └── Traveling By Night-75yOApYoc6Y.mp3
	
root@lain:/srv/radio/music/cyberia# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press 2 and u to update the database, and you will see the transitions directory appear:

#and in it we have our transition audio tracks:

#so let's add them to the correct playlist album:
#select the tracks with INSERT
#then press a to add them to the Lainchan Radio Transitions playlist:

#press 5 to go to the playlists and you should see that result:

#now add a few more albums just like we did previously 
#so that schedule.py has a few albums to cycle through and to put transitions in between:

root@lain:/srv/radio/music/cyberia# youtube-dl -x --audio-format mp3 https://www.youtube.com/playlist?list=PL9AnroPjNnk0ukW3RvRQlbxA6nsNjG_9k
root@lain:/srv/radio/music/cyberia# youtube-dl -x --audio-format mp3 https://www.youtube.com/playlist?list=PLEBA2CD188C3A8278
root@lain:/srv/radio/music/cyberia# youtube-dl -x --audio-format mp3 https://www.youtube.com/playlist?list=PL4D3810FF245A180B

#Each time make sure you add them to separate albums with ncmpcpp just like we previously did to keep things clean.


So right now i have 3 albums (playlists) and a transition playlist, now let's use schedule.py:


root@lain:/srv/lainonlife/scripts# ls -l
total 16
-rwxr-xr-x 1 root root 2334 Jul 11 22:11 album_times.py
-rwxr-xr-x 1 root root  943 Jul 11 22:11 file-list.sh
-rwxr-xr-x 1 root root 4631 Jul 12 12:48 schedule.py
root@lain:/srv/lainonlife/scripts# python3 schedule.py --host=127.0.0.1 6601
Traceback (most recent call last):
  File "schedule.py", line 168, in <module>
    schedule_radio(client)
  File "schedule.py", line 114, in schedule_radio
    album, album_dur = pick_album(client, target_dur)
  File "schedule.py", line 68, in pick_album
    album = all_albums[0]
IndexError: list index out of range

Apologies on my part, for MPD playlists are NOT albums, so we need to create albums for each artist:

To do that we can add id3 tags:


root@lain:/srv/lainonlife/scripts# apt install libid3-tools -y
root@lain:/srv/lainonlife/scripts# which id3tag
/usr/bin/id3tag	

So with id3tag we're going to add the album tags to our mp3 files:


root@lain:/srv/radio/music# vim tagger.sh
	

Now before running it we're going to add a tag to one track and see if it works as intended:


cd cyberia/transitions/	
root@lain:/srv/radio/music/cyberia/transitions# ls -lash Coloris-YTY7UV6GpkM.mp3
2.5M -rw-r--r-- 1 root root 2.5M Sep 15  2019 Coloris-YTY7UV6GpkM.mp3

Now since this is a Transition track let's add the Lainchan Radio Transitions album tag to it:


root@lain:/srv/radio/music/cyberia/transitions# id3tag --album "Lainchan Radio Transitions" Coloris-YTY7UV6GpkM.mp3
+++ Album   = Lainchan Radio Transitions
Tagging Coloris-YTY7UV6GpkM.mp3: attempting v1 and v2, tagged v1 and v2
	

And when we check it inside of ncmpcpp we see the following after updating the database with u:


root@lain:/srv/radio/music/cyberia/transitions# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press u to update the database
#press 4 to goto the Media Library 

So right here we have been able to create an album thanks to the id3tag tool, now if you're a masochist you can go ahead and do that manually for every song, if you're like me and want to make things simpler, we can use a script. i place it into /srv/radio/music/ to tag the transition tracks:


root@lain:/srv/radio/music# vim tagger_transitions.sh
root@lain:/srv/radio/music# cat tagger_transitions.sh
#! /bin/bash

GREEN="\033[0;32m"
ORANGE="\033[0;33m"
RED="\033[0;31m"
NC="\033[0m"

echo -en "${GREEN}[+]${NC} Type the name of the channel directory (ex: cyberia): "
read channel
echo -en "${GREEN}[+]${NC} default album name: ${GREEN} Lainchan Radio Transitions"
album="Lainchan Radio Transitions"


#### First list the songs and ask if the user wants to add the album tag to them:

#change IFS
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")

cd $channel/transitions
SONGS=*

for SONG in $SONGS ;
do
        echo -en "\n ${ORANGE}[+] $SONG${NC}"
done
cd ../..

#restore IFS
IFS=$SAVEIFS



echo -en "\n\n${GREEN}[+]${NC} add the ${RED}$album${NC} ${ORANGE}album${NC} tag to these songs ? (y/n)"
read choice


####### If user says 'y' then add the album tag to the selected songs:

if [ "$choice" == "y" ];
then
        echo 'yes'

        #change IFS
        SAVEIFS=$IFS
        IFS=$(echo -en "\n\b")

        cd $channel/transitions
        SONGS=*

        for SONG in $SONGS ;
        do
                echo -en "\n ${GREEN}[+] $SONG${RED} ADDING TAG: ${ORANGE}$album${NC}"
                id3tag  --album="\"$album\"" $SONG
        done
        cd ../..

        #restore IFS
        IFS=$SAVEIFS
else
        echo 'cancelling...'
fi

echo
exit

So for me this script works, make sure you place it in /srv/radio/music/ otherwise it will not work as intended.


root@lain:/srv/radio/music# ./tagger_transitions.sh
[+] Type the name of the channel directory (ex: cyberia): cyberia
[+] default album name:  Lainchan Radio Transitions
 [+] Autumn In Space-nWqcLWxZOH0.mp3
 [+] Circuit Lover-zduKG8YUKxM.mp3
 [+] Coloris-YTY7UV6GpkM.mp3
 [+] Destination Luna4-VGaQwDylJPM.mp3
 [+] Distort Into Me-_zLINqJYH0U.mp3
 [+] Fuse-MhHshcp5tQs.mp3
 [+] Gum-tTGBo0Fmf4A.mp3
 [+] In Time-8PmEsQY5rq4.mp3
 [+] Monochrome-uFNppANT-Dk.mp3
 [+] Orbit-SHFmUt1Jit4.mp3
 [+] Reality-C0ybGDNZos8.mp3
 [+] Together-ohViqbZZnpE.mp3
 [+] Tokyo Nights-5fXc8aoF2eU.mp3
 [+] Touch and Go-gxfyax1IPdk.mp3
 [+] Traveling By Night-75yOApYoc6Y.mp3

[+] add the Lainchan Radio Transitions album tag to these songs ? (y/n)y
yes

 [+] Autumn In Space-nWqcLWxZOH0.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Autumn In Space-nWqcLWxZOH0.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Circuit Lover-zduKG8YUKxM.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Circuit Lover-zduKG8YUKxM.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Coloris-YTY7UV6GpkM.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Coloris-YTY7UV6GpkM.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Destination Luna4-VGaQwDylJPM.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Destination Luna4-VGaQwDylJPM.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Distort Into Me-_zLINqJYH0U.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Distort Into Me-_zLINqJYH0U.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Fuse-MhHshcp5tQs.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Fuse-MhHshcp5tQs.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Gum-tTGBo0Fmf4A.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Gum-tTGBo0Fmf4A.mp3: attempting v1 and v2, tagged v1 and v2

 [+] In Time-8PmEsQY5rq4.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging In Time-8PmEsQY5rq4.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Monochrome-uFNppANT-Dk.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Monochrome-uFNppANT-Dk.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Orbit-SHFmUt1Jit4.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Orbit-SHFmUt1Jit4.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Reality-C0ybGDNZos8.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Reality-C0ybGDNZos8.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Together-ohViqbZZnpE.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Together-ohViqbZZnpE.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Tokyo Nights-5fXc8aoF2eU.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Tokyo Nights-5fXc8aoF2eU.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Touch and Go-gxfyax1IPdk.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Touch and Go-gxfyax1IPdk.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Traveling By Night-75yOApYoc6Y.mp3 ADDING TAG: Lainchan Radio Transitions+++ Album   = "Lainchan Radio Transitions"
Tagging Traveling By Night-75yOApYoc6Y.mp3: attempting v1 and v2, tagged v1 and v2
	

Now to tag the mp3 files in /channel/songname.mp3 with their album names i made a different script in /srv/radio/music/tagger.sh, again make sure the location is correct otherwise it won't work either :


root@lain:/srv/radio/music# cat tagger.sh
#! /bin/bash

GREEN="\033[0;32m"
ORANGE="\033[0;33m"
RED="\033[0;31m"
NC="\033[0m"

echo -en "${GREEN}[+]${NC} Type the name of the channel directory (ex: cyberia): "
read channel
echo -en "${GREEN}[+]${NC} Type the name of the album: "
read album
echo -en "${GREEN}[+]${NC} Type the name of the regex to match the album: "
read regex


#### First list the songs and ask if the user wants to add the album tag to them:

#change IFS
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")

cd $channel
SONGS=*$regex*

for SONG in $SONGS ;
do
        echo -en "\n ${ORANGE}[+] $SONG${NC}"
done
cd ..

#restore IFS
IFS=$SAVEIFS



echo -en "\n\n${GREEN}[+]${NC} add the ${RED}$album${NC} ${ORANGE}album${NC} tag to these songs ? (y/n)"
read choice


####### If user says 'y' then add the album tag to the selected songs:

if [ "$choice" == "y" ];
then
        echo 'yes'

        #change IFS
        SAVEIFS=$IFS
        IFS=$(echo -en "\n\b")

        cd $channel
        SONGS=*$regex*

        for SONG in $SONGS ;
        do
                echo -en "\n ${GREEN}[+] $SONG${RED} ADDING TAG: ${ORANGE}$album${NC}"
                id3tag  --album="\"$album\"" $SONG
        done
        cd ..

        #restore IFS
        IFS=$SAVEIFS
else
        echo 'cancelling....'
fi

echo
exit
	

This time we specify the channel, the album, and the regex that we will use to match the files to add the tag to:


root@lain:/srv/radio/music# ./tagger.sh
[+] Type the name of the channel directory (ex: cyberia): cyberia
[+] Type the name of the album: Pendulum - Hold your Color^C
root@lain:/srv/radio/music# ./tagger.sh
[+] Type the name of the channel directory (ex: cyberia): cyberia
[+] Type the name of the album: Infected Mushroom - Army Of Mushrooms
[+] Type the name of the regex to match the album: Infected

 [+] Infected Mushroom - (01) Nevermind [HQ] 2012-Fzj7BZeqVzk.mp3
 [+] Infected Mushroom - (02) Nothing to Say [HQ] 2012-fQcB8yo46eI.mp3
 [+] Infected Mushroom - (03) Send Me an Angel [HQ] 2012-W2Ks5AkCHow.mp3
 [+] Infected Mushroom - (04) U R So Fucked [HQ] 2012-62gm17b2bMI.mp3
 [+] Infected Mushroom - (05) The Rat [HQ] 2012-nHIWgbBDx0o.mp3
 [+] Infected Mushroom - (06) Nation of Wusses [HQ] 2012-L25QxK0Csg0.mp3
 [+] Infected Mushroom - (07) Wanted To [HQ] 2012-2PaSS6JFvXc.mp3
 [+] Infected Mushroom - (08) Serve My Thirst [HQ] 2012-_J6feIrbcEo.mp3
 [+] Infected Mushroom (09) - I Shine [HQ] 2012-OtbFUedy6Xs.mp3
 [+] Infected Mushroom - (10) Drum n Baasa [HQ] 2012-4IhPuiuYx30.mp3
 [+] Infected Mushroom - (11) The Pretender (Foo Fighters Cover) [HQ] 2012-boKAyiI-8WQ.mp3
 [+] Infected Mushroom - (12) The Messenger 2012 [HQ] 2012-x0lRqrID3i4.mp3
 [+] Infected Mushroom - (13) Swingish (Bonus Track) [HQ] 2012-7KwBE6XJbj4.mp3

[+] add the Infected Mushroom - Army Of Mushrooms album tag to these songs ? (y/n)y
yes

 [+] Infected Mushroom - (01) Nevermind [HQ] 2012-Fzj7BZeqVzk.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (01) Nevermind [HQ] 2012-Fzj7BZeqVzk.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (02) Nothing to Say [HQ] 2012-fQcB8yo46eI.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (02) Nothing to Say [HQ] 2012-fQcB8yo46eI.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (03) Send Me an Angel [HQ] 2012-W2Ks5AkCHow.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (03) Send Me an Angel [HQ] 2012-W2Ks5AkCHow.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (04) U R So Fucked [HQ] 2012-62gm17b2bMI.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (04) U R So Fucked [HQ] 2012-62gm17b2bMI.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (05) The Rat [HQ] 2012-nHIWgbBDx0o.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (05) The Rat [HQ] 2012-nHIWgbBDx0o.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (06) Nation of Wusses [HQ] 2012-L25QxK0Csg0.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (06) Nation of Wusses [HQ] 2012-L25QxK0Csg0.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (07) Wanted To [HQ] 2012-2PaSS6JFvXc.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (07) Wanted To [HQ] 2012-2PaSS6JFvXc.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (08) Serve My Thirst [HQ] 2012-_J6feIrbcEo.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (08) Serve My Thirst [HQ] 2012-_J6feIrbcEo.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom (09) - I Shine [HQ] 2012-OtbFUedy6Xs.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom (09) - I Shine [HQ] 2012-OtbFUedy6Xs.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (10) Drum n Baasa [HQ] 2012-4IhPuiuYx30.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (10) Drum n Baasa [HQ] 2012-4IhPuiuYx30.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (11) The Pretender (Foo Fighters Cover) [HQ] 2012-boKAyiI-8WQ.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (11) The Pretender (Foo Fighters Cover) [HQ] 2012-boKAyiI-8WQ.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (12) The Messenger 2012 [HQ] 2012-x0lRqrID3i4.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (12) The Messenger 2012 [HQ] 2012-x0lRqrID3i4.mp3: attempting v1 and v2, tagged v1 and v2

 [+] Infected Mushroom - (13) Swingish (Bonus Track) [HQ] 2012-7KwBE6XJbj4.mp3 ADDING TAG: Infected Mushroom - Army Of Mushrooms+++ Album   = "Infected Mushroom - Army Of Mushrooms"
Tagging Infected Mushroom - (13) Swingish (Bonus Track) [HQ] 2012-7KwBE6XJbj4.mp3: attempting v1 and v2, tagged v1 and v2
	

After adding the other albums we can check the result in ncmpcpp:


root@lain:/srv/radio/music# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press 2 to get to the browsing tab
#press u to update the database
#see the album tags appear:

#press 4 to get to the media library:


We also fix the missing fonts in /srv/http/fonts/



127.0.0.1 - - [12/Jul/2021 14:55:14] "GET /fonts/fontawesome-webfont.woff2?v=4.7.0 HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 14:55:16] "GET /fonts/fontawesome-webfont.woff?v=4.7.0 HTTP/1.0" 200 -
127.0.0.1 - - [12/Jul/2021 14:55:17] "GET /fonts/fontawesome-webfont.ttf?v=4.7.0 HTTP/1.0" 304 -
	
root@lain:/srv/http# mkdir fonts
root@lain:/srv/http# cd fonts
root@lain:/srv/http/fonts# wget https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.woff2
root@lain:/srv/http/fonts# wget https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/fonts/fontawesome-webfont.ttf

Now the fonts are fixed once we check them in the browser:

first edit : index.html had broken links to the radio:



root@lain:/srv/http# cat index.html  | grep void
    lain.void.yt 
const ICECAST_STREAM_URL_BASE = "https://lain.void.yt/radio/";
            <source src="https://lain.void.yt//cyberia.ogg" type="audio/ogg"/>
            <source src="https://lain.void.yt//cyberia.mp3" type="audio/mpeg"/>
          <a id="ogglink" href="https://lain.void.yt//cyberia.ogg.m3u">ogg</a> /
          <a id="mp3link" href="https://lain.void.yt//cyberia.mp3.m3u">mp3</a>
	
root@lain:/srv/http# vim index.html
root@lain:/srv/http# cat index.html  | grep void
    lain.void.yt 
const ICECAST_STREAM_URL_BASE = "https://lain.void.yt/radio/";
            <source src="https://lain.void.yt/radio/cyberia.ogg" type="audio/ogg"/>
            <source src="https://lain.void.yt/radio/cyberia.mp3" type="audio/mpeg"/>
          <a id="ogglink" href="https://lain.void.yt/radio/cyberia.ogg.m3u">ogg</a> /
          <a id="mp3link" href="https://lain.void.yt/radio/cyberia.mp3.m3u">mp3</a>

Now the radio actually plays from the browser :

However the title of each track are still missing.

EDIT 2: figured out how to go around the schedule.py errors


root@lain:/srv/lainonlife/scripts# cat schedule.py
#!/usr/bin/env python3

"""Radio scheduling program.

Usage:
  schedule.py [--host=HOST] PORT

Options:
  --host=HOST  Hostname of MPD [default: localhost]
  -h --help    Show this text

Takes a port number, and does the following:

1. Sets the play order to normal, looping.
2. Clears all music before the cursor position in the playlist.
3. Appends a roughly three-hour block of music to the end of the playlist in this format:
   a. A transition track
   b. A full album
   c. Enough random tracks to make up the difference.

The new segment may be a little shorter if an exact fit is not possible, but in practice it will
be close.

Why do this?  Listening to entire albums in order is nice, as tracks in an album often build off
each other.  On the other hand, variety is also nice!

"""

from docopt import docopt
from mpd import MPDClient
from random import shuffle


def duration_of(filterty, filterval):
    """Get the combined duration of all tracks matching a filter."""

    return int(client.count(filterty, filterval).get("playtime", "0"))


def pick_transition(client):
    """Picks a transition track."""

    all_transitions = list(
        filter(lambda t: "directory" not in t, client.listall("transitions"))
    )

    shuffle(all_transitions)

    transition = all_transitions[0]["file"]
    transition_dur = duration_of("file", transition)

    return transition, transition_dur


def pick_album(client, dur):
    """Picks a random album which fits in the duration."""

    # Get all albums
    all_albums = [
        a["album"]
        for a in client.list("album")
        if "album" in a and a["album"] not in ["", "Lainchan Radio Transitions"]
    ]

    for a in client.list("album"):
        print(a)
        all_albums.append(a)

    print(all_albums)
    shuffle(all_albums)

    album = all_albums[0]
    album_dur = duration_of("album", album)
    return album, album_dur


def pick_tracks(client, chosen_album, dur):
    """Attempts to pick a list of tracks to fill the given time.

    Radio transitions and the chosen album are excluded from the list.

    Because retrieving and operating over the list of all tracks is expensive, this does not try
    more than once.  It uses the simple greedy algorithm, and so may exceed the limit.
    """

    all_tracks = []
    for t in client.list("file"):
            #print(t)
            all_tracks.append(t)
    #print(all_tracks)
    #all_tracks = [
    #    t["file"]
    #    for t in client.list("file")
    #    if "file" in t
    #]

    shuffle(all_tracks)

    chosen = []
    remaining = dur
    for t in all_tracks:
        album = client.list("album", "file", t)[0]
        duration = duration_of("file", t)
        if album in [chosen_album, "Lainchan Radio Transitions"]:
            continue
        if duration > remaining:
            continue
        chosen.append(t)
        remaining = remaining - duration

    return chosen, dur - remaining


def schedule_radio(client, target_dur= 3 * 60 * 60):
    """Schedule music.

    Keyword arguments:
    target_dur -- the target duration to fill.
    """

    # Pick a transition and two albums
    transition, transition_dur = pick_transition(client)
    album, album_dur = pick_album(client, target_dur)

    # Determine how much time is remaining in the two-hour slot
    time_remaining = target_dur - transition_dur - album_dur

    # Pick a list of tracks to fill the gap
    tracks, tracks_dur = pick_tracks(client, album, time_remaining)

    # Some stats first
    print("Transition: {} ({}s)".format(transition, transition_dur))
    print("Album: {} ({}s)".format(album, album_dur))
    print("Tracks: #{} ({}s)".format(len(tracks), tracks_dur))
    print()
    print("Target duration: {}s".format(target_dur))
    print("Actual duration: {}s".format(transition_dur + album_dur + tracks_dur))
    print("Difference: {}s".format(time_remaining - tracks_dur))

    # Set playback to in-order repeat
    client.random(0)
    client.repeat(1)

    # Make sure it's playing
    client.play()

    # Add tracks
    client.add(transition)
    client.findadd("album", album)
    for t in tracks:
        client.add(t)

    # Delete up to the current track, minus 10 tracks (for the web
    # playlist)
    status = client.status()
    if "song" in status:
        client.delete((0, max(0, int(status["song"]) - 10)))


if __name__ == "__main__":
    args = docopt(__doc__)

    try:
        args["PORT"] = int(args["PORT"])
    except ValueError:
        print("PORT must be an integer")
        exit(1)

    try:
        client = MPDClient()
        client.connect(args["--host"], args["PORT"])
    except Exception as e:
        print(f"could not connect to MPD: {e.args[0]}")
        exit(2)

    client.update()
    schedule_radio(client)
	

Editing schedule.py to fix the python errors now it can be used:


root@lain:/srv/lainonlife/scripts# python3 schedule.py --host=127.0.0.1 6601
"Cyberia"
"Infected Mushroom - Army Of Mushrooms"
"Lainchan Radio Transitions"
"Pendulum"
['"Cyberia"', '"Infected Mushroom - Army Of Mushrooms"', '"Lainchan Radio Transitions"', '"Pendulum"']
Transition: transitions/Destination Luna4-VGaQwDylJPM.mp3 (142s)
Album: "Pendulum" (8631s)
Tracks: #0 (0s)

Target duration: 1800s
Actual duration: 8773s
Difference: -6973s

Now another guess for the 'undefined' song names problem, i assume that it's the missing tag titles, so let's see if that's the case:


root@lain:/srv/radio/music/cyberia# id3tag --song "Pendulum - The Tempest" The\ Tempest\ -\ Pendulum\ \[HQ\]-uy-3tWaKyg8.mp3
+++ Song    = Pendulum - The Tempest
Tagging The Tempest - Pendulum [HQ]-uy-3tWaKyg8.mp3: attempting v1 and v2, tagged v1 and v2

root@lain:/srv/radio/music/cyberia# id3tag --song "Pendulum - The Other Side" Pendulum\ \- The\ Other\ Side\ -\ Pendulum\ \[HQ\]-z0hwFJ1rbbU.mp3
+++ Song    = Pendulum - The Other Side

root@lain:/srv/radio/music/cyberia# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press u to update database
	

Looks like we were right! What we were missing here was the song/title tags for each mp3 file. so let's make a script to automatically add all of them:


root@lain:/srv/radio/music# vim tagger_titles.sh
root@lain:/srv/radio/music# cat tagger_titles.sh
#! /bin/bash

GREEN="\033[0;32m"
ORANGE="\033[0;33m"
RED="\033[0;31m"
NC="\033[0m"

echo -en "${GREEN}[+]${NC} Type the name of the channel directory (ex: cyberia): "
read channel

#### First list the songs and ask if the user wants to add the album tag to them:

#change IFS
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")

cd $channel
SONGS=*

for SONG in $SONGS ;
do
        echo -en "\n ${ORANGE}[+] $SONG${NC}"
done
cd ..

#restore IFS
IFS=$SAVEIFS



echo -en "\n\n${GREEN}[+]${NC} add the filename as ${ORANGE}title${NC} tag to these songs ? (y/n)"
read choice


####### If user says 'y' then add the album tag to the selected songs:

if [ "$choice" == "y" ];
then
        echo 'yes'

        #change IFS
        SAVEIFS=$IFS
        IFS=$(echo -en "\n\b")

        cd $channel
        SONGS=*

        for SONG in $SONGS ;
        do
                echo -en "\n ${GREEN}[+] $SONG${RED} ADDING TITLE TAG: ${ORANGE}$SONG${NC}"
                id3tag  --song="\"$SONG\"" $SONG
        done
        cd ..

        #restore IFS
        IFS=$SAVEIFS
else
        echo 'cancelling....'
fi

echo
exit

Now that's done we update the database with ncmpcpp as usual:


root@lain:/srv/radio/music# ncmpcpp -h 127.0.0.1 -p 6601 -c ~/.config/ncmpcpp/config
#press u to update the database	

And there you go! We managed to make each file's name display.

Automation



So now that we know our webserver is working as intended, let's write systemd services to handle automatically mpd, icecast and the run.sh bashscript:



root@lain:~# icecast -c /srv/Icecast-Server/conf/icecast.xml	
root@lain:~# CONFIG=../config.json HTTP_DIR=/srv/http PORT=8002 ./run.sh
root@lain:~# mpd ~/.config/mpd.conf

In essence we need a systemd service to make the root user run these 3 commands so let's make them one by one. First the icecast systemd service:


root@lain:/srv/radio/music# cd /etc/systemd/system/

root@lain:/etc/systemd/system# vim icecast.service

root@lain:/etc/systemd/system# cat icecast.service
[Unit]
Description=Icecast systemd service with config in /srv/Icecast-Server/conf/icecast.xml
After=network.target
After=systemd-user-sessions.service
After=network-online.target

[Service]
Type=simple
ExecStart=icecast -c /srv/Icecast-Server/conf/icecast.xml
Restart=always
RestartSec=10
KillMode=process

[Install]
WantedBy=multi-user.target
root@lain:/etc/systemd/system# systemctl daemon-reload

root@lain:/etc/systemd/system# systemctl enable --now icecast
Created symlink /etc/systemd/system/multi-user.target.wants/icecast.service → /etc/systemd/system/icecast.service.
root@lain:/etc/systemd/system# systemctl status icecast
● icecast.service - Icecast systemd service with config in /srv/Icecast-Server/conf/icecast.xml
   Loaded: loaded (/etc/systemd/system/icecast.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-07-12 22:52:27 CEST; 5s ago
 Main PID: 10092 (icecast)
    Tasks: 4 (limit: 4700)
   Memory: 1.0M
   CGroup: /system.slice/icecast.service
           └─10092 /usr/local/bin/icecast -c /srv/Icecast-Server/conf/icecast.xml

Jul 12 22:52:27 lain systemd[1]: Started Icecast systemd service with config in /srv/Icecast-Server/conf/icecast.xml.
Jul 12 22:52:27 lain icecast[10092]: [2021-07-12  22:52:27] WARN CONFIG/_parse_root Warning, <location> not configured, using default value "Earth".
Jul 12 22:52:27 lain icecast[10092]: [2021-07-12  22:52:27] WARN CONFIG/_parse_root Warning, <admin> contact not configured, using default value "icemaster@localhost". 

We do the same for the backend script run.sh used to run the radio itself:



root@lain:/etc/systemd/system# vim lainradio.service
root@lain:/etc/systemd/system# cat lainradio.service

[Unit]
Description=running the run.sh script with the correct arguements
After=network.target
After=systemd-user-sessions.service
After=network-online.target

[Service]
Type=simple
WorkingDirectory=/srv/lainonlife/backend/
Environment="CONFIG=../config.json"
Environment="HTTP_DIR=/srv/http"
Environment="PORT=8002"
ExecStart=/srv/lainonlife/backend/run.sh
Restart=always
RestartSec=10
KillMode=process

[Install]
WantedBy=multi-user.target

root@lain:/etc/systemd/system# systemctl daemon-reload
root@lain:/etc/systemd/system# systemctl enable --now lainradio
root@lain:/etc/systemd/system# systemctl status lainradio
● lainradio.service - running the run.sh script with the correct arguements
   Loaded: loaded (/etc/systemd/system/lainradio.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-07-12 23:03:15 CEST; 2min 47s ago
 Main PID: 10345 (bash)
    Tasks: 13 (limit: 4700)
   Memory: 30.8M
   CGroup: /system.slice/lainradio.service
           ├─10345 bash /srv/lainonlife/backend/run.sh
           └─10354 python3 backend.py

Jul 12 23:04:58 lain run.sh[10345]: 127.0.0.1 - - [12/Jul/2021 23:04:58] "GET /playlist/cyberia.json HTTP/1.0" 200 -
Jul 12 23:04:58 lain run.sh[10345]: 127.0.0.1 - - [12/Jul/2021 23:04:58] "GET /playlist/cyberia.json HTTP/1.0" 200 -
Jul 12 23:05:13 lain run.sh[10345]: 127.0.0.1 - - [12/Jul/2021 23:05:13] "GET /playlist/cyberia.json HTTP/1.0" 200 -
Jul 12 23:05:13 lain run.sh[10345]: 127.0.0.1 - - [12/Jul/2021 23:05:13] "GET /playlist/cyberia.json HTTP/1.0" 200 -
Jul 12 23:05:28 lain run.sh[10345]: 127.0.0.1 - - [12/Jul/2021 23:05:28] "GET /playlist/cyberia.json HTTP/1.0" 200 -

Now that's done we're going to setup the systemd service for mpd, now i intend to make a multiple-channel setup so i will make mpd use the config file:


root@lain:/etc/systemd/system# vim mpd-cyberia.service
root@lain:/etc/systemd/system# cat mpd-cyberia.service

[Unit]
Description=mpd systemd service with config in /root/.config/mpd/cyberia.conf
After=network.target
After=systemd-user-sessions.service
After=network-online.target

[Service]
Type=simple
ExecStart=mpd --no-daemon /root/.config/mpd/cyberia.conf
Restart=always
RestartSec=10
KillMode=process

[Install]
WantedBy=multi-user.target

root@lain:/etc/systemd/system# systemctl daemon-reload
root@lain:/etc/systemd/system#

root@lain:/etc/systemd/system# cd ~/.config/mpd/
root@lain:~/.config/mpd# ls -l
total 4
-rw-r--r-- 1 root root 1773 Jul 12 09:32 mpd.conf

root@lain:~/.config/mpd# cp mpd.conf cyberia.conf

root@lain:~/.config/mpd# cd -
/etc/systemd/system

root@lain:/etc/systemd/system# systemctl disable mpd
root@lain:/etc/systemd/system# kill $(pidof mpd)
root@lain:/etc/systemd/system# kill $(pidof mpd)
root@lain:/etc/systemd/system# kill $(pidof mpd)
root@lain:/etc/systemd/system# systemctl enable --now mpd-cyberia

root@lain:/etc/systemd/system# systemctl status mpd-cyberia.service
● mpd-cyberia.service - mpd systemd service with config in /root/.config/mpd/cyberia.conf
   Loaded: loaded (/etc/systemd/system/mpd-cyberia.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2021-07-12 23:20:47 CEST; 3min 13s ago
 Main PID: 10921 (mpd)
    Tasks: 10 (limit: 4700)
   Memory: 15.7M
   CGroup: /system.slice/mpd-cyberia.service
           └─10921 /usr/bin/mpd --no-daemon /root/.config/mpd/cyberia.conf

Jul 12 23:20:47 lain systemd[1]: Started mpd systemd service with config in /root/.config/mpd/cyberia.conf.
Jul 12 23:20:48 lain mpd[10921]: Jul 12 23:20 : hybrid_dsd: The Hybrid DSD decoder is disabled because it was not explicitly enabled
Jul 12 23:20:48 lain mpd[10921]: Jul 12 23:20 : exception: Input plugin 'tidal' is unavailable: No Tidal application token configured
Jul 12 23:20:48 lain mpd[10921]: Jul 12 23:20 : exception: Input plugin 'qobuz' is unavailable: No Qobuz app_id configured

And that's it! We managed to create 3 independent systemd services that will run everytime the host boots up, ensuring that the radio stays up and running from the start automatically. Now that should already be done but make sure that nginx and prometheus' systemd services are enabled too:


root@lain:/etc/systemd/system# systemctl enable --now nginx
root@lain:/etc/systemd/system# systemctl enable --now prometheus

Multi-Channel + Album Ordering Setup



Before adding channels let's get a more organized music folder architecture:

Basically, for each channel, we're going to have a folder for each album, and inside of each album folder we're going to have our song files. And also let's not forget that we have our transition files in the transitions folder, we want to tag them correctly with Lainchan Radio Transitions special album name. So from here we proceed:

First i move all of my songs into folders:


  mkdir "Serial Experiments Lain OST"
  mkdir "Infected Mushroom - Army of Mushrooms"
  mkdir "Pendulum - In Silico"

  mv Infec* Infected\ Mushroom\ -\ Army\ of\ Mushrooms/
  mv Serial* Serial\ Experiments\ Lain\ OST/
  mv *Pendulum* Pendulum\ -\ In\ Silico/
  mv *pendulum* Pendulum\ -\ In\ Silico/
  mv Lain\'s\ Theme\ -\ Lain-o-cwuTmqz8c.mp3 Serial\ Experiments\ Lain\ OST/

root@lain:/srv/radio/music/cyberia# ls -lash
total 24K
4.0K drwxr-xr-x 6 root root 4.0K Jul 13 09:48  .
4.0K drwxr-xr-x 3 root root 4.0K Jul 13 09:44  ..
4.0K drwxr-xr-x 2 root root 4.0K Jul 13 09:47 'Infected Mushroom - Army of Mushrooms'
4.0K drwxr-xr-x 2 root root 4.0K Jul 13 09:48 'Pendulum - In Silico'
4.0K drwxr-xr-x 2 root root 4.0K Jul 13 09:48 'Serial Experiments Lain OST'
4.0K drwxr-xr-x 2 root root 4.0K Jul 13 09:11  transitions

root@lain:/srv/radio/music/cyberia# tree .
.
├── Infected Mushroom - Army of Mushrooms
│   ├── Infected Mushroom - (01) Nevermind [HQ] 2012-Fzj7BZeqVzk.mp3
│   ├── Infected Mushroom - (02) Nothing to Say [HQ] 2012-fQcB8yo46eI.mp3
│   ├── Infected Mushroom - (03) Send Me an Angel [HQ] 2012-W2Ks5AkCHow.mp3
│   ├── Infected Mushroom - (04) U R So Fucked [HQ] 2012-62gm17b2bMI.mp3
│   ├── Infected Mushroom - (05) The Rat [HQ] 2012-nHIWgbBDx0o.mp3
│   ├── Infected Mushroom - (06) Nation of Wusses [HQ] 2012-L25QxK0Csg0.mp3
│   ├── Infected Mushroom - (07) Wanted To [HQ] 2012-2PaSS6JFvXc.mp3
│   ├── Infected Mushroom - (08) Serve My Thirst [HQ] 2012-_J6feIrbcEo.mp3
│   ├── Infected Mushroom (09) - I Shine [HQ] 2012-OtbFUedy6Xs.mp3
│   ├── Infected Mushroom - (10) Drum n Baasa [HQ] 2012-4IhPuiuYx30.mp3
│   ├── Infected Mushroom - (11) The Pretender (Foo Fighters Cover) [HQ] 2012-boKAyiI-8WQ.mp3
│   ├── Infected Mushroom - (12) The Messenger 2012 [HQ] 2012-x0lRqrID3i4.mp3
│   └── Infected Mushroom - (13) Swingish (Bonus Track) [HQ] 2012-7KwBE6XJbj4.mp3
├── Pendulum - In Silico
│   ├── 9,000 Miles - Pendulum [HQ]-9AudFoAMTik.mp3
│   ├── Different - Pendulum [HQ]-3JUwQglYFig.mp3
│   ├── Granite - Pendulum [HQ]-h17WaF6alVY.mp3
│   ├── Midnight Runner - Pendulum [HQ]-oSbop6jmBS4.mp3
│   ├── Mutiny - Pendulum [HQ]-ucchDCKCTTs.mp3
│   ├── pendulum axle grinder-UiGJbBlsrqE.mp3
│   ├── pendulum out here-SM3fWfBgyJk.mp3
│   ├── pendulum-prelude-PYUHFPqI3Rw.mp3
│   ├── Propane Nightmares - Pendulum [HQ]-SBohx23x0jw.mp3
│   ├── Showdown - Pendulum [HQ]-mszCJSB02_8.mp3
│   ├── The Other Side - Pendulum [HQ]-z0hwFJ1rbbU.mp3
│   ├── The Tempest - Pendulum [HQ]-uy-3tWaKyg8.mp3
│   └── Visions - Pendulum [HQ]-HXSfB-zQUAg.mp3
├── Serial Experiments Lain OST
│   ├── Lain's Theme - Lain-o-cwuTmqz8c.mp3
│   ├── Serial Experiment Lain - Antidepressant 044-KaOsmUDMdaE.mp3
│   ├── Serial Experiment Lain - Cloudy with occasional rain-1TunrW7dRr0.mp3
│   ├── Serial Experiment Lain - Duvet (cyberia remix)-juDqDMlTfUg.mp3
│   ├── Serial Experiment Lain -  Duvet (tv version)-EnEaNaqGMqU.mp3
│   ├── Serial Experiment Lain -  Infanity world-AsPLBQfZQ04.mp3
│   ├── Serial Experiment Lain - Invisible file-32IlMQ8Bs6w.mp3
│   ├── Serial Experiment Lain - Island in video cassette-CCdyzUPrLpM.mp3
│   ├── Serial Experiment Lain -  k.i.d.s-TvCJnW46ISo.mp3
│   ├── Serial Experiment Lain - Prayer-yyPMWWjzMG8.mp3
│   ├── Serial Experiment Lain - Professed intention and real-D4G97Xsc8PA.mp3
│   ├── Serial Experiment Lain - Psychedelic farm-XEGz6CJnY04.mp3
│   ├── Serial Experiment Lain - Speed-zFwQFdAsGGA.mp3
│   └── Serial Experiments Lain - Cyberia Theme-5dbi4N6NGn4.mp3
└── transitions
    ├── Autumn
    ├── Autumn In Space-nWqcLWxZOH0.mp3
    ├── Circuit
    ├── Circuit Lover-zduKG8YUKxM.mp3
    ├── Coloris-YTY7UV6GpkM.mp3
    ├── Destination
    ├── Destination Luna4-VGaQwDylJPM.mp3
    ├── Distort
    ├── Distort Into Me-_zLINqJYH0U.mp3
    ├── Fuse-MhHshcp5tQs.mp3
    ├── Gum-tTGBo0Fmf4A.mp3
    ├── In
    ├── In Time-8PmEsQY5rq4.mp3
    ├── Monochrome-uFNppANT-Dk.mp3
    ├── Orbit-SHFmUt1Jit4.mp3
    ├── Reality-C0ybGDNZos8.mp3
    ├── Together-ohViqbZZnpE.mp3
    ├── Tokyo
    ├── Tokyo Nights-5fXc8aoF2eU.mp3
    ├── Touch
    ├── Touch and Go-gxfyax1IPdk.mp3
    ├── Traveling
    └── Traveling By Night-75yOApYoc6Y.mp3
	

And now that's done we're going to change our tagger script as follows:


root@lain:/srv/radio/music# vim tagger.sh
root@lain:/srv/radio/music# cat tagger.sh
#! /bin/bash

GREEN="\033[0;32m"
ORANGE="\033[0;33m"
RED="\033[0;31m"
NC="\033[0m"

echo -en "${GREEN}[+]${NC} Type the name of the channel directory (ex: cyberia): "
read channel


#### First list the songs and ask if the user wants to add the album tag to them:

#change IFS
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")


ALBUMS="*"
cd $channel
for ALBUM in $ALBUMS ;
do
                echo -en "\n ${ORANGE}[+] $ALBUM${NC}"
                cd $ALBUM
                SONGS="*"
                for SONG in $SONGS;
                do
                        echo -en "\n ${GREEN} [+] $SONG${NC}"
                done
                cd ..
done
cd ..

#restore IFS
IFS=$SAVEIFS


echo -en "\n\n${GREEN}[+]${NC} add the ${RED}$album${NC} ${ORANGE}album${NC} tag to these ${GREEN}songs ${NC}? (y/n)"
read choice






####### If user says 'y' then add the album tag to the selected songs:
if [ "$choice" == "y" ];
then
        echo 'yes'
        #change IFS
        SAVEIFS=$IFS
        IFS=$(echo -en "\n\b")


        ALBUMS="*"
        cd $channel
        for ALBUM in $ALBUMS ;
        do
                echo -en "\n ${ORANGE}[+] $ALBUM${NC}"
                cd $ALBUM
                SONGS="*"
                for SONG in $SONGS;
                do
                        echo -en "\n ${GREEN} [+] $SONG${NC}"
                        if [ $ALBUM == "transitions" ];
                        then
                                id3tag  --album="Lainchan Radio Transitions" $SONG
                                id3tag  --song="\"$SONG\"" $SONG
                        else
                                id3tag  --album="\"$ALBUM\"" $SONG
                                id3tag  --song="\"$SONG\"" $SONG

                        fi
                done
                cd ..
        done
        cd ..
        #restore IFS
        IFS=$SAVEIFS

else
        echo 'cancelling....'
fi


echo
exit
	

Basically, this script goes into the channel/ directory, then goes into each album/ directory, and then will add the tags as follows: the filename.mp3 becomes the song's id3tag, and the song's album/ directory becomes the song's album id3tag. Which fits how we want our service to function. Once that's done we simply run the schedule.py script to schedule our songs once again after updating the database:


root@lain:/srv/radio/music# ./tagger.sh

root@lain:/srv/radio/music# ncmpcpp -h 127.0.0.1 -p 6601
#press u to update database
#press q to quit

root@lain:/srv/radio/music# python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6601
"Infected Mushroom - Army of Mushrooms"
"Pendulum - In Silico"
"Serial Experiments Lain OST"
Lainchan Radio Transitions
['"Infected Mushroom - Army of Mushrooms"', '"Pendulum - In Silico"', '"Serial Experiments Lain OST"', 'Lainchan Radio Transitions']
Transition: transitions/In Time-8PmEsQY5rq4.mp3 (129s)
Album: "Serial Experiments Lain OST" (4057s)
Tracks: #18 (6581s)

Target duration: 10800s
Actual duration: 10767s
Difference: 33s

Now another thing we can do is run the schedule.py script automatically every 3 hours so that our songs are continuously being updated, we can do that with a cronjob:


root@lain:/srv/radio/music# crontab -e
no crontab for root - using an empty one

Select an editor.  To change later, run 'select-editor'.
  1. /bin/nano        ---- easiest
  2. /usr/bin/vim.basic
  3. /usr/bin/vim.tiny

Choose 1-3 [1]: 2


# m h  dom mon dow   command
0 */3 * * * python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6601

:wq
	

Now in order to test our cronjob we can use cronitor:


root@lain:/srv/radio/music# curl -sOL https://cronitor.io/dl/cronitor-stable-linux-amd64.tgz
root@lain:/srv/radio/music# tar xvf cronitor-stable-linux-amd64.tgz -C /usr/bin/
cronitor
root@lain:/srv/radio/music#  cronitor configure --api-key 1234567890

Configuration File:
/etc/cronitor/cronitor.json

Version:
27.0

API Key:
1234567890

Ping API Key:
Not Set

Environment:
Not Set

Hostname:
lain

Timezone Location:
{Europe/Paris}

Debug Log:
Off
root@lain:/srv/radio/music#  cronitor list

----► Checking user "root" crontab
+-------------------+------------------------------------------------------------------------------------------------------+
| SCHEDULE          | COMMAND                                                                                              |
+-------------------+------------------------------------------------------------------------------------------------------+
| 0 */3 * * *       | python3                                                                                              |
|                   | /srv/lainonlife/scripts/schedule.py                                                                  |
|                   | --host=127.0.0.1 6601                                                                                |
+-------------------+------------------------------------------------------------------------------------------------------+

----► Checking /etc/crontab
+-------------------+------------------------------------------------------------------------------------------------------+
| SCHEDULE          | COMMAND                                                                                              |
+-------------------+------------------------------------------------------------------------------------------------------+
| 17 * * * *        | cd / && run-parts --report                                                                           |
|                   | /etc/cron.hourly                                                                                     |
| 25 6 * * *        | test -x /usr/sbin/anacron ||                                                                         |
|                   | ( cd / && run-parts --report                                                                         |
|                   | /etc/cron.daily )                                                                                    |
| 47 6 * * 7        | test -x /usr/sbin/anacron ||                                                                         |
|                   | ( cd / && run-parts --report                                                                         |
|                   | /etc/cron.weekly )                                                                                   |
| 52 6 1 * *        | test -x /usr/sbin/anacron ||                                                                         |
|                   | ( cd / && run-parts --report                                                                         |
|                   | /etc/cron.monthly )                                                                                  |
+-------------------+------------------------------------------------------------------------------------------------------+

	

Now to test the cronjob we run cronitor select


root@lain:/srv/radio/music# cronitor select

✔ python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6601
----► Running command: python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6601

"Infected Mushroom - Army of Mushrooms"
"Pendulum - In Silico"
"Serial Experiments Lain OST"
Lainchan Radio Transitions
['"Infected Mushroom - Army of Mushrooms"', '"Pendulum - In Silico"', '"Serial Experiments Lain OST"', 'Lainchan Radio Transitions']
Transition: transitions/Together-ohViqbZZnpE.mp3 (144s)
Album: "Serial Experiments Lain OST" (4057s)
Tracks: #19 (6506s)

Target duration: 10800s
Actual duration: 10707s
Difference: 93s

----► ✔ Command successful    Elapsed time 0.223s

root@lain:/srv/radio/music#
	

And here we see that we have been able to successfully run the script through our cronjob, and the way we set it up makes it so schedule.py is being ran every 3 hours, that way it matches the 3 hours of audio it tries to queue. Now let's add another channel. To do that we need 3 things, a channel directory in /srv/radio/music/, another mpd service running on another port (i'll do ports 6601, 6602, 6603, 6604, etc) and to edit the website configuration config.json to let the clients choose which channel they want to listen to:


root@lain:/srv/radio/music# mkdir metal
root@lain:/srv/radio/music# cd metal
root@lain:/srv/radio/music/metal# mkdir "Rivers of Nihil - Where Owls Know My Name"
root@lain:/srv/radio/music/metal# mkdir "Behemoth - The Satanist"
root@lain:/srv/radio/music/metal# mkdir "Carnifex - Slow Death"
root@lain:/srv/radio/music/metal# mkdir "Rivers of Nihil - Monarchy"
root@lain:/srv/radio/music/metal# cd Rivers\ of\ Nihil\ -\ Where\ Owls\ Know\ My\ Name/
root@lain:/srv/radio/music/metal/Rivers of Nihil - Where Owls Know My Name#
root@lain:/srv/radio/music/metal/Rivers of Nihil - Where Owls Know My Name# youtube-dl https://www.youtube.com/playlist?list=PLEouLkiLHdSDIqsVoq7a9bJDifla6XNq9 -x --audio-format mp3
[youtube:tab] PLEouLkiLHdSDIqsVoq7a9bJDifla6XNq9: Downloading webpage
[download] Downloading playlist: Rivers Of Nihil - Where Owls Know My Name
[youtube:tab] playlist Rivers Of Nihil - Where Owls Know My Name: Downloading 10 videos

Now like this you can fill in each of your album folders just like we did for the cyberia channel, don't forget the transitions either:


root@lain:/srv/radio/music/metal# mkdir transitions
root@lain:/srv/radio/music/metal# youtube-dl LINK -x --audio-format mp3
	

once you filled up the channel directory with album directories, run the tagger:


root@lain:/srv/radio/music# ls -l metal/
total 40
drwxr-xr-x 2 root root 4096 Jul 13 11:34 'Behemoth - I Loved You At Your Darkest'
drwxr-xr-x 2 root root 4096 Jul 13 11:11 'Behemoth - The Satanist'
drwxr-xr-x 2 root root 4096 Jul 13 11:09 'Carnifex - Slow Death'
drwxr-xr-x 2 root root 4096 Jul 13 11:22 'Meshuggah - Obzen'
drwxr-xr-x 2 root root 4096 Jul 13 11:24 'Meshuggah - The Violent Sleep of Reason'
drwxr-xr-x 2 root root 4096 Jul 13 11:07 'Rivers of Nihil - Monarchy'
drwxr-xr-x 2 root root 4096 Jul 13 11:06 'Rivers of Nihil - Where Owls Know My Name'
drwxr-xr-x 2 root root 4096 Jul 13 11:27 'The Black Dahlia Murder - Abysmal'
drwxr-xr-x 2 root root 4096 Jul 13 11:26 'The Black Dahlia Murder - Deflorate'
drwxr-xr-x 2 root root 4096 Jul 13 11:20  transitions
root@lain:/srv/radio/music# ./tagger.sh	

Once that's done we're going to make another mpd config for our second channel, my second channel's mpd config will be in /root/.config/mpd/metal.conf:


root@lain:~/.config/mpd# cat metal.conf
music_directory     "/srv/radio/music/metal"
playlist_directory  "/srv/radio/data/metal/playlists"
db_file             "/srv/radio/data/metal/db"
state_file          "/srv/radio/data/metal/state"
sticker_file        "/srv/radio/data/metal/sticker.sql"
log_file            "/srv/radio/data/syslog.metal"
bind_to_address     "127.0.0.1"
#bind_to_address     "10.0.0.201"
port                "6602"

audio_output {
  name        "[mpd] metal (ogg)"
  description "classic lainchan radio: electronic, chiptune, weeb"
  type        "shout"
  encoder     "vorbis"
  host        "localhost"
  port        "8000"
  mount       "/mpd-metal.ogg"
  user        "source"
  password    "P@SSWORD"
  quality     "3"
  format      "44100:16:2"
  always_on   "yes"
}

audio_output {
  name        "[mpd] metal (mp3)"
  description "classic lainchan radio: electronic, chiptune, weeb"
  type        "shout"
  encoder     "lame"
  host        "localhost"
  port        "8000"
  mount       "/mpd-metal.mp3"
  user        "source"
  password    "P@SSW0RD"
  quality     "3"
  format      "44100:16:2"
  always_on   "yes"
}

audio_output {
  type "null"
  name "null"
}

 audio_output {
     type  "alsa"
     name  "alsa audio"
     mixer_type      "software"
 }

audio_output {
    type                    "fifo"
    name                    "my_fifo2"
    path                    "/tmp/mpd.fifo2"
    format                  "44100:16:2"
}

I also create the directories it requires for the playlists and database:


root@lain:~/.config/mpd# mkdir -p /srv/radio/data/metal/playlists/
root@lain:~/.config/mpd# cd /srv/radio/data/
root@lain:/srv/radio/data# tree .
.
├── cyberia
│   ├── db
│   ├── playlists
│   │   ├── cyberia.m3u
│   │   ├── Infected Mushroom - Army of Mushrooms.m3u
│   │   ├── Lainchan Radio Transitions.m3u
│   │   └── pendulum.m3u
│   ├── state
│   └── sticker.sql
├── metal
│   └── playlists
└── syslog

4 directories, 8 files
	

Now since we already made a systemd service to run the cyberia mpd process, let's make another one for the second channel:


root@lain:/srv/radio/data# cd /etc/systemd/system/
root@lain:/etc/systemd/system# ls -l | grep mpd
-rw-r--r-- 1 root root  334 Jul 12 23:19 mpd-cyberia.service
root@lain:/etc/systemd/system# cp mpd-cyberia.service mpd-metal.service

root@lain:/etc/systemd/system# vim mpd-metal.service
root@lain:/etc/systemd/system# cat mpd-metal.service
[Unit]
Description=mpd systemd service with config in /root/.config/mpd/metal.conf
After=network.target
After=systemd-user-sessions.service
After=network-online.target

[Service]
Type=simple
ExecStart=mpd --no-daemon /root/.config/mpd/metal.conf
Restart=always
RestartSec=10
KillMode=process

[Install]
WantedBy=multi-user.target
root@lain:/etc/systemd/system# systemctl daemon-reload
root@lain:/etc/systemd/system# systemctl enable --now mpd-metal
Created symlink /etc/systemd/system/multi-user.target.wants/mpd-metal.service → /etc/systemd/system/mpd-metal.service.
	

Now once that's done we can connect to it on port 6602:


root@lain:/srv/radio/data#  ncmpcpp -h 127.0.0.1 -p 6602
#press u to update database
#press 7 to go to the outputs
#press enter to activate the metal ogg and mp3 outputs	
#press 4 to check the album tags

#press q to exit

root@lain:/srv/radio/data# python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6602
"Behemoth - I Loved You At Your Darkest"
"Behemoth - The Satanist"
"Carnifex - Slow Death"
"Meshuggah - Obzen"
"Meshuggah - The Violent Sleep of Reason"
"Rivers of Nihil - Monarchy"
"Rivers of Nihil - Where Owls Know My Name"
"The Black Dahlia Murder - Abysmal"
"The Black Dahlia Murder - Deflorate"
Lainchan Radio Transitions
['"Behemoth - I Loved You At Your Darkest"', '"Behemoth - The Satanist"', '"Carnifex - Slow Death"', '"Meshuggah - Obzen"', '"Meshuggah - The Violent Sleep of Reason"', '"Rivers of Nihil - Monarchy"', '"Rivers of Nihil - Where Owls Know My Name"', '"The Black Dahlia Murder - Abysmal"', '"The Black Dahlia Murder - Deflorate"', 'Lainchan Radio Transitions']
Transition: transitions/Buckethead - Asylum Of Glass (Song Only)-AgzzsbPj_1w.mp3 (278s)
Album: "Behemoth - The Satanist" (2671s)
Tracks: #29 (7762s)

Target duration: 10800s
Actual duration: 10711s
Difference: 89s

root@lain:/srv/radio/data#  ncmpcpp -h 127.0.0.1 -p 6602
#press enter to start playing the tracks
#press q to exit

Now that's done let's add another cronjob to make sure the tracks are constantly added every 3 hours for our second channel:


root@lain:/srv/radio/data# crontab -e
	
# m h  dom mon dow   command
0 */3 * * * python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6601
0 */3 * * * python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6602

:wq

root@lain:/srv/radio/data# cronitor select

✔ python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6602
----► Running command: python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6602

"Behemoth - I Loved You At Your Darkest"
"Behemoth - The Satanist"
"Carnifex - Slow Death"
"Meshuggah - Obzen"
"Meshuggah - The Violent Sleep of Reason"
"Rivers of Nihil - Monarchy"
"Rivers of Nihil - Where Owls Know My Name"
"The Black Dahlia Murder - Abysmal"
"The Black Dahlia Murder - Deflorate"
Lainchan Radio Transitions
['"Behemoth - I Loved You At Your Darkest"', '"Behemoth - The Satanist"', '"Carnifex - Slow Death"', '"Meshuggah - Obzen"', '"Meshuggah - The Violent Sleep of Reason"', '"Rivers of Nihil - Monarchy"', '"Rivers of Nihil - Where Owls Know My Name"', '"The Black Dahlia Murder - Abysmal"', '"The Black Dahlia Murder - Deflorate"', 'Lainchan Radio Transitions']
Transition: transitions/Buckethead - I love my parents-BwXlzy9k7jI.mp3 (253s)
Album: "The Black Dahlia Murder - Deflorate" (2041s)
Tracks: #27 (8418s)

Target duration: 10800s
Actual duration: 10712s
Difference: 88s

----► ✔ Command successful    Elapsed time 0.291s

Now that we finished our cronitor job for our second channel, we need to edit icecast.xml:


root@lain:/srv/radio/data# vim /srv/Icecast-Server/conf/icecast.xml

	

Once we add the following block we restart our icecast service:


root@lain:/srv/radio/data# systemctl restart icecast
root@lain:/srv/radio/data# systemctl status icecast
● icecast.service - Icecast systemd service with config in /srv/Icecast-Server/conf/icecast.xml
   Loaded: loaded (/etc/systemd/system/icecast.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2021-07-13 12:06:07 CEST; 1s ago
 Main PID: 5283 (icecast)
    Tasks: 4 (limit: 4915)
   Memory: 1.2M
   CGroup: /system.slice/icecast.service
           └─5283 /usr/local/bin/icecast -c /srv/Icecast-Server/conf/icecast.xml

Jul 13 12:06:07 lain systemd[1]: Started Icecast systemd service with config in /srv/Icecast-Server/conf/icecast.xml.
Jul 13 12:06:07 lain icecast[5283]: [2021-07-13  12:06:07] WARN CONFIG/_parse_root Warning, location> not configured, using default value "Earth".
Jul 13 12:06:07 lain icecast[5283]: [2021-07-13  12:06:07] WARN CONFIG/_parse_root Warning, admin> contact not configured, using default value "icemaster@localhost". This breaks YP
root@lain:/srv/radio/data#
	

Now if you do this you will probably see that icecast doesnt detect the mountpoints anymore, to fix that restart the mpd channels:


root@lain:/srv/radio/data# systemctl restart mpd-cyberia
root@lain:/srv/radio/data# systemctl restart mpd-metal
	

You can verify if it's working at localip:8000/admin/listmounts.xsl

Now we need to change config.json to include our second channel:


root@lain:/srv/lainonlife# ls -l
total 36
drwxr-xr-x  4 root root 4096 Jul 12 18:49 backend
drwxr-xr-x  2 root root 4096 Jul 12 16:34 concourse
-rw-r--r--  1 root root  366 Jul 12 10:05 config.json
-rw-r--r--  1 root root  695 Jul 11 22:11 config.json.example
drwxr-xr-x  2 root root 4096 Jul 11 23:32 examples
drwxr-xr-x 10 root root 4096 Jul 12 10:16 frontend
-rw-r--r--  1 root root 1082 Jul 11 22:11 LICENSE
-rw-r--r--  1 root root 2344 Jul 11 22:11 README.md
drwxr-xr-x  2 root root 4096 Jul 13 08:48 scripts

root@lain:/srv/lainonlife# vim config.json
root@lain:/srv/lainonlife# cat config.json


{ "channels":
    { "cyberia":    { "mpd_host": "localhost", "mpd_port": 6601, "description": "classic lainchan radio: electronic, chiptune, weeb" }
    , "metal":       { "mpd_host": "localhost", "mpd_port": 6602, "description": "Death Metal, Deathcore, Black Metal" }}

, "template":
  { "default_channel": "cyberia"
  , "icecast_status_url": "/radio/status-json.xsl"
  , "icecast_stream_url_base": "https://lain.void.yt/radio/"
  , "server_cost": 10.00
  , "currency_symbol": "€"
  }
}

Now since we modified the config file let's restart the lainradio service:


root@lain:/srv/lainonlife# systemctl restart lainradio.service
	

Now by default the radio.js file looks for the channel names by their ogg extention, so i modified it to look for mp3 instead, and we finally get the channels to be displayed:


root@lain:/srv/http/js# cat radio.js | grep mp3
            if(sname !== undefined && sname.startsWith("[mpd] ") && sname.endsWith(" (mp3)")) {

I changed it from ogg to mp3 because for some reason i can't get my .ogg output streams to work, but the mp3 ones work fine so we're using them instead:

And when we change channels we see that everything updates as intended:

So with the same process we can add other channels, each of them have a systemd mpd-channel.service file to launch a mpd process on a specific port (ex: 6603 with a mpd config at /root/.config/mpd/channel.conf, with a directory at /srv/radio/music/channel/of their own. Of course you need to edit /srv/lainonlife/config.json and icecast.xml with your changes everytime. and make sure that you have a cronjob running python3 /srv/lainonlife/scripts/schedule.py --host=127.0.0.1 6603 every 3 hours after you added the transition tracks just like for our previous 2 channels in order to keep the radios constantly updated with new albums / songs to play for the listeners.

here's the forked repository with all important files i edited here:


-icecast.service
-mpd-cyberia.service
-lainradio.service

-schedule.py
-tagger.sh
-index.html

-ncmpcpp.conf
-mpd-cyberia.conf
-js/player.js

My Bunker

Some Address 67120,
Duttlenheim, France.

About Ech0

This cute theme was created to showcase your work in a simple way. Use it wisely.