Skip to main content

Likes, skips

A listener can do three things to the song that is playing: skip it, like it, dislike it. None of them is a local UI action. Each is a call to Feed.fm addressed by play id, and the server decides what actually happens.

This page assumes you have read Station playback for the loop these calls fit into. Every command below was run against https://feed.fm/api/v3 with the demo/demo test credentials and shows what actually came back. They continue the CLIENT_ID and PLAY_ID shell variables set on the earlier pages.

Which controls to show​

can_skip and can_like come back on POST /play/{id}/start. Both are advisory and tell a client which controls to show for this play. They vary from one play to the next depending on how the track is licensed for this listener. Certain types of licensing can forbid liking and skipping outright, so some listeners may never see these controls.

Working out when a like or skip will be allowed is the server's job, not the client's. Two rules are what a client follows:

  • Do not offer like or dislike on a play whose can_like is false.
  • Do not skip a song without permission. can_skip: false means do not offer the control. can_skip: true is not permission either, only a hint the control is worth showing. Permission comes from POST /play/{id}/skip, and that call can still say no.

Skipping​

Skip is a request, not a command​

warning

POST /play/{id}/skip returning HTTP 200 does not mean the skip was granted. skipDenied (code 7) and playNotActive (code 12) both arrive as HTTP 200 with success: false. Check success, and when it is false, keep playing the current song.

A granted skip is the bare acknowledgement:

curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/skip" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"
{ "success": true }

Here is a real skipDenied response, from calling skip on a play that had already been invalidated:

{
"success": false,
"error": {
"code": 7,
"message": "You may not skip a song you aren't playing",
"status": 200
}
}

Two rules follow from this. Do not stop audio until the skip comes back successful, and do not disable the current song's controls while you wait. A client that tears down playback optimistically and then reads success: false has nothing left to fall back to.

seconds is optional and reports how far the listener got before asking:

curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/skip" \
-H 'Content-Type: application/json' \
-d "{\"client_id\":\"$CLIENT_ID\",\"seconds\":42}"

A granted skip ends the play. Reserve the next song with POST /play and do not also report the skipped one as complete: that call fails with playbackComplete (code 21).

Skipping without permission​

Skipping a song the server refused is a licensing violation, not a UI shortcut. Clients that appear to play songs faster than their durations allow trigger review, and the app credentials can be revoked. This is the reason the skip call exists at all rather than the client simply moving on.

Liking and disliking​

Likes and dislikes are used to guide music selection and evaluate performance.

# like
curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/like" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"

# dislike
curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/dislike" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"

# remove a like, returning the song to neutral
curl -sS -u demo:demo -X DELETE "https://feed.fm/api/v3/play/$PLAY_ID/like" \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"

All three answer the same way:

{ "success": true }

They are forgiving. Liking twice is a no-op, a like replaces an existing dislike, a dislike replaces an existing like, and removing a like that was never there still succeeds. An unknown play id is the only common failure, and it is a normal missingObject (code 17) with HTTP 404.

A dislike does not skip the song​

Disliking records the opinion and nothing else. Audio keeps playing. If your UI implies the song will change, send POST /play/{id}/skip as well and handle the refusal, because that skip can be denied like any other.

A like outlives the play​

The like is stored against the client and the song. Later plays of the same song for the same client carry it back on the audio file:

{
"play": {
"audio_file": {
"id": "123262",
"track": { "id": "15360576", "title": "All the Time" },
"liked": true
}
}
}

liked is present only when the song is liked. It is never sent as false, so test for the key, not for its value. A song the listener disliked looks exactly like one they never touched.

The method override inverts a like​

Clients that cannot issue DELETE may POST with an X-HTTP-Method-Override: DELETE header. On /play/{id}/like, where both verbs exist and mean opposite things, that changes what the call does. The header is read before routing, so the request is dispatched to DELETE and the POST handler never runs:

# This REMOVES the like. It does not record one.
curl -sS -u demo:demo -X POST "https://feed.fm/api/v3/play/$PLAY_ID/like" \
-H 'X-HTTP-Method-Override: DELETE' \
-H 'Content-Type: application/json' -d "{\"client_id\":\"$CLIENT_ID\"}"
{ "success": true }

The response is indistinguishable from a recorded like, so a client that sets the header globally will silently unlike every song the listener likes. Send it on this path only when removal is what you want.