{"action":"created","comment":{"url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/comments/70605906","id":70605906,"diff_hunk":"@@ -0,0 +1,358 @@\n+import time\n+import json\n+import re\n+from collections import defaultdict\n+from yattag import Doc\n+import yattag\n+from flask import Blueprint, request, render_template\n+from flask_login import login_required, current_user\n+from webserver.external import messybrainz\n+from webserver.rate_limiter import ratelimit\n+from webserver.errors import InvalidAPIUsage\n+import xmltodict\n+from api_tools import insert_payload\n+from db.mockdata import User, Session, Token\n+\n+api_bp = Blueprint('api_compat', __name__)\n+\n+\n+@api_bp.route('/api/auth/', methods=['GET'])\n+@ratelimit()\n+@login_required\n+def api_auth():\n+    \"\"\" Renders the token activation page.\n+    \"\"\"\n+    token = request.args['token']\n+    return render_template(\n+        \"user/auth.html\",\n+        user_id=current_user.musicbrainz_id,\n+        token=token\n+    )\n+\n+\n+@api_bp.route('/api/auth/', methods=['POST'])\n+@ratelimit()\n+@login_required\n+def api_auth_approve():\n+    \"\"\" Authenticate the user token provided.\n+    \"\"\"\n+    user = User.load_by_name(current_user.musicbrainz_id)\n+    token = Token.load(request.form['token'])\n+    if not token:\n+        return render_template(\n+            \"user/auth.html\",\n+            user_id=current_user.musicbrainz_id,\n+            msg=\"Either this token is already used or invalid. Please try again.\"\n+        )\n+\n+    if token.api_key != user.api_key:\n+        return render_template(\n+            \"user/auth.html\",\n+            user_id=current_user.musicbrainz_id,\n+            msg=\"This token does not belong to this account. Please login in the correct account.\"\n+        )\n+    if token.user:\n+        return render_template(\n+            \"user/auth.html\",\n+            user_id=current_user.musicbrainz_id,\n+            msg=\"This token is already approved. Please check the token and try again.\"\n+        )\n+    if token.has_expired():\n+        return render_template(\n+            \"user/auth.html\",\n+            user_id=current_user.musicbrainz_id,\n+            msg=\"This token has expired. Please create a new token and try again.\"\n+        )\n+    token.approve(user.name)\n+    return render_template(\n+        \"user/auth.html\",\n+        user_id=current_user.musicbrainz_id,\n+        msg=\"Token %s approved for user %s, press continue in client.\" % (token.token, current_user.musicbrainz_id)\n+    )\n+\n+\n+@api_bp.route('/2.0/', methods=['GET'])\n+@ratelimit()\n+def api_get():\n+    \"\"\" Receives the GET-API calls and redirects them to appropriate methods.\n+    \"\"\"\n+    method = request.args['method'].lower()\n+\n+    def invalid_method_error(*args):\n+        raise InvalidAPIUsage(3, output_format=request.args.get('format', \"xml\"))   # Invalid Method\n+\n+    return {\n+        'user.getinfo': user_info,\n+        'auth.gettoken': get_token\n+    }.get(method, invalid_method_error)(request, request.args)\n+\n+\n+@api_bp.route('/2.0/', methods=['POST'])\n+@ratelimit()\n+def api_post():\n+    \"\"\" Receives the POST-API calls and redirects them to appropriate methods.\n+    \"\"\"\n+    method = request.form['method'].lower()\n+\n+    def invalid_method_error(*args):\n+        raise InvalidAPIUsage(3, output_format=request.args.get('format', \"xml\"))   # Invalid Method\n+\n+    return {\n+        'track.updatenowplaying': scrobble_listens,\n+        'track.scrobble': scrobble_listens,\n+        'auth.getsession': get_session\n+    }.get(method, invalid_method_error)(request, request.form)\n+\n+\n+\n+def get_token(request, data):\n+    \"\"\" Issue a token to user after verying his API_KEY\n+    \"\"\"\n+    output_format = data.get(\"format\", \"xml\")\n+    api_key = data.get('api_key', None)\n+\n+    if not api_key:\n+        raise InvalidAPIUsage(6, output_format=output_format)   # Missing required params\n+    if not Token.is_valid_api_key(api_key):\n+        raise InvalidAPIUsage(10, output_format=output_format)   # Invalid API_KEY\n+\n+    token = Token.generate(api_key)\n+\n+    doc, tag, text = Doc().tagtext()\n+    with tag('lfm', status=\"ok\"):\n+        with tag('token'):\n+            text(token.token)\n+    return format_response('<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n' + yattag.indent(doc.getvalue()),\n+                           output_format)\n+\n+\n+def get_session(request, data):\n+    \"\"\" Create new session after validating the API_key and token.\n+    \"\"\"\n+    output_format = data.get(\"format\", \"xml\")\n+    try:\n+        api_key = data['api_key']\n+        token = Token.load(data['token'], api_key)\n+        if token.has_expired():\n+            raise InvalidAPIUsage(15, output_format=output_format)  # Token expired\n+    except KeyError:\n+        raise InvalidAPIUsage(6, output_format=output_format)       # Missing Required Params\n+\n+    if not token:\n+        if not Token.is_valid_api_key(api_key):\n+            raise InvalidAPIUsage(10, output_format=output_format)  # Invalid API_key\n+        raise InvalidAPIUsage(4, output_format=output_format)       # Invalid token\n+\n+    if not token.user:\n+        raise InvalidAPIUsage(14, output_format=output_format)      # Unauthorized token\n+\n+    session = Session.create(token)\n+\n+    doc, tag, text = Doc().tagtext()\n+    with tag('lfm', status=\"ok\"):\n+        with tag('session'):\n+            with tag('name'):\n+                text(session.user.name)\n+            with tag('key'):\n+                text(session.sid)\n+            with tag('subscriber'):\n+                text('0')\n+\n+    return format_response('<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n' + yattag.indent(doc.getvalue()),\n+                           data.get('format', \"xml\"))\n+\n+\n+def _to_native_api(lookup, method=\"track.scrobble\"):\n+    \"\"\" Converts the list of listens received in the new Last.fm submission format\n+        to the native ListenBrainz API format.\n+        Returns: type_of_listen and listen_payload\n+    \"\"\"\n+    listen_type = \"listens\"\n+    if method == \"track.updateNowPlaying\":\n+        listen_type = \"playing_now\"\n+        if len(loopkup.keys()) != 1:\n+            raise InvalidAPIUsage(6, output_format=output_format)       # Invalid parameters\n+\n+    listens = []\n+    for ind, data in lookup.iteritems():\n+        listen = {\n+            'track_metadata': {\n+                'additional_info': {}\n+            }\n+        }\n+        if 'timestamp' in data:\n+            listen['listened_at'] = data['timestamp']\n+        if 'album' in data:\n+            listen['track_metadata']['release_name'] = data['album']\n+        if 'track' in data:\n+            listen['track_metadata']['track_name'] = data['track']\n+        if 'artist' in data:\n+            listen['track_metadata']['artist_name'] = data['artist']\n+        if 'mbid' in data:\n+            listen['track_metadata']['release_mbid'] = data['mbid']\n+        if 'trackNumber' in data or 'duration' in data or 'albumArtist' in data or \\\n+                'choosenByUser' in data or 'streamId' in data or 'context' in data:\n+            # Album artist, Song Duration, trackNumber on the album, etc\n+            # are not supported by the native ListenBrainz API\n+            pass\n+        listens.append(listen)\n+    return listen_type, listens\n+\n+\n+def scrobble_listens(request, data):\n+    \"\"\" Submit the listen in the lastfm format to be inserted in db.\n+        Accepts listens for both track.updateNowPlaying and track.scrobble methods.\n+    \"\"\"\n+    output_format = data.get('format', \"xml\")\n+    try:\n+        sk, api_key = data['sk'], data['api_key']\n+    except KeyError:\n+        raise InvalidAPIUsage(6, output_format=output_format)       # Invalid parameters\n+\n+    session = Session.load(sk, api_key)\n+    if not session:\n+        if not Token.is_valid_api_key(api_key):\n+            raise InvalidAPIUsage(10, output_format=output_format)   # Invalid API_KEY\n+        raise InvalidAPIUsage(9, output_format=output_format)        # Invalid Session KEY\n+\n+    lookup = defaultdict(dict)\n+    for key, value in data.items():\n+        if key == \"sk\" or key == \"token\" or key == \"api_key\" or key == \"method\":\n+            continue\n+        matches = re.match('(.*)\\[(\\d+)\\]', key)\n+        if matches:\n+            key = matches.group(1)\n+            number = matches.group(2)\n+        else:\n+            number = 0\n+        lookup[number][key] = value\n+\n+    # Convert to native payload then submit 'em.\n+    listen_type, native_payload = _to_native_api(lookup, data['method'])\n+    insert_payload(native_payload, session.user.name, listen_type=listen_type)\n+\n+    # With corrections than the original submitted listen.\n+    doc, tag, text = Doc().tagtext()\n+    with tag('lfm', status=\"ok\"):\n+        with tag(\"nowplaying\" if listen_type == \"playing_now\" else \"scrobbles\"):","path":"webserver/views/api_compat.py","position":237,"original_position":237,"commit_id":"80853743e33b069ca052fc11f3f7a8c9f38f7bcc","original_commit_id":"80853743e33b069ca052fc11f3f7a8c9f38f7bcc","user":{"login":"mayhem","id":49745,"avatar_url":"https://avatars.githubusercontent.com/u/49745?v=3","gravatar_id":"","url":"https://api.github.com/users/mayhem","html_url":"https://github.com/mayhem","followers_url":"https://api.github.com/users/mayhem/followers","following_url":"https://api.github.com/users/mayhem/following{/other_user}","gists_url":"https://api.github.com/users/mayhem/gists{/gist_id}","starred_url":"https://api.github.com/users/mayhem/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mayhem/subscriptions","organizations_url":"https://api.github.com/users/mayhem/orgs","repos_url":"https://api.github.com/users/mayhem/repos","events_url":"https://api.github.com/users/mayhem/events{/privacy}","received_events_url":"https://api.github.com/users/mayhem/received_events","type":"User","site_admin":false},"body":"except here as a string constant, the word scrobbles is allowed. :)\r\n","created_at":"2016-07-13T11:09:30Z","updated_at":"2016-07-13T11:09:30Z","html_url":"https://github.com/metabrainz/listenbrainz-server/pull/82#discussion_r70605906","pull_request_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82","_links":{"self":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/comments/70605906"},"html":{"href":"https://github.com/metabrainz/listenbrainz-server/pull/82#discussion_r70605906"},"pull_request":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82"}}},"pull_request":{"url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82","id":74437891,"html_url":"https://github.com/metabrainz/listenbrainz-server/pull/82","diff_url":"https://github.com/metabrainz/listenbrainz-server/pull/82.diff","patch_url":"https://github.com/metabrainz/listenbrainz-server/pull/82.patch","issue_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues/82","number":82,"state":"open","locked":false,"title":"[WIP] Compatible API for lastfm","user":{"login":"pinkeshbadjatiya","id":14854424,"avatar_url":"https://avatars.githubusercontent.com/u/14854424?v=3","gravatar_id":"","url":"https://api.github.com/users/pinkeshbadjatiya","html_url":"https://github.com/pinkeshbadjatiya","followers_url":"https://api.github.com/users/pinkeshbadjatiya/followers","following_url":"https://api.github.com/users/pinkeshbadjatiya/following{/other_user}","gists_url":"https://api.github.com/users/pinkeshbadjatiya/gists{/gist_id}","starred_url":"https://api.github.com/users/pinkeshbadjatiya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pinkeshbadjatiya/subscriptions","organizations_url":"https://api.github.com/users/pinkeshbadjatiya/orgs","repos_url":"https://api.github.com/users/pinkeshbadjatiya/repos","events_url":"https://api.github.com/users/pinkeshbadjatiya/events{/privacy}","received_events_url":"https://api.github.com/users/pinkeshbadjatiya/received_events","type":"User","site_admin":false},"body":"## Compatible API for ListenBrainz (api_compat)\r\nA submission API compatible with Last.fm scrobblers, [more details](https://community.metabrainz.org/t/listenbrainz-a-submission-api-compatible-with-last-fm-scrobblers/2708)\r\n\r\n### Features\r\n- Create new schema: `token` & `session` & `listen_json`.\r\n- Add class for `User`, `Token` and `Session`\r\n- Add complete support for the following methods:\r\n  - **auth.getToken**\r\n  - **auth.getSession**\r\n  - **track.scrobble**\r\n  - **track.updateNowPlaying**\r\n  - **user.getinfo**\r\n- Throw errors with appropriate error codes mentioned [here](http://www.last.fm/api/errorcodes).\r\n- Response rendering can be done in `xml` or `json`\r\n- Generic **xml** -> **json** conversion. Multiple formats can be supported by writing the specific xml parser for that particular format.\r\n- **TOKEN_EXPIRATION_TIME** specifies the max time in minutes before a token expires. Initial value is `60 minutes`\r\n- Separate common bits from the native_API and the compat_API.\r\n- Fixed a few PEP8 and pylint indenting issues.\r\n\r\n### More details about the API\r\n- The API key would be the one given on this [page](https://listenbrainz.org/user/import) (User token)\r\n- Routes for submission: `http://listenbrainz.org/2.0/`\r\n- Allows single token per user.\r\n- Allows multiple sessions per user.\r\n\r\n### Known issues/Open questions for discussion:\r\n- Duplicate token generation might disturb functionality.\r\n- Session limit per API key to avoid overuse of the service? any suggestion?\r\n- Isolate tests, even those belonging to same class.","created_at":"2016-06-20T12:50:33Z","updated_at":"2016-07-13T11:09:30Z","closed_at":null,"merged_at":null,"merge_commit_sha":"99f64b2fc756c1f5f71909aeccaf6f6b3672ba83","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82/commits","review_comments_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82/comments","review_comment_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/comments{/number}","comments_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues/82/comments","statuses_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/statuses/80853743e33b069ca052fc11f3f7a8c9f38f7bcc","head":{"label":"pinkeshbadjatiya:api_compat","ref":"api_compat","sha":"80853743e33b069ca052fc11f3f7a8c9f38f7bcc","user":{"login":"pinkeshbadjatiya","id":14854424,"avatar_url":"https://avatars.githubusercontent.com/u/14854424?v=3","gravatar_id":"","url":"https://api.github.com/users/pinkeshbadjatiya","html_url":"https://github.com/pinkeshbadjatiya","followers_url":"https://api.github.com/users/pinkeshbadjatiya/followers","following_url":"https://api.github.com/users/pinkeshbadjatiya/following{/other_user}","gists_url":"https://api.github.com/users/pinkeshbadjatiya/gists{/gist_id}","starred_url":"https://api.github.com/users/pinkeshbadjatiya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pinkeshbadjatiya/subscriptions","organizations_url":"https://api.github.com/users/pinkeshbadjatiya/orgs","repos_url":"https://api.github.com/users/pinkeshbadjatiya/repos","events_url":"https://api.github.com/users/pinkeshbadjatiya/events{/privacy}","received_events_url":"https://api.github.com/users/pinkeshbadjatiya/received_events","type":"User","site_admin":false},"repo":{"id":53163472,"name":"listenbrainz-server","full_name":"pinkeshbadjatiya/listenbrainz-server","owner":{"login":"pinkeshbadjatiya","id":14854424,"avatar_url":"https://avatars.githubusercontent.com/u/14854424?v=3","gravatar_id":"","url":"https://api.github.com/users/pinkeshbadjatiya","html_url":"https://github.com/pinkeshbadjatiya","followers_url":"https://api.github.com/users/pinkeshbadjatiya/followers","following_url":"https://api.github.com/users/pinkeshbadjatiya/following{/other_user}","gists_url":"https://api.github.com/users/pinkeshbadjatiya/gists{/gist_id}","starred_url":"https://api.github.com/users/pinkeshbadjatiya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pinkeshbadjatiya/subscriptions","organizations_url":"https://api.github.com/users/pinkeshbadjatiya/orgs","repos_url":"https://api.github.com/users/pinkeshbadjatiya/repos","events_url":"https://api.github.com/users/pinkeshbadjatiya/events{/privacy}","received_events_url":"https://api.github.com/users/pinkeshbadjatiya/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/pinkeshbadjatiya/listenbrainz-server","description":"Server for the ListenBrainz project","fork":true,"url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server","forks_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/forks","keys_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/teams","hooks_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/hooks","issue_events_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/issues/events{/number}","events_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/events","assignees_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/assignees{/user}","branches_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/branches{/branch}","tags_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/tags","blobs_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/git/refs{/sha}","trees_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/statuses/{sha}","languages_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/languages","stargazers_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/stargazers","contributors_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/contributors","subscribers_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/subscribers","subscription_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/subscription","commits_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/commits{/sha}","git_commits_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/git/commits{/sha}","comments_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/comments{/number}","issue_comment_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/issues/comments{/number}","contents_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/contents/{+path}","compare_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/merges","archive_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/downloads","issues_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/issues{/number}","pulls_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/pulls{/number}","milestones_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/milestones{/number}","notifications_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/labels{/name}","releases_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/releases{/id}","deployments_url":"https://api.github.com/repos/pinkeshbadjatiya/listenbrainz-server/deployments","created_at":"2016-03-04T20:28:41Z","updated_at":"2016-03-04T20:28:42Z","pushed_at":"2016-07-09T19:34:01Z","git_url":"git://github.com/pinkeshbadjatiya/listenbrainz-server.git","ssh_url":"git@github.com:pinkeshbadjatiya/listenbrainz-server.git","clone_url":"https://github.com/pinkeshbadjatiya/listenbrainz-server.git","svn_url":"https://github.com/pinkeshbadjatiya/listenbrainz-server","homepage":"https://listenbrainz.org/","size":1280,"stargazers_count":0,"watchers_count":0,"language":"CSS","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"metabrainz:master","ref":"master","sha":"aac58f7df849467afd4566ca2539525d734c3a21","user":{"login":"metabrainz","id":293421,"avatar_url":"https://avatars.githubusercontent.com/u/293421?v=3","gravatar_id":"","url":"https://api.github.com/users/metabrainz","html_url":"https://github.com/metabrainz","followers_url":"https://api.github.com/users/metabrainz/followers","following_url":"https://api.github.com/users/metabrainz/following{/other_user}","gists_url":"https://api.github.com/users/metabrainz/gists{/gist_id}","starred_url":"https://api.github.com/users/metabrainz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/metabrainz/subscriptions","organizations_url":"https://api.github.com/users/metabrainz/orgs","repos_url":"https://api.github.com/users/metabrainz/repos","events_url":"https://api.github.com/users/metabrainz/events{/privacy}","received_events_url":"https://api.github.com/users/metabrainz/received_events","type":"Organization","site_admin":false},"repo":{"id":41960013,"name":"listenbrainz-server","full_name":"metabrainz/listenbrainz-server","owner":{"login":"metabrainz","id":293421,"avatar_url":"https://avatars.githubusercontent.com/u/293421?v=3","gravatar_id":"","url":"https://api.github.com/users/metabrainz","html_url":"https://github.com/metabrainz","followers_url":"https://api.github.com/users/metabrainz/followers","following_url":"https://api.github.com/users/metabrainz/following{/other_user}","gists_url":"https://api.github.com/users/metabrainz/gists{/gist_id}","starred_url":"https://api.github.com/users/metabrainz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/metabrainz/subscriptions","organizations_url":"https://api.github.com/users/metabrainz/orgs","repos_url":"https://api.github.com/users/metabrainz/repos","events_url":"https://api.github.com/users/metabrainz/events{/privacy}","received_events_url":"https://api.github.com/users/metabrainz/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/metabrainz/listenbrainz-server","description":"Server for the ListenBrainz project","fork":false,"url":"https://api.github.com/repos/metabrainz/listenbrainz-server","forks_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/forks","keys_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/keys{/key_id}","collaborators_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/teams","hooks_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/hooks","issue_events_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues/events{/number}","events_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/events","assignees_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/assignees{/user}","branches_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/branches{/branch}","tags_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/tags","blobs_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/git/refs{/sha}","trees_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/git/trees{/sha}","statuses_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/statuses/{sha}","languages_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/languages","stargazers_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/stargazers","contributors_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/contributors","subscribers_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/subscribers","subscription_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/subscription","commits_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/commits{/sha}","git_commits_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/git/commits{/sha}","comments_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/comments{/number}","issue_comment_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues/comments{/number}","contents_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/contents/{+path}","compare_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/compare/{base}...{head}","merges_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/merges","archive_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/downloads","issues_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues{/number}","pulls_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls{/number}","milestones_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/milestones{/number}","notifications_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/labels{/name}","releases_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/releases{/id}","deployments_url":"https://api.github.com/repos/metabrainz/listenbrainz-server/deployments","created_at":"2015-09-05T12:30:49Z","updated_at":"2016-07-12T04:54:14Z","pushed_at":"2016-07-09T19:34:02Z","git_url":"git://github.com/metabrainz/listenbrainz-server.git","ssh_url":"git@github.com:metabrainz/listenbrainz-server.git","clone_url":"https://github.com/metabrainz/listenbrainz-server.git","svn_url":"https://github.com/metabrainz/listenbrainz-server","homepage":"https://listenbrainz.org/","size":1282,"stargazers_count":60,"watchers_count":60,"language":"CSS","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":13,"mirror_url":null,"open_issues_count":6,"forks":13,"open_issues":6,"watchers":60,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82"},"html":{"href":"https://github.com/metabrainz/listenbrainz-server/pull/82"},"issue":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues/82"},"comments":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/issues/82/comments"},"review_comments":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82/comments"},"review_comment":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/pulls/82/commits"},"statuses":{"href":"https://api.github.com/repos/metabrainz/listenbrainz-server/statuses/80853743e33b069ca052fc11f3f7a8c9f38f7bcc"}}}}
{"actor":{"display_login":"mayhem"}}