Complete Plex (Tautulli) log parsing logic

This commit is contained in:
2026-04-17 01:28:07 -04:00
parent aaa45d5743
commit c716e8f219
2 changed files with 40 additions and 20 deletions

View File

@@ -12,6 +12,10 @@ in settings and let your Pebble keep track!
>Jellyfin is strongly encouraged! Plex support requires installing [Tautulli](https://tautulli.com/)
>since the official API doesn't seem to want to return watch history (unless you have Plex Pass?).
# Contributing
If you use a different streaming service with an accessible API, I'd love help supporting them!
PRs are welcome, or even just issues telling me "Hey, this thing I use has an API!".
# Spring 2026 Developer Contest
This app was entered in the Spring 2026 Developer Contest on the official Pebble appstore.

View File

@@ -9,18 +9,18 @@ Pebble.addEventListener("ready", function () {
Pebble.addEventListener("appmessage", function (dict) {
if (dict.payload["PKJS_SLEEP_TIMESTAMP"]) {
const sleepTimestamp = dict.payload["PKJS_SLEEP_TIMESTAMP"] * 1000; // convert to milliseconds to later comparison
const sleepTimestamp = dict.payload["PKJS_SLEEP_TIMESTAMP"];
const cfg = JSON.parse(localStorage.getItem('clay-settings'));
// TODO report last episode as "Configure app settings!" if CLAY_API_HOST, CLAY_API_KEY, or CLAY_USER is empty
// TODO use API to dynamically determine user (prompt on watch)
if (cfg.CLAY_API_IS_JELLYFIN == true) {
callAPI(cfg.CLAY_API_HOST + "/System/ActivityLog/Entries", cfg.CLAY_API_KEY, true, cfg.CLAY_USER, sleepTimestamp);
callAPI(cfg.CLAY_API_HOST + "/System/ActivityLog/Entries?limit=50", cfg.CLAY_API_KEY, true, cfg.CLAY_USER, sleepTimestamp * 1000);
} else {
// The official history endpoint doesn't seem to return history w/o Plex Pass.
// If someone with Plex Pass wants to give it a shot, I'd be happy to accept a PR adding this as an option.
// Until then, I'm using Tautulli because it's free.
//callAPI(cfg.CLAY_API_HOST + "/status/sessions/history/all", cfg.CLAY_API_KEY, false, cfg.CLAY_USER, sleepTimestamp);
callAPI(cfg.CLAY_API_HOST + "/api/v2?apikey=" + cfg.CLAY_API_KEY + "&cmd=get_history", null, false, cfg.CLAY_USER, sleepTimestamp);
callAPI(cfg.CLAY_API_HOST + "/api/v2?apikey=" + cfg.CLAY_API_KEY + "&cmd=get_history&length=50", null, false, cfg.CLAY_USER, sleepTimestamp);
}
}
});
@@ -30,27 +30,43 @@ function callAPI(fullURL, apiKey, isJellyfin, trackedUser, sleepTimestamp) {
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
const resp = JSON.parse(xhr.responseText);
if (resp && Array.isArray(resp.Items)) {
const re = /^(.+) is playing (.+) on .*$/;
let arr;
let logTimestamp;
if (resp && ((isJellyfin === true && Array.isArray(resp.Items)) || (isJellyfin === false && Array.isArray(resp.response.data.data)))) {
let delta;
let bestDelta = Infinity;
let lastWatched;
resp.Items.forEach(function (item) {
arr = re.exec(item.Name);
if (arr != null && arr[1] == trackedUser) {
logTimestamp = Date.parse(item.Date);
if (!Number.isNaN(logTimestamp)) {
delta = Math.abs(logTimestamp - sleepTimestamp);
if (delta < bestDelta) {
bestDelta = delta;
lastWatched = arr[2];
let lastWatched = null;
if (isJellyfin === true) {
const re = /^(.+) is playing (.+) on .*$/;
let arr;
let logTimestamp;
resp.Items.forEach(function (item) {
arr = re.exec(item.Name);
if (arr != null && arr[1] == trackedUser) {
logTimestamp = Date.parse(item.Date);
if (!Number.isNaN(logTimestamp)) {
delta = Math.abs(logTimestamp - sleepTimestamp);
if (delta < bestDelta) {
bestDelta = delta;
lastWatched = arr[2];
}
}
}
}
});
console.log("Last watched: " + lastWatched)
});
} else {
resp.response.data.data.forEach(function (item) {
if (item.user == trackedUser) {
// we use "date" instead of "started" because started tracks the first
// time the user watched the media, not just the start of the last session.
delta = Math.abs(item.date - sleepTimestamp);
if (delta < bestDelta) {
bestDelta = delta;
lastWatched = item.full_title;
}
}
});
}
if (lastWatched != null) {
console.log("Last watched: " + lastWatched);
}
}
}
};