Skip to content

Commit

Permalink
Add remixes and stems to API (#893)
Browse files Browse the repository at this point in the history
* Stems + Remixing + Remixes endpoints

* Add caching

* Cleanup

* Revisions for client compatability

* Lint fixes
  • Loading branch information
piazzatron committed Oct 8, 2020
1 parent 46c39e5 commit 78296e3
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 130 deletions.
18 changes: 18 additions & 0 deletions discovery-provider/src/api/v1/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ def extend_track(track):

return track

def stem_from_track(track):
track_id = encode_int_id(track["track_id"])
parent_id = encode_int_id(track["stem_of"]["parent_track_id"])
category = track["stem_of"]["category"]
return {
"id": track_id,
"parent_id": parent_id,
"category": category,
"cid": track["download"]["cid"],
"user_id": encode_int_id(track["owner_id"])
}

def extend_playlist(playlist):
playlist_id = encode_int_id(playlist["playlist_id"])
Expand Down Expand Up @@ -228,6 +239,13 @@ def to_dict(multi_dict):
"""Converts a multi dict into a dict where only list entries are not flat"""
return {k: v if len(v) > 1 else v[0] for (k, v) in multi_dict.to_dict(flat=False).items()}

def get_current_user_id(args):
"""Gets current_user_id from args featuring a "user_id" key"""
if args.get("user_id"):
return decode_string_id(args["user_id"])
return None



search_parser = reqparse.RequestParser()
search_parser.add_argument('query', required=True)
Expand Down
13 changes: 13 additions & 0 deletions discovery-provider/src/api/v1/models/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,16 @@
"cover_art": fields.String,
"remix_of": fields.Nested(full_remix_parent),
})

stem_full = ns.model('stem_full', {
"id": fields.String(required=True),
"parent_id": fields.String(required=True),
"category": fields.String(required=True),
"cid": fields.String(required=True),
"user_id": fields.String(required=True)
})

remixes_response = ns.model('remixes_response', {
"count": fields.Integer(required=True),
"tracks": fields.List(fields.Nested(track_full))
})
79 changes: 73 additions & 6 deletions discovery-provider/src/api/v1/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import logging # pylint: disable=C0302
from flask import redirect
from flask_restx import Resource, Namespace, fields, reqparse
from flask_restx import resource
from src.queries.get_tracks import get_tracks
from src.queries.get_track_user_creator_node import get_track_user_creator_node
from src.api.v1.helpers import abort_not_found, decode_with_abort, \
extend_track, make_response, search_parser, extend_user, get_default_max, \
trending_parser, full_trending_parser, success_response, abort_bad_request_param, to_dict, \
format_offset, format_limit, decode_string_id
from .models.tracks import track, track_full
format_offset, format_limit, decode_string_id, stem_from_track, \
get_current_user_id

