def _scan(self, request, stream):
        """
        Scans the stream to a specific part of the currently playing track.
        """
        Stream = apps.get_model("streams", "Stream")

        stream = Stream.objects.get(user=request.user)

        if not stream.now_playing.is_playing:
            raise Exception("Stream has to be playing")

        total_duration_ms = stream.now_playing.duration_ms
        total_duration = timedelta(milliseconds=int(total_duration_ms))
        started_at_raw = self.param(request, self.PARAM_STARTED_AT)
        started_at = time_util.int_to_dt(int(started_at_raw))
        now = time_util.now()

        valid_started_at = (
            now > started_at
            and now < started_at + total_duration - timedelta(seconds=5)
        )

        if not valid_started_at:
            raise Exception("Invalid scan")

        stream.now_playing.started_at = started_at
        stream.now_playing.status_at = time_util.now()
        stream.now_playing.save()

        return stream
Beispiel #2
0
    def filter_idle(self):
        """
        Filters to streams which are idle.
        """
        Queue = apps.get_model("streams", "Queue")

        now = time_util.now()
        three_hours_ago = now - timedelta(hours=3)

        newest_queue_track_id = (Queue.objects.filter(
            stream_id=OuterRef("uuid"),
            deleted_at__isnull=True,
        ).order_by("-index").values_list("track_id", flat=True)[:1])

        newest_queue_collection_id = (Queue.objects.filter(
            stream_id=OuterRef("uuid"),
            deleted_at__isnull=True,
        ).order_by("-index").values_list("collection_id", flat=True)[:1])

        return (self.annotate(
            newest_queue_track_id=Subquery(newest_queue_track_id),
        ).annotate(newest_queue_collection_id=Subquery(
            newest_queue_collection_id), ).filter(
                # No recently played queues == streams that are idle.
                ~Exists(
                    Queue.objects.filter(stream_id=OuterRef("uuid")).exclude(
                        played_at__lte=three_hours_ago))).exclude(
                            newest_queue_track_id__isnull=True,
                            newest_queue_collection_id__isnull=True,
                        ))
Beispiel #3
0
    def _delete_queue(self, request, stream):
        """
        When a user deletes (archives) something from the queue.
        """
        Queue = apps.get_model("streams", "Queue")

        queue_uuid = self.param(request, "queueUuid")
        queue = Queue.objects.get(uuid=queue_uuid,
                                  stream=stream,
                                  user=request.user)

        head = Queue.objects.get_head(stream)
        if queue.index <= head.index:
            raise Exception("Cannot delete a queue that is not up next.")

        now = time_util.now()
        with transaction.atomic():

            # delete queue
            queue.deleted_at = now
            queue.save()

            # fix offset for up next indexes
            children_queue_count = queue.children.count()
            offset = max(1, children_queue_count)
            relative_next_up = Queue.objects.filter(stream=stream,
                                                    index__gt=queue.index,
                                                    deleted_at__isnull=True)
            relative_next_up.update(index=F("index") - offset)

            # also delete children
            queue.children.all().update(deleted_at=now)
    def _next_track(self, request, stream):
        Queue = apps.get_model("streams", "Queue")

        total_duration_ms = stream.now_playing.duration_ms
        if total_duration_ms:
            total_duration = timedelta(milliseconds=int(total_duration_ms))
        is_planned = self.param(request, "isPlanned")

        next_head = Queue.objects.get_next(stream)

        if not next_head:

            if not stream.now_playing.is_playing and not stream.now_playing.is_paused:
                raise ValueError("Stream needs to be playing or paused")

            stream.now_playing.status_at = time_util.now()
            stream.now_playing.status = Queue.STATUS_ENDED_AUTO
            stream.now_playing.save()
            return stream

        if is_planned:
            playing_at = stream.now_playing.started_at + total_duration
        else:
            playing_at = time_util.now() + timedelta(milliseconds=100)

        with transaction.atomic():

            next_head.started_at = playing_at
            next_head.status = Queue.STATUS_PLAYED
            next_head.status_at = time_util.now()
            next_head.save()

            prev_head = stream.now_playing
            prev_head.status = Queue.STATUS_ENDED_AUTO
            prev_head.status_at = time_util.now()
            prev_head.save()

            stream.now_playing = next_head
            stream.save()

        return stream
