Plex playlist played status

Show played status on playlist when using Plex

  1. // ==UserScript==
  2. // @name Plex playlist played status
  3. // @description Show played status on playlist when using Plex
  4. // @version 1.1
  5. // @namespace cybolic.me
  6. // @author Christian Dannie Storgaard
  7. // @include https://app.plex.tv/desktop
  8. // @include https://app.plex.tv/desktop/*
  9. // @include https://app.plex.tv/desktop#*
  10. // @include http://localhost:32400/web/*
  11. // @include http://127.0.0.1:32400/web/*
  12. // @include https://localhost:32400/web/*
  13. // @include https://127.0.0.1:32400/web/*
  14. // @grant none
  15. // @license GNU GPLv3
  16. // ==/UserScript==
  17.  
  18. var info = {
  19. serverId: '',
  20. playlistId: '',
  21. playlistDOM: undefined
  22. };
  23.  
  24. var logMessage = function (msg) {
  25. console.info(`[Plex Playlist Played] ${msg}`);
  26. };
  27.  
  28. var getCurrentUsersData = function () {
  29. return JSON.parse(localStorage.users)?.users || [];
  30. };
  31.  
  32. var getCurrentServerId = function () {
  33. return window.location.hash
  34. .split('/server/').pop()
  35. .split?.('/').shift();
  36. };
  37. var getCurrentPlaylistId = function () {
  38. return decodeURIComponent(window.location.hash)
  39. .split('key=/playlists/').pop()
  40. .split?.('&').shift();
  41. };
  42.  
  43.  
  44. var getServerDetailsForAll = function () {
  45. return Object.fromEntries(
  46. getCurrentUsersData()
  47. .filter(user => user.servers.length)
  48. .map(userWithServers => userWithServers.servers)
  49. .flat()
  50. .map(server => [ server.machineIdentifier, server ])
  51. );
  52. }
  53.  
  54. var getServerDetailsForId = function (serverId) {
  55. return getServerDetailsForAll()[serverId];
  56. };
  57.  
  58. var getPlaylist = function (serverId, playlistId) {
  59. const serverDetails = getServerDetailsForId(serverId);
  60. const url = `${serverDetails.connections.shift()?.uri || 'https://localhost:32400'}/playlists/${playlistId}/items?includeExternalMedia=1&X-Plex-Token=${serverDetails.accessToken}`;
  61. logMessage(`looking up ${url}`);
  62. return fetch(url)
  63. .then(response => response.text())
  64. .then(responseText => (new DOMParser()).parseFromString(responseText, 'application/xml'));
  65. };
  66.  
  67. var getPlaylistElementFromKey = function (serverId, key) {
  68. return document.querySelectorAll(`a[data-testid="metadataTitleLink"][href*="/server/${serverId}"][href*="${encodeURIComponent(key)}"]`).shift();
  69. };
  70.  
  71. var addStyle = function (css) {
  72. let shouldAddToHead = false;
  73. let style = document.querySelector('#plex-playlist-played-css');
  74. if (style == null) {
  75. style = document.createElement('style');
  76. style.id = 'plex-playlist-played-css';
  77. shouldAddToHead = true;
  78. }
  79.  
  80. style.textContent = css;
  81.  
  82. if (shouldAddToHead) {
  83. document.head.append(style);
  84. }
  85. };
  86.  
  87. var getCssForPlaylist = function (serverId, dom) {
  88. logMessage("creating css");
  89. const videosNotPlayed = Array.from(dom.querySelectorAll('Video:not([viewCount])'));
  90. const css = videosNotPlayed
  91. // get the library key for these items
  92. .map(video => ({
  93. key: video.attributes['key'].value,
  94. type: video.attributes['type'].value,
  95. }))
  96. // generate a CSS rule for each playlist item that targets the only element we can track, the link to the item
  97. .map(attrs => {
  98. const selector = `a[data-testid="metadataTitleLink"][href*="/server/${serverId}"][href*="${encodeURIComponent(attrs.key)}&"]`;
  99. return `${selector}::after {
  100. content: '';
  101. position: absolute;
  102. ${attrs.type === 'episode'
  103. ? `left: 155px; top: 16px;`
  104. : `left: 141px; top: 7px;`
  105. }
  106. width: 0px;
  107. height: 0px;
  108. border: 8px solid #e5a00d;
  109. border-color: #e5a00d #e5a00d transparent transparent;
  110. filter: drop-shadow(0px 1px 0px rgba(0,0,0,0.5));
  111. pointer-events: none;
  112. }`;
  113. })
  114. .join('\n');
  115. return css;
  116. };
  117.  
  118. var checkCurrentPage = function () {
  119. logMessage("checking if current location is playlist");
  120. if (window.location.hash.includes(`key=${encodeURIComponent('/playlists/')}`)) {
  121. logMessage("current page is playlist; fetching playlist data");
  122. info.serverId = getCurrentServerId();
  123. info.playlistId = getCurrentPlaylistId();
  124. getPlaylist(info.serverId, info.playlistId).then(dom => {
  125. logMessage("playlist data fetched, creating CSS");
  126. info.playlistDOM = dom;
  127. const css = getCssForPlaylist(info.serverId, info.playlistDOM);
  128. addStyle(css);
  129. logMessage('play status added');
  130. });
  131. }
  132. };
  133.  
  134. window.addEventListener('hashchange', checkCurrentPage);
  135. logMessage("added event handler");
  136. checkCurrentPage();