from .models.tracks import track, track_full, stem_full, remixes_response
from src.queries.search_queries import SearchKind, search
from src.queries.get_trending_tracks import get_trending_tracks
from flask.json import dumps
Expand All @@ -20,6 +21,9 @@
from src.queries.get_reposters_for_track import get_reposters_for_track
from src.queries.get_savers_for_track import get_savers_for_track
from src.queries.get_tracks_including_unlisted import get_tracks_including_unlisted
from src.queries.get_stems_of import get_stems_of
from src.queries.get_remixes_of import get_remixes_of
from src.queries.get_remix_track_parents import get_remix_track_parents

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -360,10 +364,8 @@ def get(self, track_id):
decoded_id = decode_with_abort(track_id, full_ns)
limit = get_default_max(args.get('limit'), 10, 100)
offset = get_default_max(args.get('offset'), 0)
current_user_id = get_current_user_id(args)

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args["user_id"])
args = {
'repost_track_id': decoded_id,
'current_user_id': current_user_id,
Expand All @@ -373,3 +375,68 @@ def get(self, track_id):
users = get_reposters_for_track(args)
users = list(map(extend_user, users))
return success_response(users)

track_stems_response = make_response("stems_response", full_ns, fields.List(fields.Nested(stem_full)))

@full_ns.route("/<string:track_id>/stems")
class FullTrackStems(Resource):
@full_ns.marshal_with(track_stems_response)
@cache(ttl_sec=10)
def get(self, track_id):
decoded_id = decode_with_abort(track_id, full_ns)
stems = get_stems_of(decoded_id)
stems = list(map(stem_from_track, stems))
return success_response(stems)


remixes_response = make_response("remixes_response", full_ns, fields.Nested(remixes_response))
remixes_parser = reqparse.RequestParser()
remixes_parser.add_argument('user_id', required=False)
remixes_parser.add_argument('limit', required=False, default=10)
remixes_parser.add_argument('offset', required=False, default=0)

@full_ns.route("/<string:track_id>/remixes")
class FullRemixesRoute(Resource):
@full_ns.marshal_with(remixes_response)
@cache(ttl_sec=10)
def get(self, track_id):
decoded_id = decode_with_abort(track_id, full_ns)
request_args = remixes_parser.parse_args()
current_user_id = get_current_user_id(request_args)

args = {
"with_users": True,
"track_id": decoded_id,
"current_user_id": current_user_id,
"limit": format_limit(request_args),
"offset": format_offset(request_args)
}
response = get_remixes_of(args)
response["tracks"] = list(map(extend_track, response["tracks"]))
return success_response(response)

remixing_response = make_response("remixing_response", full_ns, fields.List(fields.Nested(track_full)))
remixing_parser = reqparse.RequestParser()
remixing_parser.add_argument('user_id', required=False)
remixing_parser.add_argument('limit', required=False, default=10)
remixing_parser.add_argument('offset', required=False, default=0)

@full_ns.route("/<string:track_id>/remixing")
class FullRemixingRoute(Resource):
@full_ns.marshal_with(remixing_response)
@cache(ttl_sec=10)
def get(self, track_id):
decoded_id = decode_with_abort(track_id, full_ns)
request_args = remixes_parser.parse_args()
current_user_id = get_current_user_id(request_args)

args = {
"with_users": True,
"track_id": decoded_id,
"current_user_id": current_user_id,
"limit": format_limit(request_args),
"offset": format_offset(request_args)
}
tracks = get_remix_track_parents(args)
tracks = list(map(extend_track, tracks))
return success_response(tracks)
86 changes: 61 additions & 25 deletions discovery-provider/src/queries/get_remix_track_parents.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,77 @@
import logging # pylint: disable=C0302
from sqlalchemy import desc, and_

from flask.globals import request
from src.models import Track, Remix
from src.utils import helpers
from src.utils.db_session import get_db_read_replica
from src.queries.query_helpers import get_current_user_id, populate_track_metadata, \
paginate_query, add_users_to_tracks
from src.queries.query_helpers import populate_track_metadata, \
add_query_pagination, add_users_to_tracks
from src.utils.redis_cache import extract_key, use_redis_cache

logger = logging.getLogger(__name__)

UNPOPULATED_REMIX_PARENTS_CACHE_DURATION_SEC = 10

def make_cache_key(args):
cache_keys = {
"limit": args.get("limit"),
"offset": args.get("offset"),
"track_id": args.get("track_id")
}
return extract_key(f"unpopulated-remix-parents:{request.path}", cache_keys.items())

def get_remix_track_parents(track_id, args):
def get_remix_track_parents(args):
"""Fetch remix parents for a given track.
Args:
args:dict
args.track_id: track id
args.limit: limit
args.offset: offset
args.with_users: with users
args.current_user_id: current user ID
"""
track_id = args.get("track_id")
current_user_id = args.get("current_user_id")
limit = args.get("limit")
offset = args.get("offset")
db = get_db_read_replica()

with db.scoped_session() as session:
base_query = (
session.query(Track)
.join(
Remix,
and_(
Remix.parent_track_id == Track.track_id,
Remix.child_track_id == track_id
def get_unpopulated_remix_parents():
base_query = (
session.query(Track)
.join(
Remix,
and_(
Remix.parent_track_id == Track.track_id,
Remix.child_track_id == track_id
)
)
.filter(
Track.is_current == True,
Track.is_unlisted == False
)
.order_by(
desc(Track.created_at),
desc(Track.track_id)
)
)
.filter(
Track.is_current == True,
Track.is_unlisted == False
)
.order_by(
desc(Track.created_at),
desc(Track.track_id)
)

tracks = add_query_pagination(base_query, limit, offset).all()
tracks = helpers.query_result_to_list(tracks)
track_ids = list(map(lambda track: track["track_id"], tracks))
return (tracks, track_ids)

key = make_cache_key(args)
(tracks, track_ids) = use_redis_cache(
key,
UNPOPULATED_REMIX_PARENTS_CACHE_DURATION_SEC,
get_unpopulated_remix_parents
)

tracks = paginate_query(base_query).all()
tracks = helpers.query_result_to_list(tracks)
track_ids = list(map(lambda track: track["track_id"], tracks))
current_user_id = get_current_user_id(required=False)
tracks = populate_track_metadata(session, track_ids, tracks, current_user_id)

if args.get("with_users", False):
add_users_to_tracks(session, tracks)
add_users_to_tracks(session, tracks, current_user_id)

return tracks

0 comments on commit 78296e3

Please sign in to comment.