Kissfm Rating Tracks
Pasito a pasito, suave suavecito

We have a radio in the kitchen and my roommates really like kissfm. After I heard that kissfm allows their listeners to influence the program by voting, I just had to take look; I mean, who would say no to a 24/7 Despacito radio station?

The candidates

Kissfm calls it RatingTracks and provides their listeners with around 20 candidates to choose from. Upvoted songs are played more, downvoted songs are played less - "You are the Beat of Berlin!". That's the theory, at least.

Sadly, they prevent people from voting tracks more than once. So I can't just cancel my appointments and spam 5 stars on my favorite track until kissfm is great again.

rate.gif
Nothing to see here, they validate their inputs. Time to go home.

But just for curiosity's sake, let's take a peek behind the scenes. Here are the requests fired when voting a track 5 stars:

vote-network-tab.png
If only there was a hint which request we need to look at…

Even if the request to kissfm didn't boast the 5 we just voted, excluding the other requests would have been easy - google-analytics.com is obviously unrelated and de.ioam.de can be excluded as advertising related after a quick google search leading us to an optout page. A look at the relevant headers of the kissfm request tells us there's not much going on. And after a few votes it becomes obvious that the template for the url is rate/TRACK_ID/RATING.

POST /beats-stars/rating-tracks/rate/5372/5 HTTP/1.1
Host: www.kissfm.de
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://www.kissfm.de/beats-stars/rating-tracks/
Cookie: …

ajax=1

So now that we have a hunch which request we want to send, we just need the TRACK_ID of our favorite song. The easy way would be voting and just copying the request. The somewhat harder way requires inspecting the rating html element for Taylor Swift - Gorgeous

<div class="track">
  …
  <div class="rating" data-track-id="5515" data-inited="true"> … (ref:data-track-id)
  </div>
  …
</div>

I didn't expect it to be that easy and actually looked at the js 1 because of that. After all, kissfm is a somewhat big 2 radio station.

Voting

Let's see if it works. We just send 100 POSTS to the rating url and check what happens.

kissfm() {
    TRACK_ID=$1
    RATING=$2
    for i in $(seq 100); do
        curl -X POST  "http://www.kissfm.de/beats-stars/rating-tracks/rate/$TRACK_ID/$RATING" --data 'ajax=1'
        echo $i
    done
}

Taylor Swift start out on place #17 with a rating of 2.8

rating-taylor-before.png

After voting her 5***** 100 times (kissfm 5515 5) she's finally #2 with a rating of 3.6 - Awesome!

rating-taylor-after.png

Now, I feel bad abusing the system if it's not for Despacito, so let's undo that and put her back on #17 with kissfm 5515 1.

rating-taylor-after-after.png

Interestingly the rating is not exactly the same as before, she actually lost .1 even though the votes should have cancelled each other out. The explanation I like most is that someone must have seen her sitting at #2 and immediately voted her down just because.

24/7 Despacito?

Even though I had the voting part figured out, my plan ultimately failed. Despacito was not part of the candidates and I didn't find a way to find rating track id's for tracks not currently in the selection. The next best thing to do was checking whether my plan could have worked if only the stars had been aligned the right way, i.e. if rating actually affects the frequencey with which songs are played. That would mean that I would just have to wait for another masterpiece of pop art to enter the candidate selection.

I tried my hand at answering this question and the results are not at all promising. At least not for the 24/7 Despacito use case.

Note: Those results are from sometime in the evening, so only ~20 hours were taken into account. The rating tracks did not change during that time.

from collections import Counter
import json
import requests
from bs4 import BeautifulSoup

def get_rating_tracks():
    html = requests.get("http://www.kissfm.de/beats-stars/rating-tracks/").content
    soup = BeautifulSoup(html)
    tracks = []
    for track in soup.find_all("div", class_="track"):
        artist = track.find("div", class_="artist").text
        print artist
        title = track.find("div", class_="title").text
        name = artist + " - " + title
        tracks.append(name)
    return [[i + 1, name] for i, name in enumerate(tracks)]

def get_tracks_for_day(day):
    url = "http://www.kissfm.de/beats-stars/trackfinder?format=json&day={}&hours=0&minutes=00".format(day)
    tracks = []
    while "day={}".format(day) in url:
        print url
        current_tracks, url = get_tracks_and_next_url(url)
        tracks.extend(current_tracks)
    return tracks

def get_tracks_and_next_url(url):
    text = requests.get(url).content
    response = json.loads(text)
    next_url = "http://www.kissfm.de" + response.get("FutureLink", "") + "&format=json"
    tracks = []
    for track in response["items"]:
        artist = track["ArtistName"]
        title = track["TrackName"]
        time = track["PlayTime"]
        name = (artist + " - " + title)
        tracks.append([time, name])
    return tracks, next_url

