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/) >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?). >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 # Spring 2026 Developer Contest
This app was entered in the Spring 2026 Developer Contest on the official Pebble appstore. 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) { Pebble.addEventListener("appmessage", function (dict) {
if (dict.payload["PKJS_SLEEP_TIMESTAMP"]) { 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')); 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 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) // TODO use API to dynamically determine user (prompt on watch)
if (cfg.CLAY_API_IS_JELLYFIN == true) { 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 { } else {
// The official history endpoint doesn't seem to return history w/o Plex Pass. // 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. // 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. // 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 + "/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,13 +30,14 @@ function callAPI(fullURL, apiKey, isJellyfin, trackedUser, sleepTimestamp) {
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
const resp = JSON.parse(xhr.responseText); const resp = JSON.parse(xhr.responseText);
if (resp && Array.isArray(resp.Items)) { if (resp && ((isJellyfin === true && Array.isArray(resp.Items)) || (isJellyfin === false && Array.isArray(resp.response.data.data)))) {
let delta;
let bestDelta = Infinity;
let lastWatched = null;
if (isJellyfin === true) {
const re = /^(.+) is playing (.+) on .*$/; const re = /^(.+) is playing (.+) on .*$/;
let arr; let arr;
let logTimestamp; let logTimestamp;
let delta;
let bestDelta = Infinity;
let lastWatched;
resp.Items.forEach(function (item) { resp.Items.forEach(function (item) {
arr = re.exec(item.Name); arr = re.exec(item.Name);
if (arr != null && arr[1] == trackedUser) { if (arr != null && arr[1] == trackedUser) {
@@ -50,7 +51,22 @@ function callAPI(fullURL, apiKey, isJellyfin, trackedUser, sleepTimestamp) {
} }
} }
}); });
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);
}
} }
} }
}; };