Guides .md
Building a Kodi integration
Connect a Kodi addon running on a TV to FlickList, so playback there updates watch history and feeds a next-episode prompt back into the addon's UI.
1. Get a device code and let the user approve it#
Kodi runs on a screen with no keyboard sign-in, so use the device-code flow instead of a password form. Request a code, show the user_code on screen, and poll until the person approves it from their phone or laptop. The full request-and-poll sequence is in Quickstart. This guide picks up once your addon is holding a bearer token in its settings.
# see /dev/quickstart for the request/poll loop itself
token = run_device_code_login(client_id="fl_kodi_scrobbler")
addon.setSetting("access_token", token)
2. Start tracking playback#
Call this from your player class's onPlayBackStarted callback (and again from onPlayBackResumed, after a seek). Resolve the title to a TMDB ID first, since scrobble targets must carry one.
curl -X POST "https://flicklist.tv/api/v3/scrobble/start" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_type": "movie",
"ids": { "tmdb": 550 },
"progress": 0.0
}'await fetch('https://flicklist.tv/api/v3/scrobble/start', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_type: 'movie',
ids: { tmdb: 550 },
progress: 0.0,
}),
});import requests
requests.post(
"https://flicklist.tv/api/v3/scrobble/start",
headers={"Authorization": f"Bearer {token}"},
json={"media_type": "movie", "ids": {"tmdb": 550}, "progress": 0.0},
)For an episode, send "media_type": "show" (or "tv"), the show's ids block, and season and episode numbers. The full shape is on the Scrobbling page.
3. Keep sending start as your heartbeat#
Kodi doesn't fire a periodic "still playing" event, so start a 30 to 60 second interval timer while the title plays and re-POST start with the current progress each time. start is an upsert, so every call updates the same resume point rather than creating a new one.
4. Pause when the person actually pauses#
Wire this to onPlayBackPaused, not to buffering or seek events. Send it only when playback genuinely stops for the user.
{ "id": null }
If nothing was actively playing that item, pause is a no-op and id comes back null. It never creates a new resume point on its own.
5. Stop at the end of playback#
Call this from onPlayBackStopped and onPlayBackEnded alike, with the final progress value.
{ "watch_status": "watched" }
watch_status tells you the outcome. At 90% progress or more, with enough of the runtime actually played, the item is marked watched. At 90% or more but under that time floor, it's recorded as a preview. Below 90%, the resume point is kept as partial so the person can pick up where they left off.
6. Sync watched state without hammering the API#
Don't refetch full watched history every time the addon's menu opens. Poll GET /v3/sync/last_activities first and compare its per-domain timestamps against what you last cached.
curl -H "Authorization: Bearer $TOKEN" \
"https://flicklist.tv/api/v3/sync/last_activities"const res = await fetch('https://flicklist.tv/api/v3/sync/last_activities', {
headers: { Authorization: `Bearer ${token}` },
});
const activities = await res.json();r = requests.get(
"https://flicklist.tv/api/v3/sync/last_activities",
headers={"Authorization": f"Bearer {token}"},
)
activities = r.json()Only when a domain's timestamp moved forward do you call GET /v3/sync/watched/movies or GET /v3/sync/watched/shows to refresh that part of your local cache. Everything else stays as it was.
7. Surface what's next#
GET /v3/sync/up_next returns the next unwatched episode for each show the person is in the middle of, which is exactly what a "play next" prompt needs once credits roll. Scrobbling a finished episode updates it immediately, so calling it right after stop reflects the episode that just ended. Details, including dropping a show from the list, are on Up Next & change detection.