Beispiel #5
0
    def _prev_track(self, request, stream):
        Queue = apps.get_model("streams", "Queue")

        if not stream.now_playing.track_id:
            raise ValueError("Nothing to play next!")

        if (
            stream.now_playing
            and not stream.now_playing.is_playing
            and not stream.now_playing.is_paused
        ):
            playing_at = time_util.now()
            stream.now_playing.started_at = playing_at
            stream.now_playing.status = Queue.STATUS_PLAYED
            stream.now_playing.status_at = time_util.now()
            stream.now_playing.save()
            return stream

        next_head = Queue.objects.get_prev(stream)

        playing_at = time_util.now() + timedelta(milliseconds=100)
        with transaction.atomic():

            next_head.started_at = playing_at
            next_head.status = Queue.STATUS_PLAYED
            next_head.status_at = time_util.now()
            next_head.save()

            prev_head = stream.now_playing
            prev_head.status_at = time_util.now()
            prev_head.status = Queue.STATUS_QUEUED_PREVIOUS
            prev_head.save()

            stream.now_playing = next_head
            stream.save()

        return stream
    def _play_track(self, request, stream):
        """
        When a user plays a paused stream.
        """
        Queue = apps.get_model("streams", "Queue")
        if stream.now_playing.is_playing:
            raise ValueError("Cannot play a stream which is already playing")
        if not stream.now_playing.is_paused:
            raise ValueError("Cannot play a stream which is not paused")

        # NOTE: We have to be able to get ourselves out of a pickle. So here,
        #       we do not restrict the play action by checking to see if the
        #       stream has `controls_enabled`. So long as the stream is paused,
        #       we allow the play action.

        # NOTE: This is a copy paste of "scan" logic found in "scan_view."
        total_duration_ms = stream.now_playing.duration_ms
        total_duration = timedelta(milliseconds=int(total_duration_ms))
        timestamp_ms = self.param(request, self.PARAM_TIMESTAMP_MS)

        now = time_util.now()
        started_at = now - timedelta(milliseconds=int(timestamp_ms))

        # Needs validation when scanning
        valid_started_at = (
            now >= started_at
            and now < started_at + total_duration - timedelta(seconds=5))
        if not valid_started_at:
            raise Exception("Invalid scan")

        stream.now_playing.started_at = started_at
        stream.now_playing.status_at = time_util.now()
        stream.now_playing.status = Queue.STATUS_PLAYED
        stream.now_playing.save()

        return stream
Beispiel #7
0
def generate_apple_music_token():
    alg = "ES256"
    time_now = now()
    time_expired = time_now + timedelta(hours=(24 * 7))

    headers = {
        "alg": alg,
        "kid": settings.APPLE_MUSIC_KEY_ID,
    }

    payload = {
        "iss": settings.APPLE_MUSIC_TEAM_ID,
        "exp": int(time_expired.strftime("%s")),
        "iat": int(time_now.strftime("%s")),
    }

    secret = settings.APPLE_MUSIC_AUTH_KEY_P8

    return jwt.encode(payload, secret, algorithm=alg, headers=headers)
    def post(self, request, **kwargs):
        """
        Delete (archive) all TextCommentModification objects that related to a
        given TextComment. In practice, this is like "wiping" the comment
        clean.
        """
        TextCommentModification = apps.get_model("comments",
                                                 "TextCommentModification")

        text_comment_uuid = self.param(request, "textCommentUuid")

        text_comment_modification_qs = TextCommentModification.objects.filter(
            user=request.user, text_comment_id=text_comment_uuid)
        text_comment_modification_qs.update(deleted_at=time_util.now())

        return self.http_react_response(
            "textComment/clearModifications",
            {
                "textCommentUuid": text_comment_uuid,
            },
        )
    def controls_enabled(self, end_buffer, total_duration):
        """
        A stream's playback controls are disabled towards the end of the now
        playing track. This determines if the stream is able to have the
        controls enabled or not.
        """
        if not self.track_id:
            raise Exception("Can only control a queue if it has a track.")

        if self.status == self.STATUS_PAUSED:
            return True

        if self.status != self.STATUS_PLAYED:
            return False

        expected_end_at = self.started_at + timedelta(
            milliseconds=self.track.duration_ms)
        controls_disabled_at = time_util.now() + timedelta(
            milliseconds=self.CONTROL_BUFFER_MS)

        return controls_disabled_at > expected_end_at
