Show played status on playlist when using Plex
// ==UserScript==
// @name Plex playlist played status
// @description Show played status on playlist when using Plex
// @version 1.1
// @namespace cybolic.me
// @author Christian Dannie Storgaard
// @include https://app.plex.tv/desktop
// @include https://app.plex.tv/desktop/*
// @include https://app.plex.tv/desktop#*
// @include http://localhost:32400/web/*
// @include http://127.0.0.1:32400/web/*
// @include https://localhost:32400/web/*
// @include https://127.0.0.1:32400/web/*
// @grant none
// @license GNU GPLv3
// ==/UserScript==
var info = {
serverId: '',
playlistId: '',
playlistDOM: undefined
};
var logMessage = function (msg) {
console.info(`[Plex Playlist Played] ${msg}`);
};
var getCurrentUsersData = function () {
return JSON.parse(localStorage.users)?.users || [];
};
var getCurrentServerId = function () {
return window.location.hash
.split('/server/').pop()
.split?.('/').shift();
};
var getCurrentPlaylistId = function () {
return decodeURIComponent(window.location.hash)
.split('key=/playlists/').pop()
.split?.('&').shift();
};
var getServerDetailsForAll = function () {
return Object.fromEntries(
getCurrentUsersData()
.filter(user => user.servers.length)
.map(userWithServers => userWithServers.servers)
.flat()
.map(server => [ server.machineIdentifier, server ])
);
}
var getServerDetailsForId = function (serverId) {
return getServerDetailsForAll()[serverId];
};
var getPlaylist = function (serverId, playlistId) {
const serverDetails = getServerDetailsForId(serverId);
const url = `${serverDetails.connections.shift()?.uri || 'https://localhost:32400'}/playlists/${playlistId}/items?includeExternalMedia=1&X-Plex-Token=${serverDetails.accessToken}`;
logMessage(`looking up ${url}`);
return fetch(url)
.then(response => response.text())
.then(responseText => (new DOMParser()).parseFromString(responseText, 'application/xml'));
};
var getPlaylistElementFromKey = function (serverId, key) {
return document.querySelectorAll(`a[data-testid="metadataTitleLink"][href*="/server/${serverId}"][href*="${encodeURIComponent(key)}"]`).shift();
};
var addStyle = function (css) {
let shouldAddToHead = false;
let style = document.querySelector('#plex-playlist-played-css');
if (style == null) {
style = document.createElement('style');
style.id = 'plex-playlist-played-css';
shouldAddToHead = true;
}
style.textContent = css;
if (shouldAddToHead) {
document.head.append(style);
}
};
var getCssForPlaylist = function (serverId, dom) {
logMessage("creating css");
const videosNotPlayed = Array.from(dom.querySelectorAll('Video:not([viewCount])'));
const css = videosNotPlayed
// get the library key for these items
.map(video => ({
key: video.attributes['key'].value,
type: video.attributes['type'].value,
}))
// generate a CSS rule for each playlist item that targets the only element we can track, the link to the item
.map(attrs => {
const selector = `a[data-testid="metadataTitleLink"][href*="/server/${serverId}"][href*="${encodeURIComponent(attrs.key)}&"]`;
return `${selector}::after {
content: '';
position: absolute;
${attrs.type === 'episode'
? `left: 155px; top: 16px;`
: `left: 141px; top: 7px;`
}
width: 0px;
height: 0px;
border: 8px solid #e5a00d;
border-color: #e5a00d #e5a00d transparent transparent;
filter: drop-shadow(0px 1px 0px rgba(0,0,0,0.5));
pointer-events: none;
}`;
})
.join('\n');
return css;
};
var checkCurrentPage = function () {
logMessage("checking if current location is playlist");
if (window.location.hash.includes(`key=${encodeURIComponent('/playlists/')}`)) {
logMessage("current page is playlist; fetching playlist data");
info.serverId = getCurrentServerId();
info.playlistId = getCurrentPlaylistId();
getPlaylist(info.serverId, info.playlistId).then(dom => {
logMessage("playlist data fetched, creating CSS");
info.playlistDOM = dom;
const css = getCssForPlaylist(info.serverId, info.playlistDOM);
addStyle(css);
logMessage('play status added');
});
}
};
window.addEventListener('hashchange', checkCurrentPage);
logMessage("added event handler");
checkCurrentPage();