I've created a file system, shoutcastfs, which enables you to mount the Shoutcast Radio directory as a file system. The genres are represented as directories and stations as files. Each file contains the station's playlist and the files are suffixed with .pls which makes it possible to load the playlist in a media player such as Amarok by double-clicking the file.
Of course, I'm using pyshoutcast (Python shoutcast API) to access the shoutcast service.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import errno
import fuse
import stat
import os
import shoutcast
fuse.fuse_python_api = (0, 2)
_shoutcastApi = shoutcast.ShoutCast()
class RootInfo(fuse.Stat):
def __init__(self):
fuse.Stat.__init__(self)
self.st_mode = stat.S_IFDIR | 0755
self.st_nlink = 2
self._genres = {}
@property
def genres(self):
if not self._genres:
for g in _shoutcastApi.genres():
self._genres[g] = GenreInfo(g)
return self._genres
class GenreInfo(fuse.Stat):
def __init__(self, name):
fuse.Stat.__init__(self)
self.st_mode = stat.S_IFDIR | 0755
self.st_nlink = 2
self.name = name
self._stations = {}
@property
def stations(self):
if not self._stations:
for s in _shoutcastApi.stations(self.name):
name = '{0}.pls'.format(s[0])
name = name.replace('/', '|')
self._stations[name] = StationInfo(name, s[1])
return self._stations
class StationInfo(fuse.Stat):
def __init__(self, name, station_id):
fuse.Stat.__init__(self)
self.st_mode = stat.S_IFREG | 0644
self.st_nlink = 1
# Hope no playlist exceeds this size
self.st_size = 4096
self.name = name
self.station_id = station_id
self._content = None
@property
def content(self):
if self._content is None:
self._content = _shoutcastApi.tune_in(self.station_id).read()
return self._content
class ShoutcastFS(fuse.Fuse):
def __init__(self, *args, **kw):
fuse.Fuse.__init__(self, *args, **kw)
self.root = RootInfo()
def split_path(self, path):
""" Returns genre and station """
if path == '/':
return (None, None)
parts = path.split('/')[1:]
if len(parts) == 1:
return (parts[0], None)
else:
return parts
def getattr(self, path):
genre, station = self.split_path(path)
if genre is None:
stat = self.root
else:
stat = self.root.genres.get(genre)
if not stat:
return -errno.ENOENT
if station:
stat = stat.stations.get(station)
if not stat:
return -errno.ENOENT
return stat
def readdir(self, path, offset):
yield fuse.Direntry('.')
yield fuse.Direntry('..')
if path == '/':
entries = self.root.genres.keys()
else:
entries = self.root.genres[path[1:]].stations.keys()
for e in entries:
yield fuse.Direntry(e)
def open(self, path, flags):
# Only support for 'READ ONLY' flag
access_flags = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
if flags & access_flags != os.O_RDONLY:
return -errno.EACCES
else:
return 0
def read(self, path, size, offset):
genre, station = self.split_path(path)
info = self.root.genres[genre].stations[station]
if offset < info.st_size:
if offset + size > info.st_size:
size = info.st_size - offset
return info.content[offset:offset+size]
else:
return ''
if __name__ == '__main__':
fs = ShoutcastFS()
fs.parse(errex=1)
fs.multithreaded = False
fs.main()
To try the file system, run:
$ # Download shoutcast.py $ wget http://github.com/mariob/pyshoutcast/raw/master/src/shoutcast.py $ mkdir mnt $ ./shoutcastfs mnt $ cd mnt/ $ ls ...A list of genres... $ cd Samba $ ls ...A list of 'Samba' stations... $ cat [station name] ...Playlist data...Have fun!