|
|
Building a Shoutcast ServerI visited a friend of mine who had copied all his CDs to hard disk, using the Itunes system, and buying a dedicated player that hooked into his hifi. I began to wonder if I could do the same, but for free (or, more accurately, for no money). First, I copied all my CDs to mp3 files using ripit, although I installed the standard package available under Debian etch. Next challenge was how to get these mp3's playing on the hifi. Some sort of internet radio broadcasting thing, I guessed. I poked around the Shoutcast site and discovered I could pretty easily do what I wanted using the Shoutcast server (agreeing to a licence required) and broadcasting utilities (now no longer available) from http://www.shoutcast.com/downloads/sc_trans_posix_040.tgz. The server handles the delivery of content to clients, while sc_trans defines what the content is. Both of these come as Linux binaries, so if you are leary of binaries, you might want to look at other sources of supply. I installed both of these packages on my main Debian box and configured them. The server is fairly easy, and the config file is well commented. I configured sc_trans to deliver random content from a playlist file (just a list of mp3 files). In order to ease the creation of playlist files for my collection of mp3 files, I wrote a small python program playlist.py, which created playlists based on a simple database of categorised mp3 directories. All the ripped music lives is the /rep/music/mp3 directory, each subdirectory holding a CD's tracks. E.g: johnny_lytle_-_the_loop_-_new_and_groovy/ josh_rouse_-_nashville/ kent,_stacey_-_dreamsville/ kevin_ayers_-_the_confessions_of_dr_dream/ The database is a text file, each line consisting of a directory name (i.e. CD) and category, separated by a colon ":". More than one category may be assigned; multiple categories are comma "," separated. So, the above album directories could be represented as: johnny_lytle_-_the_loop_-_new_and_groovy:jazz josh_rouse_-_nashville:rock kent,_stacey_-_dreamsville:jazz kevin_ayers_-_the_confessions_of_dr_dream:rock,prog playlist.py allows the creation of playlists, based on categories one specifies on the command line. For full details, see the source comments below. To control the server and broadcast playlists, I generated a GNU make file. The targets provided are:
Once this thing was broadcasting on the LAN, I could use yet another knackered laptop from work, now running Debian, to act as the client. It had a wireless card, so all I had to do was fire up xmms and plug the headphone outlet into the hifi. How easy was that? Here's the Makefile:
# Make file to control shoutcast server
.SILENT:
.PHONY: start stop clean skip reload playlists use
MR=/rep/music
PID-TRANS=${MR}/pid.sc_trans
PID-SERV=${MR}/pid.sc_serv
start:
if [ -r ${PID-TRANS} -o -r ${PID-SERV} ]; then \
echo "Server already running" ; \
exit 1 ; \
fi
cd ${MR}/sc/sc_trans_040 ;\
./sc_trans_linux </dev/null >/dev/null 2>&1 & echo $$! >${PID-TRANS}
cd ${MR}/sc/server ;\
./sc_serv </dev/null >/dev/null 2>&1 & echo $$! >${PID-SERV}
echo "Shoutcast server and transcoder started."
stop:
if [ -r ${PID-TRANS} ]; then \
/bin/kill -TERM `cat ${PID-TRANS}` ; \
fi
if [ -r ${PID-SERV} ]; then \
/bin/kill -TERM `cat ${PID-SERV}` ; \
fi
rm -f pid.*
playlists:
find `tools/playlist.py -a` -type f -name "*.mp3" >playlists/all
for genre in `tools/playlist.py -l`; do \
find `tools/playlist.py $$genre` -type f -name "*.mp3" \
>playlists/$$genre ;\
done
use:
ifndef cat
${error Must specifiy category with cat=}
endif
if [ -r ${MR}/playlists/${cat} ]; then \
cp ${MR}/playlists/${cat} ${MR}/sc/sc_trans_040/playlist ; \
else \
echo "No such playlist: ${cat}" ; \
fi
skip:
kill -WINCH `cat ${PID-TRANS}`
reload:
kill -USR1 `cat ${PID-TRANS}`
clean:
find ${MR} -type f -name "*.log" -delete
And, as trailed above, here's the source for playlist.py:
#!/usr/bin/python
"""
NAME
playlist.py: Output album entries matching category criteria
SYNOPSIS
playlist.py [-a] [-x cat[,cat ...]] [-l] [-f album-list] [-d dir]
[-U] [-n] [-c cat[,cat ...]] [cat cat ...]
Switches:
-a output all album entries
-x exclude categories
-l list all categories defined in album-list file
-f read albums and categories from album-list file; default
is album.list in the working directory
-d path location for all album entry names; default is
/rep/music/mp3
-U update album list with any new directories found in the mp3
directory, as specified by the -d switch
-n don't prompt for categories when adding new albums via the -U
switch; a default category of NEW will be assigned, unless
overridden by the -c switch.
-c use category list as default when adding new albums via the
-U switch
One more more categories may be provided. Categories are implicitly
or'ed together. To 'and' categories (i.e select those albums that
have been tagged with all the categories given, use the plus (+)
character to join categories.
DESCRIPTION
playlist.py is driven by the album-list file, which consists of
album directory names, one per line, followed by a comma-separated
list of assigned categories. Categories are separated from the
album directory name by a colon.
For example:
various_-_stuff:prog,rock
The program will return a list of album directories which match
the desired catagories, each album directory prefixed by the -d dir
argument (or /rep/music/mp3 if -d dir is not given).
This list is designed to be used by find in order to create a
playlist of song files.
The -x argument may be used to exclude certain categories, e.g.
"playlist.py -a -x rock" will omit rock genre music from an
otherwise complete playlist. As another example, "playlist.py
-x funk jazz" will return all jazz music that is not also
categorised as funk.
If -U is specified, any playlist options and categories are
ignored. By default, the -U switch will cause playlist.py to
prompt for the category to assign for each new album located in
the directory identified by the -d switch. This prompting may be
suppressed using the -n switch, in which case new entries are
given the category NEW. This default may be modified by providing
a preferred default category (or list) using -c.
EXAMPLES
Generate complete playlist:
find `playlist.py -a` -type f -name "*.mp3" -print >playlist
Generate 'pure' jazz playlist:
find `playlist.py -x rock,funk,world jazz` \
-type f -name "*.mp3" -print >jazz.pl
Generate jazz rock playlist:
find `playlist.py -x world,funk jazz+rock` \
-type f -name "*.mp3" -print >jazzrock.pl
Update album.list file with new album entries in the mp3 directory,
do not prompt for categories but assign the category of "rock":
python playlist.py -U -n -c rock
"""
import sys
import getopt
import os
def build_lists(file_name):
"Build and return dictionary of category lists."
meta = dict()
try:
for line in open(file_name):
toks = line.strip().split(":")
cats = toks[1].split(",")
for cat in cats:
if cat in meta.keys():
meta[cat].append(toks[0])
else:
meta[cat] = [toks[0],]
except IOError,e:
print >>sys.stderr,"%s: unable to process album file: %s" %\
(sys.argv[0],e)
sys.exit(1)
return meta
def read_album_list(file_name):
"Build in-memory version of album-list file."
albums = dict()
try:
for line in open(file_name):
toks = line.strip().split(":")
albums[toks[0]] = toks[1]
except IOError,e:
print >>sys.stderr,"%s: unable to process album file: %s" %\
(sys.argv[0],e)
sys.exit(1)
return albums
def write_album_list(albums,file_name):
"Write in-memory version of album-list to file."
f = open(file_name,"w")
for album in sorted(albums.keys()):
f.write("%s:%s\n"%(album,albums[album]))
f.close()
return
def add_from_dir(albums,album_dir,ask,default_cat):
"Add categories to new directory entries; prompt user if requested."
count = 0
dirs = os.listdir(album_dir)
for dir in dirs:
if dir not in albums:
if ask:
albums[dir] = get_input("%s: "%(dir,))
else:
albums[dir] = default_cat
count += 1
return count
def get_input(prompt):
"Get category from user."
try:
cat = raw_input(prompt)
if cat == "q": sys.exit(1)
except EOFError:
print >>sys.stderr,"End of file reading from stdin."
sys.exit(1)
return cat
def parse_catands(album_cats,cat_string):
"Return sequence of albums that match anded categories."
cats = cat_string.split('+')
if len(cats) <= 1:
print >>sys.stderr,"%s: '%s' is not a catand - internal error." % \
(sys.argv[0],cat_string)
sys.exit(1)
cat = cats[0]
try:
s = set(album_cats[cat])
for cat in cats[1:]:
s = s.intersection(set(album_cats[cat]))
except KeyError:
print >>sys.stderr,"%s: no such category as %s"% \
(sys.argv[0],cat)
sys.exit(1)
return s
def process_cats(meta_list,cats,exclude_list,root_dir,all):
"Generate list of albums as determined by arguments."
names = dict()
try:
if all:
for albums in meta_list.values():
for album in albums:
names[album] = 0
else:
# split category arg list into two: those with
# ands (+) and ors (implicit)
catands = list()
cators = list()
for cat in cats:
if cat.find('+') > 0:
catands.append(cat)
else:
cators.append(cat)
# get all anded categories first
for catand in catands:
albums = parse_catands(meta_list,catand)
for album in albums:
names[album] = 0
for cat in cators:
for album in meta_list[cat]:
names[album] = 0
if exclude_list:
excludes = exclude_list.strip().split(",")
for cat in excludes:
for album in meta_list[cat]:
try:
del names[album]
except:
continue
except KeyError:
print >>sys.stderr,"%s: no such category as %s" % (sys.argv[0],cat)
sys.exit(1)
for album in names.keys():
print "%s/%s"%(root_dir,album)
return
###################################################################
# program starts here #
###################################################################
ls = False
all = False
album_file = "album.list"
exclude_list = None
root_dir = "/rep/music/mp3"
update_list = False
ask = True
default_new = "NEW"
try:
opts,args = getopt.getopt(sys.argv[1:],'arx:lfUnc:')
for o,v in opts:
if o == '-l': ls = True
elif o == '-a': all = True
elif o == '-f': album_file = v
elif o == '-x': exclude_list = v
elif o == '-d': root_dir = v
elif o == "-U": update_list = True
elif o == "-n": ask = False
elif o == "-c": default_new = v
except getopt.GetoptError,e:
print >>sys.stderr,"%s: illegal argument -%s" % (sys.argv[0],e.opt)
sys.exit(1)
# if update of album list file required, that's all we'll do
if update_list:
albums = read_album_list(album_file)
added = add_from_dir(albums,root_dir,ask,default_new)
write_album_list(albums,album_file)
print "Added",added,"albums."
else:
# read in album file and build category lists
meta_list = build_lists(album_file)
# if a list of the categories desired, that's it.
if ls:
for cat in meta_list.keys():
print cat
else:
process_cats(meta_list,args,exclude_list,root_dir,all)
|