#!/usr/bin/env python
"""\
%prog [options] user

Build a playlist of all the tracks published by <user>. <user> must be a URL in
one of the following formats:

    http://soundcloud.com/<user>
    http://official.fm/users/<user>

The playlist is written to the file <user>.xspf.
"""

import codecs
import json
import logging
import sys

from collections import namedtuple
from functools import partial
from itertools import count
from optparse import OptionParser, make_option as Opt
from urllib import urlencode
from urlparse import urljoin, urlparse

import requests

from lxml.builder import E
from lxml import etree as xml

log = logging.getLogger(__name__)

def main():
    optparser = OptionParser(usage=__doc__, option_list=options)
    (opts, args) = optparser.parse_args()

    verbose = int(opts.verbose)
    if verbose > 0:
        log.level = max(1, logging.WARNING - (10 * verbose))
        log.addHandler(logging.StreamHandler())

    (source,) = args
    domain = urlparse(source).netloc

    try:
        service = services[domain]
    except KeyError:
        sys.stderr.write('unsupported service: %s\n' % domain)
        return 1

    user = source.split('/')[-1]
    client = service()

    playlist = Playlist(tracks=client.users(user).tracks)

    with opener(user + '.xspf', 'w') as f:
        f.write(playlist.xspf)

options = [
    Opt('-v', '--verbose', default=0, help='logging verbosity'),
]

Track = namedtuple('Track', 'location creator title info')

class Service(object):
    """Cloud streaming service."""

    def get(self, url, **kwargs):
        """Send a HTTP GET to the service.

        Returns a :class:`requests.Response` object. See :func:`requests.get`
        for more information.
        """
        log.debug('GET %s', url)
        return requests.get(url, **kwargs)

class ServiceUser(object):
    """A user of a cloud streaming service."""

    def __init__(self, user=None, client=None, **kwargs):
        self.user = user
        self.client = client

class SoundCloud(Service):
    """The soundcloud.com streaming service.

    Soundcloud's read-only API only requires a registered client id; oAuth is
    not necessary.
    """
    url = 'https://api.soundcloud.com/'
    id = 'bed25dba5cb38c07ca8d5bb9d92eb0a8'
    limit = 200
    format = 'json'

    def users(self, user):
        """Return a :class:`SoundCloudUser` object.
        
        *user* is a URL like `http://soundcloud.com/<user>`.
        """
        user = user.strip('/').split('/')[-1]
        return SoundCloudUser(user=user, client=self)

    def list(self, collection, **query):
        """Return an iterator yielding items from *collection*.

        *collection* is a resource endpoint in the Soundcloud API (like
        `/users/<user>/tracks`). Items are objects parsed from the API's JSON
        response.
        """
        url = urljoin(self.url, collection)
        params = dict(
            client_id=self.id,
            format=self.format,
            limit=self.limit,
            offset = 0,
            )
        params.update(query)
        
        url = urljoin(self.url, collection)

        while True:
            response = self.get(url + '?' + urlencode(params))
            items = json.loads(response.text)
            if not items:
                return

            params['offset'] += len(items)
            log.debug('received %d items', len(items))
            for item in items:
                yield item

class SoundCloudUser(ServiceUser):
    """A user at soundcloud.com."""

    @property
    def tracks(self):
        """An iterator yield :class:`Track` objects published by the user."""
        resource = '/users/{user}/tracks'.format(user=self.user)
        for track in self.client.list(resource):
            if not track['streamable']:
                continue
            url = track['stream_url'] + '?' + urlencode(dict(client_id=self.client.id))
            url = url.replace('https://', 'http://')
            yield Track(
                title=track['title'],
                creator=track['user']['username'],
                location=url,
                info=track['permalink_url'],
                )
                

class OfficialFM(Service):
    """The official.fm cloud streaming service."""
    url = 'http://official.fm/'
    
    def users(self, user):
        """Returns a :class:`OfficialFMUser` object.
        
        *user* is the user's URL, like `http://official.fm/users/<user>`.
        """
        user = user.strip('/').split('/')[-1]
        return OfficialFMUser(user=user, client=self)

    def list(self, collection):
        """Yields items from *collection*.

        *collection* is a resource endpoint at official.fm (like
        "/users/<user>.xspf"). Items are :class:`xml.ElementBase` objects parsed
        from the API's XML response.
        """
        params = {}
        for page in count(1):
            params['page'] = page

            url = urljoin(self.url, collection + '?' + urlencode(params))
            response = self.get(url)
            doc = xml.fromstring(response.content)
            items = doc.xpath('//item')
            if not items:
                return
            log.debug('received %d items', len(items))
            for item in items:
                yield item

class OfficialFMUser(ServiceUser):
    """A user at official.fm."""

    @property
    def tracks(self):
        """Yields :class:`Track` objects published by the user."""
        for track in self.client.list('/users/{user}.xspf'.format(user=self.user)):
            yield Track(
                title=track.xpath('title/text()')[0],
                creator=track.xpath('author/text()')[0],
                location=track.xpath('enclosure[@type="audio/mpeg"]/@url')[0],
                info=track.xpath('link/text()')[0],
                )

services = {
    'soundcloud.com': SoundCloud,
    'official.fm': OfficialFM,
}

class Playlist(object):
    """A playlist.

    Little more than a list of :class:`Track` objects for now.
    """

    def __init__(self, tracks=None, **kwargs):
        self.tracks = tracks
    
    @property
    def xspf(self):
        """Render the playlist in XSPF format."""
        return xml.tostring(E.playlist(
            E.trackList(*[
                E.track(
                    E.location(unicode(track.location)),
                    E.title(unicode(track.title)),
                    E.creator(unicode(track.creator)),
                    E.info(unicode(track.info)),
                ) for track in self.tracks])),
        pretty_print=True)

opener = partial(codecs.open, encoding='utf-8')

if __name__ == '__main__':
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit()