Rating Tracks play count

day = "2017-11-26"
tracks_for_day = get_tracks_for_day(day)
rating_tracks = get_rating_tracks()
count_by_track = Counter([track[1] for track in tracks_for_day])

result = []
for rating, name in rating_tracks:
    count = count_by_track.get(name, 0)
    result.append([rating, name, count])

result
Rating Name Count
1 NICO SANTOS - ROOFTOP 28
2 ROBIN SCHULZ UND HUGEL - I BELIEVE I'M FINE 25
3 CHARLIE PUTH - HOW LONG 20
4 KYGO FEAT JUSTIN JESSO - STARGAZING 12
5 NF - LET YOU DOWN 23
6 FELIX JAEHN FEAT HEARTS UND CO - LIKE A RIDDLE 17
7 MAJOR LAZER - PARTICULA 12
8 MACKLEMORE FEAT KESHA - GOOD OLD DAYS 4
9 AVICII FT RITA ORA - LONELY TOGETHER 13
10 ALMA, FRENCH MONTANA - PHASES 6
11 KONTRA K - SOLDATEN 2.0 0
12 OFENBACH UND NICK WATERHOUSE - KATCHI 39
13 CALVIN HARRIS FEAT. KEHLANI UN - FAKING IT (PRIMC EDIT) 0
14 DAVID GUETTA UND AFROJACK FEAT - DIRTY SEXY MONEY 4
15 CAMELPHAT UND ELDERBROOK - COLA 7
16 DJ SNAKE FT LAUV - A DIFFERENT WAY 6
17 TAYLOR SWIFT - GORGEOUS 21
18 SOFI TUKKER FEAT NERVO, THE KN - BEST FRIEND 10
19 RITA ORA - ANYWHERE 0
20 KHALID - YOUNG DUMB UND BROKE 0
21 KANYE WEST FEAT. SYLEENA JOHNS - ALL FALLS DOWN 0

Most commonly played

count_by_track.most_common(10)
Name Count
BAUSA - WAS DU LIEBE NENNST 105
CAMILA CABELLO FEAT. YOUNG THU - HAVANA 98
POST MALONE FEAT. 21 SAVAGE - ROCKSTAR 87
AXWELL ^ INGROSSO - MORE THAN YOU KNOW 86
JUSTIN BIEBER - FRIENDS 81
MAROON 5 FEAT. SZA - WHAT LOVERS DO 53
IMAGINE DRAGONS - WHATEVER IT TAKES 52
PORTUGAL. THE MAN - FEEL IT STILL (OFENBACH REMIX) 52
DUA LIPA - NEW RULES 51
RUDIMENTAL FEAT. JAMES ARTHUR - SUN COMES UP 49

End

Yeah, no Despacito radio station after all. Voting doesn't turn out to be all that it was promised to be.

Footnotes


1

Kissfm is nice enough to annotate their production js with source file names, and we're greeted with this after a cursory search for something with rating in it.


$ curl -s "http://www.kissfm.de/assets/_combinedfiles/combined.js" | grep "FILE:" | grep -i rating -C1
/****** FILE: themes/main/js/App/TrackfinderBar.js *****/
/****** FILE: themes/main/js/App/TrackRating.js *****/
/****** FILE: themes/main/js/App/ContentParts.js *****/
--
/****** FILE: themes/main/js/App/ContentParts/FurtherTopics.js *****/
/****** FILE: themes/main/js/App/ContentParts/TrackRating.js *****/
/****** FILE: themes/main/js/global.js *****/

The TrackRating.js file sounded most promising and it was aptly named. The code is actually very readable, not at all what I've seen elsewhere (looking at you SAP - you horrible double-click-to-open-in-the-browser emulating people!).

Here's the relevant part

var rateUrl = $container.data('rateUrl'),
    ratingData = App.TrackRating.Data($container.data('id')),
    $audio;

$container.on('click', '.star', function() {
  var $star = $(this),
      $rating = $star.closest('.rating');

  if (!$rating.hasClass('disabled')) {
    var trackId = $rating.data('trackId'),
    userRating = $rating.find('.star').index($star) + 1;
    trackId = $rating.attr('data-track-id');
    App.Tracking.trackPageview();
    $.ajax({
      type: 'POST',
      url: rateUrl + trackId + '/' + userRating (ref:template)
    }).done(function() {
      ratingData.setRating(trackId, userRating);
      $rating.find('.stars').hide();
      $rating.find('.message').show();
      setTimeout(function() {
        $rating.find('.stars').show();
        $rating.find('.message').hide();
      }, 3000);
    }).fail(function() {
      $rating.find('.star').addClass('inactive');
    });
    $rating.addClass('disabled');
  }
}

The rating url template

2

When it comes to Berlin. I thought they were bigger, so I guess it's not such a surprise after all.