Beispiel #10
0
    def _pause_track(self, request, stream):
        """
        When a user pauses a playing stream.
        """
        Queue = apps.get_model("streams", "Queue")
        if not stream.now_playing.is_playing:
            raise ValueError(
                "Cannot pause a stream that is not already playing")
        if stream.now_playing.is_paused:
            raise ValueError("Cannot pause a stream which is already paused")

        pausing_at = time_util.now() + timedelta(milliseconds=100)
        if not stream.now_playing.controls_enabled:
            raise ValueError(
                "Cannot pause since the track will be over by the time we try to pause"
            )

        stream.now_playing.status = Queue.STATUS_PAUSED
        stream.now_playing.status_at = pausing_at
        stream.now_playing.save()

        return stream
    def create_blank_queue(self, stream):
        """
        Custom create method.
        TODO: this should probably only be done when the blank queue is the
        new head. the code should enforce that.
        """
        Queue = apps.get_model("streams", "Queue")

        head = Queue.objects.get_head(stream)
        if head:
            index = head.index + 1
        else:
            index = Queue.INITIAL_INDEX

        return Queue.objects.create(
            stream=stream,
            user=stream.user,
            index=index,
            is_abstract=False,
            duration_ms=0,
            status=Queue.STATUS_PLAYED,
            status_at=time_util.now(),
        )
 def archive(self):
     self.deleted_at = time_util.now()
     self.save()
Beispiel #13
0
def _refresh_collection_apple_music_playlist_data(collection, user):
    CollectionListing = apps.get_model("music", "CollectionListing")
    Track = apps.get_model("music", "Track")
    Request = apps.get_model("networking", "Request")

    apple_music_token = generate_apple_music_token()
    apple_music_id = collection.external_id

    response = make_request(
        Request.TYPE_GET,
        f"https://api.music.apple.com/v1/catalog/us/playlists/{apple_music_id}",
        headers={
            "Authorization": f"Bearer {apple_music_token}",
        },
    )
    response_json = response.json()

    items = response_json["data"][0]["relationships"]["tracks"]["data"]
    data = []
    for item in items:
        data.append({
            # to be saved as Track instances
            "format": Track.FORMAT_TRACK,
            "provider": Track.PROVIDER_APPLE_MUSIC,
            "external_id": item["id"],
            "name": item["attributes"]["name"],
            "artist_name": item["attributes"]["artistName"],
            "album_name": item["attributes"]["albumName"],
            "duration_ms": item["attributes"]["durationInMillis"],
            "img_url": item["attributes"]["artwork"]["url"],
            # not saved in Track table
            "_disk_number": item["attributes"]["discNumber"],
            "_track_number": item["attributes"]["trackNumber"],
        })

    # first sort by disk number, then by track number
    data = sorted(data, key=lambda d: (d["_disk_number"], d["_track_number"]))

    tracks = []
    track_eids = []
    for track_data in data:
        tracks.append(
            Track(
                format=track_data["format"],
                provider=track_data["provider"],
                external_id=track_data["external_id"],
                name=track_data["name"],
                artist_name=track_data["artist_name"],
                album_name=track_data["album_name"],
                img_url=track_data["img_url"],
                duration_ms=track_data["duration_ms"],
            ))
        track_eids.append(track_data["external_id"])

    # create Track objects if they do not already exist
    # TODO refresh more data points
    Track.objects.bulk_create(tracks, ignore_conflicts=True)
    track_qs = Track.objects.filter(external_id__in=track_eids)

    # wipe old CollectionListing objects
    # TODO this is not ok
    cl_by_tracks_qs = CollectionListing.objects.filter(
        track__external_id__in=track_eids)
    cl_by_collection_qs = CollectionListing.objects.filter(
        collection=collection)
    now = time_util.now()
    cl_by_tracks_qs.update(deleted_at=now)
    cl_by_collection_qs.update(deleted_at=now)

    # sort tracks by order one more time
    track_map = {}
    for track in track_qs:
        track_map[track.external_id] = track

    tracks = []
    for track_eid in track_eids:
        tracks.append(track_map[track_eid])

    # create CollectionListing objects if they do not already exist
    collection_listings = []
    for idx in range(len(tracks)):
        track = tracks[idx]
        collection_listings.append(
            CollectionListing(
                track=track,
                collection=collection,
                number=idx,
            ))
    CollectionListing.objects.bulk_create(collection_listings)
