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?
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.
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:
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.
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
After voting her 5***** 100 times (kissfm 5515 5
) she's finally #2 with a
rating of 3.6 - Awesome!
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
.
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.
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
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 |
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 |
Yeah, no Despacito radio station after all. Voting doesn't turn out to be all that it was promised to be.
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');
}
}