#!/usr/bin/env python
#
# moosic - the client portion of the moosic jukebox system.
#
# Copyright (C) 2001 Daniel Pearson <dpears2@umbc.edu>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

VERSION = "1.2.5"

# This function is just a fun little tool for debugging purposes. It has no
# purpose directly related to moosic.
def browse_object(x):
    print '-'*40
    for a in dir(x):
        print a, '-', type(getattr(x, a))
        print getattr(getattr(x, a), "__doc__", None)
        print '-'*40

def shuffle(seq):
    """Returns a shuffled version of the given sequence.

    The returned list contains exactly the same elements as the given sequence,
    but in a random order.
    """
    import whrandom
    shuffled = []
    seq = list(seq)
    while seq: shuffled.append(seq.pop(whrandom.choice(range(len(seq)))))
    return shuffled

if __name__ == "__main__":
    import sys, os.path, fileinput
    
    USAGE = "usage: " + os.path.basename(sys.argv[0]) + \
        " [options] <command>" + '''
    Options:
        -d, --shuffle-dir       Shuffle the results of recursing through a
                                directory argument.
        -a, --shuffle-args      Shuffle the arguments specified on the command
                                line.
        -g, --shuffle-global    Shuffle everything in the playlist.  This is
                                the default behavior.
        -o, --inorder           Don't shuffle the playlist at all.
        -n, --no-file-munge     Don't change any names given in a filelist.
                                Useful if your filelist consists of URLs or
                                other objects that aren't local files.
        -S, --showcommands      Print the list of possible commands and exit.
        -h, --help              Print this help text and exit.
        -v, --version           Print version information and exit.
                       This Moosic has Super Cow Powers.
    '''
    COMMANDS = '''\
------------------------ Adding to the playlist ------------------------
append <filelist> - Add the files to be played to the end of the playlist.

add <filelist> - An alias for "append".

pl-append <playlists> - Add the files listed in the given playlist files to the
    end of the playlist.

pl-add <playlists> - An alias for "pl-append".

prepend <filelist> - Add the files to be played to the beginning of the
    playlist.

pl-prepend <playlists> - Add the items listed in the given playlist files to
    the beginning of the playlist.

mixin <filelist> - Add the files to the playlist and reshuffle the entire
    playlist.

pl-mixin <playlists> - Add the files listed in the given playlist files to the
    playlist and reshuffle the entire playlist.

putback - Reinsert the current song at the start of the playlist.

insert <filelist> <index> - Insert the given items at a given point in the
    playlist. The items are inserted such that they will precede the item that
    previously occupied the specified index.

pl-insert <playlists> <index> - Insert the files specified in the given
    playlist files at a specified point in the playlist.

------------------------ Removing from the playlist ------------------------
cut <range> - Removes all playlist items that fall within the given range. A
    range is a pair of colon-separated numbers. Such a range addresses all
    items whose index in the playlist is both greater than or equal to the
    first number and less than the second number. Thus, "3:7" addresses items
    3, 4, 5, and 6. If the first number in the pair is omitted, then the range
    starts at the beginning of the playlist. If the second number in the pair
    is omitted, then the range continues to include the last item in the
    playlist. A range can also be a single number (with no colon), in which
    case it addresses the single item whose index is that of the given number.
    Negative numbers may be used to index items from the end of the list
    instead of the beginning. Thus, -1 refers to the last item in the playlist,
    -2 refers to the second-to-last item, etc.

crop <range> - Removes all playlist items that do NOT fall within the given
    range.

remove <regex-list> - Remove all playlist items that match the given regular
    expression, or list of regular expressions.

filter <regex-list> - Remove all playlist items that do NOT match the given
    regular expression, or list of regular expressions.

clear - Clear the playlist.

wipe - Clear the playlist and stop the current song.

------------------------ Rearranging the playlist ------------------------
move <range> <index> - Moves all items in the given range to a new position in
    the playlist.

reshuffle - Reshuffle the playlist.

shuffle - An alias for "reshuffle".

sort - Rearrange the playlist in sorted order.

reverse - Reverse the order of the playlist.

------------------------ Querying for information ------------------------
current - Print the name of the song that is currently playing.

curr - An alias for "current".

list [range] - Print the list of items in the current playlist. If a range is
    specified, only the items that fall within that range are listed. Remember
    that the playlist does not contain the currently playing song.

plainlist - Print the current playlist without numbering each line. This output
    is suitable for saving to a file which can be reloaded by the "pl-append",
    "pl-prepend", and "pl-mixin" commands.

history [num] - Print a list of files that were recently played. If a number is
    not specified, then the entire history is printed.

hist [num] - An alias for "history".

state - Print the current state of the music daemon.

version - Print version information for both the client and the server, and
    then exit.

------------------------ Player management ------------------------
next - Skip to the next song.

noplay - Tell the music daemon to stop playing any new songs, but without
    interrupting the current song.

stop - Tell the music daemon to stop playing the current song and stop
    processing the playlist.  The current song is put back into the playlist.

sleep - An alias for "stop".

pause - Suspend the current song so that it can be resumed at the exact same
    point at a later time.  Note: this often leaves the sound device locked.

play - Tell the music daemon to resume playing. (Use after "stop", "sleep",
    "noplay", or "pause".)

wake - An alias for "play".

reconfigure - Tell the music daemon to reload its configuration file.

reconfig - An alias for "reconfigure".

showconfig - Query and print the music daemon's filetype associations.

exit - Tell the music daemon to quit.

quit - An alias for "exit".

die - An alias for "exit".

                    This Moosic has Super Cow Powers.'''

    # Option processing.
    import getopt
    opts = {'shuffle-global':1, 'shuffle-args':0, 'shuffle-dir':0,
            'debug':0, 'file-munge':1}
    try:
        options, arglist = getopt.getopt(sys.argv[1:], 'ogadnhvS',
                ['inorder', 'shuffle-global', 'shuffle-args', 'shuffle-dir',
                 'no-file-munge', 'help', 'version', 'showcommands'])
    except getopt.error, e:
        print 'Option processing error:', e
        sys.exit(1)
    for opt, val in options:
        if opt == '-v' or opt == '--version':
            print "moosic", VERSION
            print """
Copyright (C) 2001 Daniel Pearson <dpears2@umbc.edu>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."""
            sys.exit(0)
        if opt == '-h' or opt == '--help':
            print USAGE
            sys.exit(0)
        if opt == '-S' or opt == '--showcommands':
            print COMMANDS
            sys.exit(0)
        if opt == '-o' or opt == '--inorder':
            opts['shuffle-global'] = 0
            opts['shuffle-args'] = 0
            opts['shuffle-dir'] = 0
        if opt == '-g' or opt == '--shuffle-global':
            opts['shuffle-global'] = 1
        if opt == '-a' or opt == '--shuffle-args':
            opts['shuffle-args'] = 1
            opts['shuffle-global'] = 0
        if opt == '-d' or opt == '--shuffle-dir':
            opts['shuffle-dir'] = 1
            opts['shuffle-global'] = 0
        if opt == '-n' or opt == '--no-file-munge':
            opts['file-munge'] = 0

    if not arglist:
        print "You must provide a command."
        print "Use the --showcommands option to learn what commands are availble."
        print "Use the --help option to learn what options are availble."
        print "usage:", os.path.basename(sys.argv[0]), "[options] <command>"
        sys.exit(1)
    command = arglist.pop(0)
    import string
    command = string.lower(command)

    # Certain commands require arguments.
    if command in ('append', 'add', 'prepend', 'mixin', 'debug', 'remove',
                   'filter', 'pl-add', 'pl-append', 'pl-mixin', 'pl-prepend',
                   'cut', 'crop', 'insert', 'pl-insert', 'move'):
        if not arglist:
            print 'The "%s" command requires at least one argument.' % command
            sys.exit(1)

    # Pluck off the "index" argument to the insert and pl-insert commands
    # before processing the rest of their argument lists.
    if command == 'insert' or command == 'pl-insert':
        insert_position = arglist.pop()

    # Turn playlists into filenames.
    if command in ('pl-add','pl-append','pl-mixin','pl-prepend','pl-insert'):
        playlists = arglist
        arglist = []
        import re
        for playlist in playlists:
            for line in fileinput.input(playlist):
                # skip empty lines
                if re.search(r'^\s*$', line):
                    continue
                # skip lines that begin with a '#' character
                if re.search(r'^#', line):
                    continue
                # chomp off trailing newlines
                if line[-1] == '\n':
                    line = line[:-1]
                arglist.append(line)

    # Process the filelist that follows certain commands.
    if command in ('append', 'add', 'prepend', 'mixin', 'insert', 'debug'):
        # When dealing with local files, all filenames should be specified by
        # their absolute paths before telling the server to play them.  Using
        # relative pathnames would be foolish because the server has no idea
        # what our current working directory is.
        if opts['file-munge']:
            arglist = map(os.path.abspath, arglist)
        if opts['shuffle-args']:
            arglist = shuffle(arglist)
        # If an item in the filelist is a directory, then recurse through the
        # directory, replacing the item with its children.
        pos = 0
        while pos < len(arglist):
            if os.path.isdir(arglist[pos]):
                dir = arglist.pop(pos)
                # Portability note: the Unix "find" program is required to be
                # present on the system. It is almost unthinkable that you
                # could have a Unix system that didn't have the "find" command,
                # but you never know.
                contents = \
                  os.popen("find '"+dir+"' -follow -type f -print").readlines()
                if opts['shuffle-dir']:
                    contents = shuffle(contents)
                else:
                    contents.sort()
                    contents.reverse()
                for item in contents:
                    arglist.insert(pos, item[:-1])
            pos = pos + 1
        if opts['shuffle-global']:
            arglist = shuffle(arglist)

    # Portability note: Unix domain sockets are used to implement interprocess
    # communication between the client and the server. This prevents this
    # program from working on (most, if not all) non-Unix systems.
    import socket
    server_addr = os.path.join(os.environ['HOME'], '.moosic',
            'server-' + socket.gethostname())
    client_addr = os.path.join(os.environ['HOME'], '.moosic',
            'client-' + socket.gethostname())
    try:
        music_server = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_server.connect(server_addr)
    except socket.error, e:
        import errno
        if e.args[0] in (errno.ECONNREFUSED, errno.ENOENT):
            print "Error: The server (moosicd) doesn't seem to be running."
            sys.exit(1)
        else:
            print "Unknown socket error:", e.args[1]
            sys.exit(1)

    if command == 'debug':
        # debug!
        for item in arglist: print item
    #------------------------------#
    elif command in ('append', 'add', 'pl-add', 'pl-append'):
        for item in arglist:
            music_server.send('APPEND ' + item)
    #------------------------------#
    elif command == 'prepend' or command == 'pl-prepend':
        arglist.reverse()
        for item in arglist:
            music_server.send('PREPEND ' + item)
    #------------------------------#
    elif command == 'insert' or command == 'pl-insert':
        arglist.reverse()
        for item in arglist:
            music_server.send('INSERT %s %s' % (insert_position, item))
    #------------------------------#
    elif command == 'clear':
        music_server.send('CLEAR .')
    #------------------------------#
    elif command == 'noplay':
        music_server.send('NOPLAY .')
    #------------------------------#
    elif command == 'wipe':
        music_server.send('CLEAR .')
        music_server.send('NEXT .')
    #------------------------------#
    elif command == 'sleep' or command == 'stop':
        music_server.send('PUTBACK .')
        music_server.send('NOPLAY .')
        music_server.send('NEXT .')
    #------------------------------#
    elif command == 'pause':
        music_server.send('PAUSE .')
    #------------------------------#
    elif command == 'wake' or command == 'play':
        music_server.send('PLAY .')
    #------------------------------#
    elif command == 'reshuffle' or command == 'shuffle':
        music_server.send('SHUFFLE .')
    #------------------------------#
    elif command == 'sort':
        music_server.send('SORT .')
    #------------------------------#
    elif command == 'reverse':
        music_server.send('REVERSE .')
    #------------------------------#
    elif command == 'mixin' or command == 'pl-mixin':
        for item in arglist:
            music_server.send('APPEND ' + item)
        music_server.send('SHUFFLE .')
    #------------------------------#
    elif command == 'curr' or command == 'current':
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('CURR .')
        print music_client.recv(2**12)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'state':
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('STATE .')
        print music_client.recv(2**12)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'list':
        if len(arglist) > 1:
            print "Warning: the list command takes at most one argument."
            print "Extraneous arguments beyond the first will be ignored."
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        if arglist:
            music_server.send('LIST ' + arglist[0])
        else:
            music_server.send('LIST .')
        listitem = music_client.recv(2**12)
        # '\0' is the End-Of-List marker
        while listitem[0] != '\0':
            print listitem
            listitem = music_client.recv(2**12)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'plainlist':
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('LIST PLAIN')
        listitem = music_client.recv(2**12)
        # '\0' is the End-Of-List marker
        while listitem[0] != '\0':
            print listitem
            listitem = music_client.recv(2**12)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'next':
        music_server.send('NEXT .')
    #------------------------------#
    elif command == 'remove':
        for pattern in arglist:
            music_server.send('REMOVE ' + pattern)
    #------------------------------#
    elif command == 'filter':
        for pattern in arglist:
            music_server.send('FILTER ' + pattern)
    #------------------------------#
    elif command == 'move':
        if len(arglist) == 2:
            music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
            music_client.bind(client_addr)
            music_server.send('MOVE %s %s' % (arglist[0], arglist[1]))
            errmsg = music_client.recv(2048)
            if errmsg: print errmsg
            music_client.close()
            os.remove(client_addr)
        else:
            print "Error: the move command requires exactly two arguments."
    #------------------------------#
    elif command == 'cut':
        if len(arglist) != 1:
            print "Warning: the cut command takes only one argument."
            print "Extraneous arguments beyond the first will be ignored."
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('CUT ' + arglist[0])
        errmsg = music_client.recv(2048)
        if errmsg: print errmsg
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'crop':
        if len(arglist) != 1:
            print "Warning: the crop command takes only one argument."
            print "Extraneous arguments beyond the first will be ignored."
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('CROP ' + arglist[0])
        errmsg = music_client.recv(2048)
        if errmsg: print errmsg
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'putback':
        music_server.send('PUTBACK .')
    #------------------------------#
    elif command == 'history' or command == 'hist':
        if len(arglist) > 1:
            print "Warning: the history command takes at most one argument."
            print "Extraneous arguments beyond the first will be ignored."
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        if arglist:
            music_server.send('HISTORY ' + arglist[0])
        else:
            music_server.send('HISTORY .')
        print music_client.recv(2**12)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command in ('die', 'quit', 'exit'):
        music_server.send('DIE .')
    #------------------------------#
    elif command == 'reconfig' or command == 'reconfigure':
        music_server.send('RECONFIG .')
    #------------------------------#
    elif command == 'version':
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('VERSION .')
        print 'Moosic Client version:', VERSION
        print 'Moosic Server version:', music_client.recv(64)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command == 'showconfig':
        music_client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        music_client.bind(client_addr)
        music_server.send('SHOWCONFIG .')
        print music_client.recv(2048)
        music_client.close()
        os.remove(client_addr)
    #------------------------------#
    elif command[:3] == 'moo':
        print '''\
    (___)   (___)  (___) (___)      (___)   (___)   (___) (___)
    (o o(___)o o)(___)o) (o o) (___)(o o(___)o o)(___) o) (o o)(___)
     \ /(o o)\ / (o o)/ (___)  (o o) \ /(o o)\ / (o o) /(___)/ (o o)
      O  \ /  O   \ /O  (o o)   \ /   O  \ /  O   \ / O (o o)   \ / 
    (__)  O (==)   O(__) \ /(||) O  (__)  O (__)    (  ) \ /(__) O
    (oo)(__)(oo)(__)(oo)(__)(oo)(__)(oo)(__)(##)(__)(oo)(__)(oo)(__)
     \/ (oo) \/ (oo) \/ (,,) \/ (oo) \/ (oo) \/ (oo) \/ (--) \/ (OO)
    (__) \/ (__) \/ (__) \/ (__) \/ (__) \/ (__) \/ (,,) \/ (__) \/  
    (**)(__)(--)(__)(oo)(__)(00)(__)(oo)(__)(oo)(__)(oo)(__)(oo)(__)
     \/ (oo) \/ (oo) \/ (oo) \/ (oo) \/ (**) \/ (OO) \/ (??) \/ (oo)
    (__) \/ (__) \/ (__) \/ (__) \/ (__) \/ (__) \/ (__) \/ (__) \/
    (oo)(__)(oo)(__)(@@)(__)(oo)(__)(oo)(__)(oo)(__)(-0)(,,)(oo)(__)
     \/ (o_) \/ (oo) \/ (oo) \/ (o#) \/ (oo) \/ (oo) \/ (oo) \/ (oo) 
         \/      \/      \/      \/      \/      \/      \/      \/


         (__)
        ,'-00                       The Local Moosical Society
       / /\_|      /                       in Concert
      /  |       _/__________________
     |   \===^__|                 __|
    _|___ /\  |___________________|
    |=====| |   I              I
    *I   I| |   I              I
     I   I^ ^   I              I                     -cfbd-'''
    #------------------------------#
    else:
        print 'Error: invalid command: "' + command + '"'
        print "Use the --showcommands option to learn what commands are availble."
        print "use the --help option to learn about other possible options."
        print "usage:", os.path.basename(sys.argv[0]), "[options] <command>"
        music_server.close()
        sys.exit(1)
    music_server.close()