Beispiel #14
0
def _refresh_collection_spotify_playlist_data(collection, user):
    Track = apps.get_model("music", "Track")
    CollectionListing = apps.get_model("music", "CollectionListing")
    Request = apps.get_model("networking", "Request")

    response = make_request(
        Request.TYPE_GET,
        f"https://api.spotify.com/v1/playlists/{collection.spotify_id}",
        headers={
            "Authorization": f"Bearer {user.spotify_access_token}",
            "Content-Type": "application/json",
        },
    )
    response_json = response.json()

    # items are pre-sorted here
    items = response_json["tracks"]["items"]
    data = []
    for item in items:
        artist_names = map(lambda o: o["name"], item["track"]["artists"])
        data.append({
            "format": Track.FORMAT_TRACK,
            "provider": Track.PROVIDER_SPOTIFY,
            "external_id": item["track"]["uri"],
            "name": item["track"]["name"],
            "artist_name": ", ".join(artist_names),
            "album_name": collection.name,
            "duration_ms": item["track"]["duration_ms"],
            "img_url": item["track"]["album"]["images"][0]["url"],
        })

    tracks = []
    track_eids = []
    for track_data in data:
        tracks.append(
            Track(
                format=track_data["format"],
                provider=track_data["provider"],
                external_id=track_data["external_id"],
                name=track_data["name"],
                artist_name=track_data["artist_name"],
                album_name=track_data["album_name"],
                img_url=track_data["img_url"],
                duration_ms=track_data["duration_ms"],
            ))
        track_eids.append(track_data["external_id"])

    # create Track objects if they do not already exist
    # TODO refresh more data points
    Track.objects.bulk_create(tracks, ignore_conflicts=True)
    track_qs = Track.objects.filter(external_id__in=track_eids)

    # wipe old CollectionListing objects to force refresh playlist
    # TODO this is not ok
    cl_by_tracks_qs = CollectionListing.objects.filter(
        track__external_id__in=track_eids)
    cl_by_collection_qs = CollectionListing.objects.filter(
        collection=collection)
    now = time_util.now()
    cl_by_tracks_qs.update(deleted_at=now)
    cl_by_collection_qs.update(deleted_at=now)

    # sort tracks by order one more time
    track_map = {}
    for track in track_qs:
        track_map[track.external_id] = track

    tracks = []
    for track_eid in track_eids:
        tracks.append(track_map[track_eid])

    # create CollectionListing objects if they do not already exist
    collection_listings = []
    for idx in range(len(tracks)):
        track = tracks[idx]
        collection_listings.append(
            CollectionListing(
                track=track,
                collection=collection,
                number=idx,
            ))
    CollectionListing.objects.bulk_create(collection_listings)
    def create_queue(self,
                     stream=None,
                     track=None,
                     collection=None,
                     user=None,
                     **kwargs):
        """
        Custom create method
        """
        Queue = apps.get_model("streams", "Queue")

        last_queue = Queue.objects.last_queue(stream)
        index = last_queue.index + 1

        queue_head = Queue.objects.get_head(stream)
        if queue_head.index >= index:
            raise ValueError("Index value is too small")

        # NOTE: This is a little hacky. We calculate the offset that in queue
        #       indexes need to be bumped up by.
        _tracks = collection.list_tracks() if collection else [None]
        if not len(_tracks):
            raise ValueError(f"Collection has no tracks: {collection.uuid}")
        offset = len(_tracks)

        with transaction.atomic():
            next_up_tracks_only = Queue.objects.filter(
                index__gte=index,
                stream=stream,
                deleted_at__isnull=True,
            )
            next_up_tracks_only.update(index=F("index") + offset)

            queues = []

            parent_queue = (Queue(
                stream=stream,
                index=(index + offset - 1),
                user=user,
                collection=collection,
                is_abstract=True,
                status=Queue.STATUS_QUEUED_INIT,
                status_at=time_util.now(),
            ) if collection else None)
            if parent_queue:
                queues.append(parent_queue)

            total_duration_ms = 0
            for _track in _tracks:
                t = _track or track
                if not t.duration_ms:
                    refresh_track_external_data(t, user)
                    t.refresh_from_db()
                queue = Queue(
                    stream=stream,
                    index=index,
                    user=user,
                    track=(_track or track),
                    collection=collection,
                    is_abstract=False,
                    parent=parent_queue,
                    duration_ms=t.duration_ms,
                    status=Queue.STATUS_QUEUED_INIT,
                    status_at=time_util.now(),
                )
                queues.append(queue)
                index += 1
                total_duration_ms += t.duration_ms
            if parent_queue:
                parent_queue.duration_ms = total_duration_ms

            Queue.objects.bulk_create(queues)