#!/usr/bin/python 

# xmms-titles
# Copyright (c) 2002, 2003 John Morton.
#
# Permission to use, copy, modify, distribute, and sell this software and
# its documentation for any purpose is hereby granted without fee, 
# provided that the above copyright notice appear in all copies and that 
# both that copyright notice and this permission notice appear in supporting
# documentation.  No representations are made about the suitability of this
# software for any purpose.  It is provided "as is" without express or 
# implied warranty.


"""Displays playlist title of the track currently playing in xmms.

It can also display the next few tracks that will be played.

Depends on the pyxmms package - http://www.via.ecp.fr/~flo/

"""

import sys, os, getopt, time, select, re, xmms
import warnings
from warnings import warn

### Defaults.

debug = 0
__version__ = '1.0'
version = __version__
maintainer = "John Morton <jwm@eslnz.co.nz>"

default_options = {'yaf_splash_cmd': '/usr/bin/yaf-splash',
                   'xss_cmd': '/usr/local/bin/xscreensaver-command',
                   
                   'display' : None,
                   'font': '-adobe-helvetica-bold-r-normal-*-*-180-*-*-p-*-*-1',
                   'placement' : 'top',
                   'transparent' : 1,
                   'timeout' : 8,

                   'xmms_session': 0,

                   'display_next_titles' : 0,
                   'check_xss' : 0,
                   
                   'sleep' : 5,
                   'check_xss' : 0,
                   }


separator = re.compile('^[\s-]+$')


### New Exceptions

class OptionError(ValueError):
    """Errors caused by incorrectly specified command line options."""    
    def __init__(self,args=None):
        self.args = args

error_exceptions = [UserWarning, OptionError, KeyboardInterrupt]

### 

def pretty_errors(type, value, tb):
    """An override exception hook to pretty print errors.

    Certain classes of exceptions, such as UserWarnings and our own exceptions,
    will just be printed verbatim, while other classes will be printed with
    a note saying they are probably bugs, and what you should do about it.

    In debug mode, we print the traceback and go into the debugger, if we're
    not in interactive mode.
    """
    
    if debug:
        if (hasattr(sys, 'ps1') or
            not (sys.stderr.isatty() and sys.stdin.isatty()) or
            type == SyntaxError):
            # we are in interactive mode or we don't have a tty-like
            # device, so we call the default hook, or it's a syntax error            
            sys.__excepthook__(type, value, tb)
        else:
            import traceback, pdb
            # we are NOT in interactive mode, print the exception...
            traceback.print_exception(type, value, tb)
            print
            # ...then start the debugger in post-mortem mode.
            pdb.pm()
    else:
        if type in error_exceptions:
            print value
            if type == UserWarning:
                print "Try --force if this is what you want."
            sys.exit(1)
        else:
            print "%s: %s" % (type.__name__, value)
            print "This is probably a bug. Please report it to %s" % maintainer
            sys.exit(3)

sys.excepthook = pretty_errors

def pretty_warning(msg, category, filename, lineno):
    """Defuglification of the warning messages
    
    Like pretty_errors, we have a debug mode that prints more information.    
    """

    # make the category not suck. 
    pretty_cat = ''
    for c in category.__name__.split():
        if c.isupper():
            pretty_cat += ' ' + c
        else:
            pretty_cat += c
    pretty_cat.lstrip()

    if debug:
        return "%s: %s at %s, %s\n" % (pretty_cat, msg, filename, lineno)
    else:
        return "%s: %s\n" % (pretty_cat, msg)

warnings.formatwarning = pretty_warning

### Option handling

def usage(options):

    strs = {'name' : os.path.basename(sys.argv[0]),
            'version' : version,
            }
    strs.update(options)
    
    print """%(name)s %(version)s - Display the current track playing in xmms

%(name)s briefly displays the current playing track, and optionally the next
few queued tracks using yaf-splash.

Usage: %(name)s [options] 

-h, --help              This message
-x, --xscreensaver-mode Only display titles when xscreensaver is active
-n, --next-tracks [n]   Also display the next n tracks in the playlist
-d, --display           Set the display for yaf-splash to run on
-f, --font [fontdesc]   Set the font used by yaf-splash 
-t, --timeout [sec]    Set the duration the message is displayed for
-p, --placement [place] Set the placement of the current track message,
                        same as for yaf-splash.
-s, --xmms-session      Which xmms session to query. Defaults to the first one.
""" % strs
    
    return None


