Meta Artists & Rolling Queue patch
Richard Jaques <[email protected]> Sun, 4 Apr 2004 21:06:34 +0100
| Newsgroups | gmane.comp.audio.bossogg.devel |
|---|---|
| Message-ID | <20040404200634.GA32340@theo> |
--2oS5YaxWCcQjTEyO
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="rpj-bosschanges.patch"
Content-Transfer-Encoding: quoted-printable
Only in dev/: Makefile
diff -ru bossogg-0.13.6/boss3/CommandInterface.py dev/boss3/CommandInterfac=
e.py
--- bossogg-0.13.6/boss3/CommandInterface.py 2004-04-02 02:44:26.000000000 =
+0100
+++ dev/boss3/CommandInterface.py 2004-04-04 18:40:00.024788328 +0100
@@ -102,7 +102,7 @@
self.player =3D player
self.dbh =3D dbh
=20
- def artists(self, anchor=3D""):
+ def artists(self, anchor=3D"", artistid =3D 0):
"""
Returns a list of all the artists in the
database. Anchor will only return
@@ -111,6 +111,7 @@
=20
Parameters:
* anchor - string(optional)
+ * artistid - int(optional)
=20
Returns:
* Array
@@ -119,7 +120,7 @@
* artistname - string
"""
#print self.dbh.listArtists(anchor)
- return self.dbh.listArtists(anchor)
+ return self.dbh.listArtists(anchor,artistid)
=20
def albums(self, artistid=3DNone, anchor=3D""):
"""
Only in dev/boss3: CommandInterface.pyc
diff -ru bossogg-0.13.6/boss3/Database.py dev/boss3/Database.py
--- bossogg-0.13.6/boss3/Database.py 2004-04-02 02:44:27.000000000 +0100
+++ dev/boss3/Database.py 2004-04-04 20:17:39.896952352 +0100
@@ -82,6 +82,7 @@
sqlite.Cursor.execute(self, SQL, *args)
except:
log.exception("SQL ERROR")
+ print SQL
if needlock and not self.nolock:
sql_lock.release()
log.debug("lock", "Release lock for database writes", stack=3D1)
@@ -274,24 +275,35 @@
result =3D {"userid":row['userid'],"authlevel":['authlevel']}
return result
=20
- def listArtists(self, anchor=3D""):
+ def listArtists(self, anchor=3D"", artistid=3D0):
result =3D []
cursor =3D self.conn.cursor()
- SQL =3D "SELECT artistid, artistname FROM artists "
+ =09
+ if (artistid !=3D 0):
+ SQL =3D 'select a.artistid, a.artistname, -1 as "s.metaartistid" FROM =
artists a where a.artistid =3D %d'%artistid
+ else:
+ ##SQL now returns only artists that have at least one album, those that=
have
+ ##represent "meta artists" have the meta-artist flag set
+ SQL =3D "select distinct a.artistid, a.artistname, s.metaartistid from =
songs s left join artists a on a.artistid =3D s.metaartistid or (s.metaarti=
stid =3D -1 and a.artistid =3D s.artistid)"
+
if (anchor !=3D None and anchor !=3D ""):
- SQL +=3D "WHERE artistname like '%s%%' " % anchor
- SQL +=3D "ORDER BY lower(artistname) ASC"
+ SQL +=3D "WHERE a.artistname like '%s%%' " % anchor
+ SQL +=3D "ORDER BY lower(a.artistname) ASC"
cursor.execute(SQL)
+ =09
for row in cursor.fetchall():
log.debug("sqlresult", "Row: %s", row)
- if row['artistname'] !=3D '':
- result.append({"artistid":row['artistid'],"artistname":row['artistname=
']})
+ if row['a.artistname'] !=3D '':
+ if row['s.metaartistid'] =3D=3D -1:
+ metaartist=3D"False"
+ else:
+ metaartist=3D"True"
+ result.append({"artistid":row['a.artistid'],"artistname":row['a.artist=
name'],"metaartist":metaartist})
return result
=20
def listAlbums(self, artistid=3DNone, anchor=3D""):
result =3D []
cursor =3D self.conn.cursor()
- #Look for real albums first and stick them at the top of the list
=20
if artistid !=3D None:
SQL =3D "SELECT albumid, albumname, year FROM albums WHERE artistid =3D=
%i " % artistid
@@ -302,22 +314,12 @@
for row in cursor.fetchall():
log.debug("sqlresult", "Row: %s", row)
result.append({"albumid":row['albumid'],"albumname":row['albumname'],"=
albumyear":row['year'],"metaartist":0})
- #Now look for metaartist related albums
- SQL =3D "SELECT a.albumid, a.albumname, a.year FROM albums a INNER JOIN=
songs s ON a.albumid =3D s.albumid WHERE s.metaartistid =3D %s " % artistid
-
- if (anchor !=3D None and anchor !=3D ""):
- SQL +=3D "AND a.albumname like '%s%%' " % anchor.replace("'", "\\'")
- SQL +=3D "ORDER BY a.year, lower(a.albumname) ASC"
- cursor.execute(SQL)
- for row in cursor.fetchall():
- log.debug("sqlresult", "Row: %s", row)
- result.append({"albumid":row['a.albumid'],"albumname":row['a.albumname=
'],"albumyear":row['a.year'],"metaartist":1})
else:
SQL =3D "SELECT ar.artistid, ar.artistname, a.albumid, a.albumname, a.y=
ear FROM albums a INNER JOIN artists ar ON ar.artistid =3D a.artistid ORDER=
BY ar.artistname, a.year, lower(a.albumname) ASC"
cursor.execute(SQL)
for row in cursor.fetchall():
log.debug("sqlresult", "Row: %s", row)
- result.append({"artistid":row['ar.artistid'],"artistname":row['ar.arti=
stname'],"albumid":row['a.albumid'],"albumname":row['a.albumname'],"albumye=
ar":row['a.year'],"metaartist":0})
+ result.append({"artistid":row['ar.artistid'],"artistname":row['ar.arti=
stname'],"albumid":row['a.albumid'],"albumname":row['a.albumname'],"albumye=
ar":row['a.year']})
return result
=20
def listPlaylists(self):
@@ -416,9 +418,9 @@
SQL =3D "SELECT s.songid, s.artistid, ar.artistname, s.albumid, s.trackn=
um, s.songname, s.filename, s.filesize, s.bitrate, s.songlength, s.timespla=
yed, a.albumname, a.year, s.metaartistid, m.artistname"
if (playlistid !=3D None and playlistid !=3D ""):
SQL +=3D ", p.indexid"
- SQL +=3D " FROM songs s, albums a, artists ar"
+ SQL +=3D " FROM songs s, albums a, artists ar "
if (playlistid !=3D None and playlistid !=3D ""):
- SQL +=3D ", playlistdata p"
+ SQL +=3D ", playlistdata p "
SQL +=3D " LEFT OUTER JOIN artists m on s.metaartistid =3D m.artistid WH=
ERE a.albumid =3D s.albumid and ar.artistid =3D s.artistid "
if (albumid !=3D None and albumid !=3D ""):
SQL +=3D "AND s.albumid =3D %i " % albumid
@@ -428,7 +430,7 @@
SQL +=3D "AND p.playlistid =3D %i and p.songid =3D s.songid " % playlis=
tid
if (anchor !=3D None and anchor !=3D ""):
SQL +=3D "AND s.songname like '%s%%' " % anchor
- SQL +=3D "ORDER BY"
+ SQL +=3D " ORDER BY"
if playlistid !=3D None and playlistid !=3D "":
SQL +=3D " p.indexid,"=09
SQL +=3D" a.year, lower(a.albumname), s.tracknum, s.songname"
@@ -450,9 +452,8 @@
somesong =3D {"songid":row['s.songid'],"artistid":row['s.artistid'],"alb=
umid":row['s.albumid'],"songname":row['s.songname'],"filename":row['s.filen=
ame'],"filesize":row['s.filesize'],"songlength":row['s.songlength'],"trackn=
um":row['s.tracknum'],"timesplayed":timesplayed,"bitrate":row['s.bitrate'],=
"albumname":row['a.albumname'],"albumyear":row['a.year'],"artistname":row['=
ar.artistname']}
if 'p.indexid' in row:
somesong['indexid'] =3D row['p.indexid']
- if row['m.artistname'] !=3D None and row['s.metaartistid'] !=3D '-1':
+ if row['s.metaartistid'] !=3D '-1':
somesong['metaartistid'] =3D row['s.metaartistid']
- somesong['metaartistname'] =3D row['m.artistname']
if getgenres =3D=3D True:
somesong['genres'] =3D self.fillSongGenreHash(row['s.songid'])
result.append(somesong)
@@ -567,7 +568,11 @@
metaartistid =3D -1
if 'metaartistname' in song.keys():
metaartistid =3D self._getArtist(self.checkBinary(song['metaartistna=
me']),True)
- albumid =3D self._getAlbum(self.checkBinary(song['albumname']), artis=
tid, song['year'])
+ albumartistid =3D artistid
+ if (metaartistid !=3D -1):
+ albumartistid =3D metaartistid
+
+ albumid =3D self._getAlbum(self.checkBinary(song['albumname']), album=
artistid, song['year'])
songid =3D self._getNSong(self.checkBinary(song['songname']),artistid=
,self.checkBinary(song['filename']),song['tracknum'],albumid=3Dalbumid,year=
=3Dsong['year'],metaartistid=3Dmetaartistid, bitrate=3Dsong["bitrate"], son=
glength=3Dsong["songlength"])
else:
log.debug("import", "Could not get bitrate of song %s. Assuming bad =
file.", song["filename"])
Only in dev/boss3: Database.pyc
Only in dev/boss3: DatabaseSchema.pyc
Only in dev/boss3: Makefile
Only in dev/boss3: Player.pyc
Only in dev/boss3: UserManager.pyc
Only in dev/boss3: __init__.pyc
Only in dev/boss3: bocmd.pyc
Only in dev/boss3/bossao: .deps
Only in dev/boss3/bossao: .libs
Only in dev/boss3/bossao: Makefile
Only in dev/boss3/bossao: __init__.pyc
Only in dev/boss3/bossao: _bossao.la
Only in dev/boss3/bossao: _bossao.so
Only in dev/boss3/bossao: bossao.lo
Only in dev/boss3/bossao: bossao.o
Only in dev/boss3/bossao: bossao.pyc
Only in dev/boss3/bossao: bossao_wrap.lo
Only in dev/boss3/bossao: bossao_wrap.o
Only in dev/boss3/bossao: flac.lo
Only in dev/boss3/bossao: flac.o
Only in dev/boss3/bossao: mp3.lo
Only in dev/boss3/bossao: mp3.o
Only in dev/boss3/bossao: ogg.lo
Only in dev/boss3/bossao: ogg.o
Only in dev/boss3/bossao: oss_mixer.lo
Only in dev/boss3/bossao: oss_mixer.o
Only in dev/boss3/bossexceptions: EndOfQueueException.pyc
Only in dev/boss3/bossexceptions: __init__.pyc
Only in dev/boss3/metadata: .deps
Only in dev/boss3/metadata: .libs
Only in dev/boss3/metadata: Makefile
Only in dev/boss3/metadata: __init__.pyc
Only in dev/boss3/metadata: _id3.la
Only in dev/boss3/metadata: _id3.so
Only in dev/boss3/metadata: _id3_la-id3.lo
Only in dev/boss3/metadata: _id3_la-id3.o
Only in dev/boss3/metadata: _id3_la-id3_wrap.lo
Only in dev/boss3/metadata: _id3_la-id3_wrap.o
Only in dev/boss3/metadata: _id3_la-id3lib_wrapper.lo
Only in dev/boss3/metadata: _id3_la-id3lib_wrapper.o
Only in dev/boss3/metadata: _id3_la-vorbis.lo
Only in dev/boss3/metadata: _id3_la-vorbis.o
Only in dev/boss3/metadata: id3.pyc
diff -ru bossogg-0.13.6/boss3/util/ConfigFile.py dev/boss3/util/ConfigFile.=
py
--- bossogg-0.13.6/boss3/util/ConfigFile.py 2004-04-02 03:09:26.000000000 +=
0100
+++ dev/boss3/util/ConfigFile.py 2004-04-03 17:34:37.000000000 +0100
@@ -80,6 +80,14 @@
#change if volume changes aren't getting recognized.
mixerdevice: %(default_mixerdevice)s
=20
+#queuetype. Two types: standard and rolling.
+# standard adds to the current queue and the player
+# has a concept of the current song.
+# rolling removes songs from the top of the queue once=20
+# they are played so that the current song is always the=20
+# top of the playlist, more like a pub jukebox.
+queuetype: %(default_queuetype)s
+
[SHOUT]
=20
#Set to 1 to enable icecasting
@@ -152,6 +160,7 @@
"default_audiodriver" : self.cfgparser.get("DEFAULT", "audiodriver"),
"default_mixertype" : self.cfgparser.get("DEFAULT", "mixertype"),
"default_mixerdevice" : self.cfgparser.get("DEFAULT", "mixerdevice"),
+ "default_queuetype" : self.cfgparser.get("DEFAULT", "queuetype"),
=20
"shout_enable" : self.cfgparser.get("SHOUT", "enable"),
"shout_host" : self.cfgparser.get("SHOUT", "host"),
@@ -183,7 +192,8 @@
self.cfgparser.set("DEFAULT","audiodriver", "auto")
self.cfgparser.set("DEFAULT","mixertype","oss"),
self.cfgparser.set("DEFAULT","mixerdevice","/dev/mixer"),
-
+ self.cfgparser.set("DEFAULT","queuetype","standard")
+ =09
self.cfgparser.set("SHOUT", "enable", "0")
self.cfgparser.set("SHOUT", "host", "127.0.0.1")
self.cfgparser.set("SHOUT", "user", "user")
Only in dev/boss3/util: ConfigFile.pyc
Only in dev/boss3/util: Logger.pyc
diff -ru bossogg-0.13.6/boss3/util/Queue.py dev/boss3/util/Queue.py
--- bossogg-0.13.6/boss3/util/Queue.py 2004-04-02 02:44:29.000000000 +0100
+++ dev/boss3/util/Queue.py 2004-04-04 15:18:49.465792216 +0100
@@ -36,9 +36,13 @@
self.items.append(item)
def remove(self, item):
if self.manager.usingendofqueue or ((item !=3D self.currentindex) and ( =
item >=3D 0) and (item < len(self.items))):
- self.items.pop(item)
- if (item < self.currentindex):
- self.currentindex=3Dself.currentindex-1
+ if (self.manager.session["cfg"].get("queuetype") !=3D "rolling" or inte=
rnalcall):
+ if(item < self.currentindex):
+ self.currentindex=3Dself.currentindex-1
+ self.items.pop(item)
+ else:
+ #rolling queue adjustment
+ self.items.pop(item+1)
def clear(self):
self.items=3D[]
self.currentindex=3D0
@@ -48,7 +52,12 @@
print self.items
def getSongIDs(self):
result =3D []
- for i in self.items:
+ if (self.manager.session["cfg"].get("queuetype") !=3D "rolling") or self=
=2Emanager.usingendofqueue =3D=3D 1:
+ songsweshow =3D self.items
+ else:
+ #we don't show the song we are currently on in the queue
+ songsweshow =3D self.items[1:]
+ for i in songsweshow:
if 'songid' in i:
result.append(int(i['songid']))
return result
@@ -70,15 +79,27 @@
return None
def next(self):
self.currentindex +=3D 1
+ #if we are in rolling queue mode then delete the top entry if it exists,
+ if (self.manager.session["cfg"].get("queuetype") =3D=3D "rolling"):
+ if len(self.items) > 0:
+ self.items.pop(0)
+ self.currentindex =3D 0
+
if self.currentindex >=3D len(self.items):
self.currentindex =3D len(self.items)
return None
else:
- return self.items[self.currentindex]
+ return self.items[self.currentindex]
def prev(self):
if self.currentindex > 0:
self.currentindex=3Dself.currentindex-1
def move(self,oldindex,newindex):
+ =09
+ if (self.manager.session["cfg"].get("queuetype") =3D=3D "rolling"):
+ #correct numbers
+ oldindex =3D oldindex + 1
+ newindex =3D newindex + 1
+
if (newindex > -1) and ( newindex < len(self.items)): =20
if (oldindex > -1) and ( oldindex < len(self.items)): =20
tmp=3Dself.items[newindex]
Only in dev/boss3/util: Queue.pyc
Only in dev/boss3/util: Session.pyc
Only in dev/boss3/util: UTFstring.pyc
Only in dev/boss3/util: __init__.pyc
Only in dev/boss3/util: bosslog.pyc
Only in dev/boss3/util/logging: __init__.pyc
Only in dev/boss3: version.pyc
Only in dev/boss3/xmlrpc: Auth.pyc
Only in dev/boss3/xmlrpc: Control.pyc
Only in dev/boss3/xmlrpc: DB.pyc
Only in dev/boss3/xmlrpc: Edit.pyc
Only in dev/boss3/xmlrpc: Info.pyc
diff -ru bossogg-0.13.6/boss3/xmlrpc/List.py dev/boss3/xmlrpc/List.py
--- bossogg-0.13.6/boss3/xmlrpc/List.py 2004-04-02 02:44:27.000000000 +0100
+++ dev/boss3/xmlrpc/List.py 2004-04-04 19:20:56.419359592 +0100
@@ -32,8 +32,8 @@
=20
=09
list("artists"): Displays a list of all the artists in the
- database. Ordered in alphabetical order.
-
+ database. Ordered in alphabetical order. Note that this will=20
+ only return artists with one or more albums.
Parameters:
* anchor - string(optional)
=20
@@ -43,10 +43,21 @@
* artistname - string(base64)
* artistid - int
=20
+ list("artist"): Return info about a single artist in the=20
+ same format as list("artists")
+
+ Parameters=20
+ * artistid - int
+
+ Returns:
+ * Array
+ * Struct
+ * artistname - string(base64)
+ * artistid - int
=09
list("albums"): Returns a list of all the albums in the
database for the given artistid. Anchor will only return
- albumnames that start with the given anchor string.
+ albumnames that start with the given anchor string. =20
=20
Parameters:
* artistid - int
@@ -58,8 +69,6 @@
* albumname - string(base64)
* albumid - int
* albumyear - int
- * metaartist - int (1 =3D True, 0 =3D False)
-
=20
list("songs"): Returns a list of all the albums in the
database for the given artistid or albumid. Anchor will
@@ -260,6 +269,13 @@
if "artistname" in i:
i['artistname'] =3D UTFstring.encode(i['artistname'])
return tmp
+ if (cmd =3D=3D "artist"):
+ tmp =3D None
+ tmp =3D cmdint.list.artists("",args[0])=09
+ for i in tmp:
+ if "artistname" in i:
+ i['artistname'] =3D UTFstring.encode(i['artistname'])
+ return tmp
elif (cmd =3D=3D "albums"):
tmp =3D None
#if (len(args)>0):
Only in dev/boss3/xmlrpc: List.pyc
Only in dev/boss3/xmlrpc: Load.pyc
Only in dev/boss3/xmlrpc: Set.pyc
Only in dev/boss3/xmlrpc: SimpleXMLRPCServer.pyc
Only in dev/boss3/xmlrpc: Util.pyc
Only in dev/boss3/xmlrpc: XmlRpcServer.pyc
Only in dev/boss3/xmlrpc: __init__.pyc
Only in dev/boss3/xmlrpc: bossxmlrpclib.pyc
Only in dev/boss3/xmlrpc: bossxmlrpcserver.pyc
Only in dev/boss3/xmlrpc: xmlrpclib.pyc
diff -ru bossogg-0.13.6/bosync dev/bosync
--- bossogg-0.13.6/bosync 2004-04-02 04:25:54.000000000 +0100
+++ dev/bosync 2004-04-04 16:32:53.207240832 +0100
@@ -96,6 +96,7 @@
( "", "ARTIST", "artist", "Force all songs to have this artist. Ignore mat=
ched"),=20
( "", "ALBUM", "album", "Force all songs to have this album. Ignore match=
ed"),=20
( "", "TRACK", "track", "Force all songs to have this track number. Ignor=
e matched"),=20
+( "", "METAARTIST", "metaartist","Force all songs to have this meta-artist=
=2E Ignore matched"),=20
)
=20
=20
@@ -209,8 +210,16 @@
files[filename] =3D st[ST_MTIME]
=20
return files
+def getInput(prompt, default):
+ ##we modify the prompt so that it of the form
+ ## prompt [default]:
+ returnVal =3D raw_input(" > " + prompt + " [" + default + "]:")
+ if (returnVal =3D=3D ""):
+ return default
+ else:
+ return returnVal
=20
-def file_info(file):
+def file_info(file, metadata):
global comp
info =3D { }
tmp =3D { 'artistname':'',
@@ -233,11 +242,10 @@
if match:
regex[2] +=3D 1
info.update(match.groupdict())
- # Update with any info we want to force
- info.update(config["force"])
return info
log.info("File does not match any format: %s", file)
else:
+ print file
try:
import readline
readline.parse_and_bind("tab: complete")
@@ -248,20 +256,23 @@
=09
if readline:
readline.set_completer(comp.artist)
- info["artistname"] =3D raw_input("Album Artist Name: ")
+ ##RPJ: I am confused here....
+ ## I thought that this was supposed to be the Song Artist Name
+ info["metaartistname"] =3D getInput("Album Artist Name", metadata["artis=
tname"])
if readline:
- comp.current_artist =3D info["artistname"]
+ comp.current_artist =3D info["metaartistname"]
readline.set_completer(comp.album)
- info["albumname"] =3D raw_input("Album Name: ")
+ info["albumname"] =3D getInput("Album Name", metadata["albumname"])
if readline:
- comp.add_album(info["artistname"], info["albumname"])
+ comp.add_album(info["metaartistname"], info["albumname"])
readline.set_completer(None)
- info["year"] =3D int(raw_input("Year: "))
- info["tracknum"] =3D int(raw_input("Track Number: "))
- info["songname"] =3D raw_input("Song Title: ")
+ info["year"] =3D int(getInput("Year", str(metadata["year"])))
+ info["tracknum"] =3D int(getInput("Track Number", str(metadata["tracknum=
"])))
+ info["songname"] =3D getInput("Song Title", metadata["songname"])
if readline:
readline.set_completer(comp.artist)
- info["metaartistname"] =3D raw_input("Song Artist Name: ")
+ ##RPJ: and this the Album Artists name.
+ info["artistname"] =3D getInput("Song Artist Name", metadata["artistname=
"])
=20
return info
=09
@@ -310,14 +321,18 @@
cnt +=3D 1
progress_meter(cnt, total)
time.sleep(0.02)
- metadata =3D rpc.db("getmetadata", UTFstring.encode(file))
- #import pprint
- #pprint.pprint(metadata)
+ metadata =3D rpc.db("getmetadata", UTFstring.encode(file))=20
for key in metadata.keys():
metadata[key] =3D UTFstring.decode(metadata[key])
- sinfo =3D file_info(file)
+ sinfo =3D file_info(file, metadata)
metadata.update(sinfo)
metadata["filename"] =3D file
+ =20
+ # Update with any info we want to force
+ metadata.update(config["force"])
+ =09
+ #import pprint
+ #pprint.pprint(metadata)
additions.append(metadata)
=20
log.debug("import", "Starting song import %d", time.time())
@@ -347,8 +362,12 @@
=20
if del_files:
display("Deleting files: %d files removed from database" % len(del_files=
))
+ #encode all the filenames first
+ del_tmp =3D []
+ for tmpfile in del_files:
+ del_tmp.append(UTFstring.encode(tmpfile))
if not config["noop"]:
- rpc.db("importdelete", del_files)
+ rpc.db("importdelete", del_tmp)
=20
if not config["noop"]:
rpc.db("importend")
@@ -482,6 +501,8 @@
config["force"]["tracknum"] =3D arg
if opt in ("--GENRE",):
config["force"]["genrename"] =3D arg
+ if opt in ("--METAARTIST",):
+ config["force"]["metaartistname"] =3D arg
if opt in ("-i", "--interactive"):
config["interactive"] =3D True
=09
Only in dev/: config.h
Only in dev/: config.log
Only in dev/: config.status
Only in dev/: libtool
Only in dev/: stamp-h1
--2oS5YaxWCcQjTEyO
Content-Type: application/pgp-signature
Content-Disposition: inline
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.2.4 (GNU/Linux)
iD8DBQFAcGrKoszcdualiuQRAqMLAJ9yd9KJMqvlKuaZhBKP0NNfd3eD2ACgi6P8
oVwuniIWXo2ffAhCvJMwKIk=
=ypMu
-----END PGP SIGNATURE-----
--2oS5YaxWCcQjTEyO--
-------------------------------------------------------
This SF.Net email is sponsored by: IBM Linux Tutorials
Free Linux tutorial presented by Daniel Robbins, President and CEO of
GenToo technologies. Learn everything from fundamentals to system
administration.http://ads.osdn.com/?ad_id=1470&alloc_id=3638&op=click