def auth_required(f):
    def wrap(self, request, *args, **kwargs):
        if SESSION_KEY not in request.session:
            # Malformed session detected.
            return self.on_unauthorized()

        try:
            request.user = User.objects.get(email=request.session['email'])
            return f(self, request, *args, **kwargs)
        except ObjectDoesNotExist:
            return self.on_unauthorized()
    return wrap class ErrorCode:
    """Error codes for HTTP response.

    """
    DUPLICATED_EMAIL = '100'
    INVALID_EMAIL = '101'
    PASSWORD_TOO_SHORT = '102'
    EMAIL_NOT_EXIST = '103'
    PASSWORD_MISMATCH = '104'
    DUPLICATED_PLAYLIST_NAME = '105'
    TRACK_ALREADY_EXIST = '106'
    PLAYLIST_NOT_EXIST = '107'
    TRACK_NOT_EXIST = '108'
    RESULT_NOT_READY = '109'
    INVALID_ACCESS = '110' class UserException(DropbeatException):
    pass class TrackException(DropbeatException):
    pass class PlaylistException(DropbeatException):
    pass class DropbeatException(Exception):
    pass class User(AbstractBaseUser, SerializeMixins):
    USERNAME_FIELD = 'email'

    class Meta:
        db_table = 'user'

    objects = UserManager()

    # Email is an unique user identifier
    email = models.CharField(max_length=128, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    _serializable = ['email', 'created_at']

    def __repr__(self):
        return '<User %s:%s>' % (self.id, self.email) class Track(models.Model, SerializeMixins):
    """Track
    @field: name
        Track name from streaming sources.
        This name is a string field which will not be synchronized with the
        track name of the original source.

    @field: uid
        Unique id which is used to identify track in each service

    @field: source
        Streaming sources

    @field: created_at
        Date when this track is added to playlist

    """
    class Meta:
        db_table = 'track'

    objects = TrackManager()

    name = models.CharField(max_length=128, null=False)
    uid = models.CharField(max_length=64, null=False)
    source = models.CharField(max_length=16, choices=TrackSource.choices())
    created_at = models.DateTimeField(auto_now_add=True)
    playlist = models.ForeignKey(
        'Playlist', null=False, related_name='tracks')

    _serializable = ['name', 'uid', 'source']

    def __repr__(self):
        return '<Track %s:%s>' % (self.id, self.name) class Playlist(models.Model, SerializeMixins):
    """Playlist

    @field: uid
        Use this uid to identify playlist (per user) as we don't want to reveal
        sequential primary key to client. However, making it a primary key is
        not a good idea because 32 bytes primary key will make every secondary
        index to become much longer while degrading mysql performance.
        Also, even if md5 collision happens, there will be no problem unless
        duplicated uids belong to a single user.

    @field: name
        Playlist name

    @field: user
        Playlist owner

    """
    class Meta:
        db_table = 'playlist'

    objects = PlaylistManager()

    uid = models.CharField(max_length=32, null=False)
    name = models.CharField(max_length=128, null=False)
    user = models.ForeignKey(
        'User', null=False, related_name='playlists')
    created_at = models.DateTimeField(auto_now_add=True)

    _serializable = ['uid', 'name', 'created_at']

    def __repr__(self):
        return '<Playlist %s:%s>' % (self.id, self.name) @app.task
def youtube_search(query):
    return Youtube.search_from_html(query) class Youtube(object):
    @staticmethod
    def search_from_html(keyword):
        """Get search result by parsing youtube HTML.
        Youtube api v3 lacks some necessary data such as video duration,
        quality. Parsing youtube search result directly is the only way to
        get those information.
        Because this is not legitimate access to youtube, machine ip could be
        banned as the number of req/s increases.
        Request throttling with multiple proxies can be a possible workaround.
        (or simply forget about the duration..)

        """
        try:
            # Because this query will be contained in GET param.
            q = urllib.quote(keyword)
        except Exception:
            q = keyword

        url = 'https://www.youtube.com/results?search_query=' + q
        try:
            resp = requests.get(url, timeout=5.0)
            resp.raise_for_status()
        except requests.exceptions.Timeout:
            return []
        except requests.exceptions.HTTPError:
            # Log this error.
            return []

        # Ignore magic numbers as HTML parsers are hardly reusable
        soup = BeautifulSoup(resp.content, 'lxml')
        elems = soup.find_all('div', {'class': 'yt-lockup-content'})

        # `elems` contains some ads and real search results.
        # Filter search result only.
        elems = [x for x in elems if x.find('a').has_attr('aria-describedby')]

        tracks = []
        for elem in elems:
            try:
                uid = elem.find_all('a')[0]['href'].split('v=')[1]
                title = elem.find('a', {
                    'aria-describedby': lambda x: x is not None})['title']
                duration = elem.find('span',
                    {'class': 'accessible-description'}).text.split(': ', 1)[1]
                # Remove garbage postfix from duration
                duration = duration[:duration.find(':') + 3]

                tracks.append({
                    'title': title,
                    'id': uid,
                    'duration': duration,
                    'thumbnail': 'https://i.ytimg.com/vi/%s/hqdefault.jpg' % uid
                })
            except Exception:
                # 1. HTML form has been changed.
                # 2. Search result can contain non-video elems such as channel,
                # playlist. In this case, as elem HTML doesn't fit into
                # video form, exception occurs.
                # TODO: Should do logging only for case 1
                continue

        return tracks class FileBroker(object):
    """For debug mode, we don't need to use asynchronous search with celery
    and redis broker.
    To achieve this without modifying client code, server mimics celery
    behavior and replaces redis with txt file.
    NOTE that `shelve` isn't thread safe. That being said, this object should
    never be used in production env.

    """
    _FILENAME = 'filebroker'

    def __init__(self):
        self._broker = shelve.open(
            os.path.join(settings.BASE_DIR, self._FILENAME))

    def get(self, key):
        if self._broker.has_key(key):
            return self._broker[key]

    def set(self, value):
        """`key` is generated by random hashing and returned.

        """
        key = gen_random_md5()
        self._broker[key] = value
        return key

    def close(self):
        self._broker.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.close() class Resource(object):
    """Base resource class.
    Provides easy and straightforward interface to route paths.
    NOTE that all resources that inherits from this class must be named as
    `(resource_name)Resource`.

    """
    def __init__(self, api_name=None):
        self._api_name = api_name
        self._prepared = []

    @property
    def name(self):
        resource_name = type(self).__name__.lower()
        if 'resource' in resource_name:
            key = 'resource'
        elif 'handler' in resource_name:
            key = 'handler'

        return resource_name[:resource_name.find(key)]

    @property
    def urls(self):
        self.prepare_urls()
        urls = self._prepared[:]
        urls += self._base_urls()
        return urls

    def prepare_urls(self):
        """Override it to hook `urls` method.
        Call `add_path` here.

        """
        pass

    def add_path(self, path, matcher=None):
        """Custom resource returns 'url' object with appropriate
        url configuration and wrap

        """
        matcher = path if matcher is None else matcher
        api_name = 'api_%s_%s' % (self.name, path)

        self._prepared.append(url(
            r'^(?P<resource_name>%s)/%s$' % (self.name, matcher),
            self.wrap_dispatcher(),
            name=api_name
        ))

    def _base_urls(self):
        """The standard URLs this `Resource` should respond to.

        """
        alias = 'api_%s' % self.name
        return [
            url(r'^(?P<resource_name>%s)$' % (self.name),
                self.wrap_dispatcher(),
                name=alias
            ),
        ]

    def wrap_dispatcher(self):
        @csrf_exempt
        def wrapper(request, *args, **kwargs):
            # TODO: Handle exception in production level by checking debug flag.
            # TODO: Check content-type as we only want json request.
            try:
                handler = getattr(self, self._find_dispatcher(request))

                try:
                    response = handler(request, *args, **kwargs)
                except Exception as e:
                    traceback.print_exc()
                    raise ResourceError(e.args[0])
                return response
            except AttributeError as e:
                return self.on_not_allowed()
            except PathError as e:
                return self.on_not_found(e.args[0])
            except ResourceError as e:
                return self.on_server_error(e.args[0])
            except Exception as e:
                traceback.print_exc()
                return self.on_server_error(e.args[0])

        return wrapper

    def _find_dispatcher(self, request):
        """Find dispacher method.
        `/api/v1/user/` -> handle_get
        `/api/v1/user/signin/` -> handle_get_signin

        """
        m = re.search(r'\/v[0-9]*\/', request.path)
        if m is None:
            # Api does not have a name. (no versioning)
            full_name = request.path[request.path.find('/', 1) + 1:-1]
        else:
            full_name = request.path[m.end():]

        # XXX: Assume that trailing slash is not attached to request path.
        dispatcher_prefix = 'handle_' + request.method.lower()
        paths = full_name.split('/')
        if not paths or paths[0] != self.name:
            raise PathError('404')
        if len(paths) == 1:
            return dispatcher_prefix
        elif len(paths) > 1:
            return dispatcher_prefix + '_' + '_'.join(paths[1:])

    def _get_params(self, request, encode=True, validate=None):
        params = {}
        if request.method == 'GET':
            # Use only the first value of the param.
            for k, v in dict(request.GET).items():
                params[k] = v[0]
        else:
            if request.POST:
                params = request.POST
            else:
                try:
                    params = json.loads(request.body)
                except ValueError:
                    raise ResourceError('Invalid request body')

        def convert(data):
            if isinstance(data, basestring):
                return data.encode('utf8')
            elif isinstance(data, collections.Mapping):
                return dict(map(convert, data.iteritems()))
            elif isinstance(data, collections.Iterable):
                return type(data)(map(convert, data))
            else:
                return data

        if encode:
            params = convert(params)

        if validate is not None:
            for i in validate:
                if i not in params.keys():
                    raise ResourceError('Parameter validation failed: %s' % i)
        return params

    def on_success(self, data=None):
        resp = {'success': True}
        if data is not None:
            resp.update(data)
        return self.json_response(resp)

    def on_error(self, error=None):
        return self.json_response({
            'success': False, 'error': error
        })

    def on_created(self, obj, json_resp=True):
        if json_resp:
            resp = {'success': True}
            resp.update(obj)
            return self.json_response(resp, status=201)
        else:
            raise NotImplementedError

    def on_not_allowed(self):
        return HttpResponseNotAllowed('Method not allowed')

    def on_unauthorized(self, msg=None):
        return HttpResponse(msg or 'Authentication required', status=401)

    def on_not_found(self, msg):
        return HttpResponseNotFound(msg)

    def on_server_error(self, msg):
        return HttpResponseServerError(msg)

    def on_bad_request(self, msg):
        return self.json_response({
            'success': False, 'error': msg
        }, status=400)

    def on_redirect(self, url):
        return HttpResponseRedirect(url)

    def response(self, msg):
        return HttpResponse(msg)

    def json_response(self, data, status=200):
        """Render json response.
        @param data: Should be `dict` of response data.

        """
        return HttpResponse(
            json.dumps(data), status=status, content_type='application/json') def parameters(list_, force=True):
    def wrap(f):
        def inner_wrap(self, request, *args, **kwargs):
            try:
                if force:
                    request.p = self._get_params(request, validate=list_)
                else:
                    request.p = self._get_params(request)
            except Exception as e:
                traceback.print_exc()
                return self.on_bad_request(e.args[0])
            return f(self, request, *args, **kwargs)
        return inner_wrap
    return wrap
from dropbeat.auth import auth_required
from dropbeat.constants import ErrorCode
from dropbeat.exceptions import (
    UserException, TrackException, PlaylistException, DropbeatException)
from dropbeat.models import User, Track, Playlist
from dropbeat.youtube import youtube_search, Youtube
from dropbeat.utils import FileBroker
from toolkit.resource import Resource, parameters
from django.db import IntegrityError
from django.contrib.auth import login, logout, authenticate
from django.conf import settings

        """
        try:
            playlist = Playlist.objects. \
                fetch_playlist(request.user, request.p['uid'])
        except PlaylistException as e:
            return self.on_error(error=e.args[0])

        # One more query.
        tracks = [x.to_dict() for x in playlist.tracks.all()]

        playlist = playlist.to_dict()
        playlist['tracks'] = tracks
        return self.on_success(data={'playlist': playlist})

    @auth_required
    def handle_get_list(self, request, *args, **kwargs):
        return self.on_success(data={
            'list': Playlist.objects.fetch_all_uids(request.user)})

    @auth_required
    def handle_get_all(self, request, *args, **kwargs):
        playlists = [x.to_dict()
            for x in Playlist.objects.filter(user=request.user)]
        return self.on_success(data={'data': playlists})

    @auth_required
    @parameters(['name'])
    def handle_post(self, request, *args, **kwargs):
        """Create a playlist.

        """
        try:
            playlist = Playlist.objects. \
                create_playlist(request.user, request.p['name'])
        except PlaylistException as e:
            return self.on_error(error=e.args[0])

        return self.on_success(data={'playlist': playlist.to_dict()})

    @auth_required
    @parameters(['uid', 'name'])
    def handle_put(self, request, *args, **kwargs):
        """Change a name of the playlist.

        """
        try:
            Playlist.objects.change_playlist_name(
                request.user, request.p['uid'], request.p['name'])
        except PlaylistException as e:
            return self.on_bad_request(e.args[0])

        return self.on_success()

    @auth_required
    @parameters(['uid'])
    def handle_delete(self, request, *args, **kwargs):
        """Delete a playlist.

        """
        try:
            Playlist.objects.remove_playlist(request.user, request.p['uid'])
        except PlaylistException as e:
            return self.on_bad_request(e.args[0])

        return self.on_success()


class TrackResource(DropbeatResource):
    @auth_required
    @parameters(['uid', 'name', 'playlist_uid'])
    def handle_post(self, request, *args, **kwargs):
        """Add track to playlist.

        """
        try:
            playlist = Playlist.objects. \
                fetch_playlist(request.user, request.p['playlist_uid'])
            track = Track.objects.create_track(
                request.p['name'], request.p['uid'], playlist)
        except DropbeatException as e:
            return self.on_error(error=e.args[0])

        return self.on_success(data={'track': track.to_dict()})

    @auth_required
    @parameters(['uid', 'playlist_uid'])
    def handle_delete(self, request, *args, **kwargs):
        """Remove track from playlist.

        """
        try:
            playlist = Playlist.objects. \
                fetch_playlist(request.user, request.p['playlist_uid'])
            Track.objects.remove_track(request.p['uid'], playlist)
        except DropbeatException as e:
            return self.on_error(error=e.args[0])

        return self.on_success()


class SearchResource(DropbeatResource):
    def prepare_urls(self):
        self.add_path('async')

    @parameters(['q'])
    def handle_get(self, request, *args, **kwargs):
        """This method blocks working thread while doing HTTP i/o
        XXX: Do not use this api in production env.

        """
        data = Youtube.search_from_html(request.p['q'])
        return self.on_success(data={'data': data})

    @parameters(['q'])
    def handle_post_async(self, request, *args, **kwargs):
        """Register async task.

        """
        if settings.DEBUG:

            result = Youtube.search_from_html(request.p['q'])