def parse_options(default_options):

    global debug
    
    options = {}
    options.update(default_options)

    longopts = ["help", "debug", "xscreensaver-mode", "next-tracks=",
                "display=", "font=", "timeout=", "placement=",
                "xmms-session="]
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hxn:d:f:t:p:s:", longopts)
    except getopt.GetoptError:
        usage(default_options)
        sys.exit(2)

    for o,a in opts:
        if o in ('-h','--help'):
            usage(default_options)
            sys.exit()
        elif o == '--debug':
            debug = 1
        elif o in ('-x', '--xscreensaver-mode'):
            options['check_xss'] = 1
        elif o in ('-n', '--next-tracks'):
            try:
                val = int(a)
                if not val >= 0:
                    raise OptionError, "%d must be zero or greater" % val
            except ValueError:
                raise OptionError, "%s is not an integer" % a
            options['display_next_titles'] = val            
        elif o in ('-d', '--display'):
            options['display'] = a
        elif o in ('-f', '--font'):        
            options['font'] = a
        elif o in ('-t', '--timeout'):
            try:
                val = int(a)
                if not val > 0:
                    raise OptionError, "%d must be greater than zero" % val
            except ValueError:
                raise OptionError, "%s is not an integer" % a
            options['timeout'] = val
        elif o in ('-p', '--placement'):            
            options['placement'] = a
        elif o in ('-s', '--xmms-session'):
            try:
                val = int(a)
                if not val >= 0:
                    raise OptionError, "%d must be zero or greater" % val
            except ValueError:
                raise OptionError, "%s is not an integer" % a
            options['xmms_session'] = val 
        else:
            raise OptionError, "%s is an unknown option" % o

    return options, args

def display(options, text):
    args = [options['yaf_splash_cmd'],
            '-font', options['font'],
            '-placement', options['placement'],
            '-timeout', str(options['timeout']),
            '-text', text]
    if options['transparent']:
        args.append('-transparent')

    if options['display'] != None:
        args.append('-display')
        args.append(options['display'])

    res = os.spawnv(os.P_WAIT, options['yaf_splash_cmd'], args)
    if res > 0:
        raise OSError, "yaf-splash returned %d" % res
    elif res < 0:
        raise OSError, "yaf-splash was killed with signal %d" % abs(res)
    return
    
def get_next_tracks(options):
    session = options['xmms_session']
    i = 0
    titles_printed = 0
    next_text = "Tracks Coming Up:"
    while titles_printed < options['display_next_titles']:
        i += 1
        if ((xmms.get_playlist_pos(session) + i + 1)
            >= xmms.get_playlist_length(session)):
            # Hit the end of the playlist
            break
        current_pos = xmms.get_playlist_pos(session=session)        
        next_title = xmms.get_playlist_title(
            xmms.get_playlist_pos(session) + i, session)
        if separator.match(next_title):
            # I occasionaly use fake tracks containing white space and
            # dashes as separators, but I don't want to see them here.
            continue
        else:
            next_text += "\n" + next_title.strip()
            titles_printed += 1
    return next_text

def main():
    global default_options
    options, args = parse_options(default_options)
    active = 1
    
    # Set up an xscreensaver watcher
    xs_watch_h = None
    if options['check_xss']:
        xs_watch_h = os.popen('%s -watch' % options['xss_cmd'], 'r')
        active = 0
        
    last_title = ""
    current_title = ""
    session = options['xmms_session']
    
    while(1):

        # Check up on xscreensaver
        if options['check_xss']:        
            while (1):
                (input,output,exc) = select.select([xs_watch_h],[],[],0)
                if len(input):
                    for fh in input:
                        line = fh.readline()
                        if line[0:5] == 'BLANK' or line[0:4] == 'LOCK':
                            active = 1
                        elif line[0:7] == 'UNBLANK':
                            active = 0
                else:
                    break

        if active and xmms.get_playlist_length(session):
            if not xmms.is_playing(session):
                current_title = ""
            else:
                # Find out what's playing        
                current_title = xmms.get_playlist_title(
                    xmms.get_playlist_pos(session), session)
                current_title = current_title.strip()
                # We are tacitly assuming that the playlist doesn't happen
                # to have two tracks of the same name in a row.
                if current_title != last_title:
                    next_text = ""
                    if options['display_next_titles']:
                        next_text = get_next_tracks(options)
                        
                    display(options,current_title + '\n\n' + next_text)
                    
                    last_title = current_title

        time.sleep(options['sleep'])




if __name__ == '__main__': main()
