Browse Source

playlist navigation functionality

Brandon Wong 1 year ago
parent
commit
0e48d260a4

+ 250 - 0
2025-03-31-3-claude-function-to-find-next-video.md

@@ -0,0 +1,250 @@
+## Me
+
+Here is some code from /home/brwong/projects/experiments/playlist/background.js:
+
+```javascript
+function findNext(playlists, url) {
+  // Extract the "v" query parameter from input URL
+  let v;
+  try {
+    const urlObj = new URL(url);
+    v = urlObj.searchParams.get("v");
+  } catch (e) {
+    return false;
+  }
+
+  // If no "v" parameter found, return false
+  if (!v) {
+    return false;
+  }
+
+  // Check each video in the playlists
+  for (const key in playlists) {
+    const items = playlists[key];
+
+    // Check each item in the array
+    for (const item of items) {
+      try {
+        const itemUrl = new URL(item.url);
+        const itemVParam = itemUrl.searchParams.get("v");
+
+        // If the "v" parameters match, return this key
+        if (itemVParam === v) {
+          return key;
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
+      }
+    }
+  }
+
+  // No match found
+  return false;
+}
+```
+refactor this function so that if the url is found in one of the arrays, it returns the subsequent item in the array if there is one present, if the url is not found, or if it is the last item in the array, return false as before
+
+## CodeCompanion (Anthropic)
+
+### Reasoning
+
+Let's break down the current function and the refactoring requirements:
+
+Current Function:
+- `findNext` takes `playlists` and `url` as parameters
+- It extracts the "v" parameter from the URL
+- It checks if the "v" parameter is present in any of the playlist items
+- It returns the playlist key if a match is found, or false otherwise
+
+Refactoring Requirements:
+- If the URL is found in a playlist array, return the subsequent item (next item) in that array, if one exists
+- If the URL is not found in any playlist, return false
+- If the URL is the last item in the array, return false
+
+Here's my approach to refactoring:
+
+1. Extract the "v" parameter from the input URL
+2. Iterate through each playlist
+3. For each playlist, iterate through the items
+4. If a match is found by comparing the "v" parameter:
+   - Check if there's a next item in that playlist array
+   - If there is, return that next item
+   - If there isn't (it's the last item), return false
+5. If no match is found in any playlist, return false
+
+Let's write the refactored code:
+
+```javascript
+function findNext(playlists, url) {
+  // Extract the "v" query parameter from input URL
+  let v;
+  try {
+    const urlObj = new URL(url);
+    v = urlObj.searchParams.get("v");
+  } catch (e) {
+    return false;
+  }
+
+  // If no "v" parameter found, return false
+  if (!v) {
+    return false;
+  }
+
+  // Check each video in the playlists
+  for (const key in playlists) {
+    const items = playlists[key];
+
+    // Check each item in the array
+    for (let i = 0; i < items.length; i++) {
+      try {
+        const itemUrl = new URL(items[i].url);
+        const itemVParam = itemUrl.searchParams.get("v");
+
+        // If the "v" parameters match
+        if (itemVParam === v) {
+          // Check if this is the last item in the array
+          if (i === items.length - 1) {
+            return false; // Last item, no next
+          } else {
+            return items[i + 1]; // Return the next item
+          }
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
+      }
+    }
+  }
+
+  // No match found
+  return false;
+}
+```
+
+Wait, I noticed an issue. The original function was returning the key of the playlist when a match was found, but the requirements ask to return the next item in the array. Let me review the original code more carefully.
+
+Looking at the original code, it searches for a video in the playlists and returns the playlist key when the video is found. The refactored function should instead return the next item in the playlist array if it exists.
+
+Let me make another pass at refactoring:
+
+```javascript
+function findNext(playlists, url) {
+  // Extract the "v" query parameter from input URL
+  let v;
+  try {
+    const urlObj = new URL(url);
+    v = urlObj.searchParams.get("v");
+  } catch (e) {
+    return false;
+  }
+
+  // If no "v" parameter found, return false
+  if (!v) {
+    return false;
+  }
+
+  // Check each video in the playlists
+  for (const key in playlists) {
+    const items = playlists[key];
+
+    // Check each item in the array
+    for (let i = 0; i < items.length; i++) {
+      try {
+        const itemUrl = new URL(items[i].url);
+        const itemVParam = itemUrl.searchParams.get("v");
+
+        // If the "v" parameters match
+        if (itemVParam === v) {
+          // Check if this is the last item in the array
+          if (i === items.length - 1) {
+            return false; // Last item, no next
+          } else {
+            return items[i + 1]; // Return the next item
+          }
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
+      }
+    }
+  }
+
+  // No match found
+  return false;
+}
+```
+
+This looks correct based on the requirements. The function now:
+
+1. Extracts the "v" parameter from the URL
+2. If the URL is malformed or doesn't have a "v" parameter, returns false
+3. Searches through each playlist for a matching item based on the "v" parameter
+4. If a match is found:
+   - If it's the last item in the array, returns false
+   - Otherwise, returns the next item in the array
+5. If no match is found in any playlist, returns false
+
+I believe this refactored function meets the requirements.
+
+### Response
+
+I'll refactor the function to return the next item in the array instead of the playlist key.
+
+```javascript
+function findNext(playlists, url) {
+  // Extract the "v" query parameter from input URL
+  let v;
+  try {
+    const urlObj = new URL(url);
+    v = urlObj.searchParams.get("v");
+  } catch (e) {
+    return false;
+  }
+
+  // If no "v" parameter found, return false
+  if (!v) {
+    return false;
+  }
+
+  // Check each video in the playlists
+  for (const key in playlists) {
+    const items = playlists[key];
+
+    // Check each item in the array
+    for (let i = 0; i < items.length; i++) {
+      try {
+        const itemUrl = new URL(items[i].url);
+        const itemVParam = itemUrl.searchParams.get("v");
+
+        // If the "v" parameters match
+        if (itemVParam === v) {
+          // Check if this is the last item in the array
+          if (i === items.length - 1) {
+            return false; // Last item, no next item available
+          } else {
+            return items[i + 1]; // Return the next item
+          }
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
+      }
+    }
+  }
+
+  // No match found
+  return false;
+}
+```
+
+The key changes:
+- Used a regular for loop with index `i` instead of `for...of` to track position
+- When a match is found, check if it's the last item in the array
+- If it's not the last item, return the next item directly
+
+Would you like me to add any comments or make any other improvements to this refactored code?
+
+## Me
+
+

+ 163 - 0
2025-03-31-4-claude-waiting-for-navigation.md

@@ -0,0 +1,163 @@
+## Me
+
+Here is some code from /home/brwong/projects/experiments/playlist/background.js:
+
+```javascript
+    await browser.tabs.update(tabId, { url: nextVideo.url });
+```
+in my firefox extension, after telling a tab to navigate to a new page, how can I wait until the navigation is complete?
+
+## CodeCompanion (Anthropic)
+
+### Reasoning
+
+The user is asking how to wait for a page navigation to complete in a Firefox extension after using `browser.tabs.update()` to navigate to a new URL.
+
+In Firefox extensions, there are a few ways to wait for navigation to complete:
+
+1. Use the `browser.webNavigation.onCompleted` event listener
+2. Use the `browser.tabs.onUpdated` event listener with a filter for the "complete" status
+3. Use `browser.tabs.executeScript` to check if the page is loaded (less reliable)
+
+Let me provide a solution using the `browser.webNavigation.onCompleted` event, which is specifically designed for this purpose:
+
+```javascript
+async function navigateAndWait(tabId, url) {
+  return new Promise((resolve) => {
+    const listener = (details) => {
+      if (details.tabId === tabId && details.frameId === 0) {
+        browser.webNavigation.onCompleted.removeListener(listener);
+        resolve();
+      }
+    };
+    
+    browser.webNavigation.onCompleted.addListener(listener);
+    browser.tabs.update(tabId, { url: url });
+  });
+}
+
+// Usage
+await navigateAndWait(tabId, nextVideo.url);
+```
+
+Alternatively, they could use the `browser.tabs.onUpdated` event:
+
+```javascript
+async function navigateAndWait(tabId, url) {
+  return new Promise((resolve) => {
+    const listener = (updatedTabId, changeInfo, tab) => {
+      if (updatedTabId === tabId && changeInfo.status === 'complete') {
+        browser.tabs.onUpdated.removeListener(listener);
+        resolve(tab);
+      }
+    };
+    
+    browser.tabs.onUpdated.addListener(listener);
+    browser.tabs.update(tabId, { url: url });
+  });
+}
+
+// Usage
+await navigateAndWait(tabId, nextVideo.url);
+```
+
+I think I should provide both options and explain the differences so the user can choose the one that best fits their needs.
+
+### Response
+
+To wait for navigation to complete after using `browser.tabs.update()`, you can use either the `webNavigation.onCompleted` event or the `tabs.onUpdated` event. Here are both approaches:
+
+### Option 1: Using webNavigation.onCompleted (recommended)
+
+```javascript
+async function navigateAndWait(tabId, url) {
+  return new Promise((resolve) => {
+    const listener = (details) => {
+      if (details.tabId === tabId && details.frameId === 0) {
+        browser.webNavigation.onCompleted.removeListener(listener);
+        resolve();
+      }
+    };
+    
+    browser.webNavigation.onCompleted.addListener(listener);
+    browser.tabs.update(tabId, { url });
+  });
+}
+
+// Replace your current code with:
+await navigateAndWait(tabId, nextVideo.url);
+```
+
+### Option 2: Using tabs.onUpdated
+
+```javascript
+async function navigateAndWait(tabId, url) {
+  return new Promise((resolve) => {
+    const listener = (updatedTabId, changeInfo, tab) => {
+      if (updatedTabId === tabId && changeInfo.status === 'complete') {
+        browser.tabs.onUpdated.removeListener(listener);
+        resolve(tab);
+      }
+    };
+    
+    browser.tabs.onUpdated.addListener(listener);
+    browser.tabs.update(tabId, { url });
+  });
+}
+
+// Replace your current code with:
+await navigateAndWait(tabId, nextVideo.url);
+```
+
+The first option is generally more reliable for determining when the page has fully loaded. Which permissions are declared in your manifest.json?
+
+## Me
+
+what permission do I need to add to enable this code?
+
+## CodeCompanion (Anthropic)
+
+### Reasoning
+
+To use the code I provided, you'll need to add specific permissions to your Firefox extension's manifest.json file. Let's identify the required permissions for each approach:
+
+1. For the first approach using `browser.webNavigation.onCompleted`:
+   - You need the "webNavigation" permission
+
+2. For the second approach using `browser.tabs.onUpdated`:
+   - You need the "tabs" permission
+
+Since you're already using `browser.tabs.update()`, you likely already have the "tabs" permission. However, for the first approach with `webNavigation`, you would need to add that permission if it's not already in your manifest.
+
+Let me provide the exact manifest.json permission you need to add for each approach:
+
+### Response
+
+You need to add the appropriate permission to your manifest.json file:
+
+### For Option 1 (webNavigation approach)
+```json
+{
+  "permissions": [
+    "webNavigation"
+  ]
+}
+```
+
+### For Option 2 (tabs approach)
+```json
+{
+  "permissions": [
+    "tabs"
+  ]
+}
+```
+
+Since you're already using `browser.tabs.update()`, you likely already have the "tabs" permission, but the `webNavigation` permission would need to be added if you choose the first approach.
+
+If you're adding this to existing permissions, just add the new permission to your current permissions array rather than replacing it entirely.
+
+Would you like me to explain the differences between these two approaches in more detail?
+
+## Me
+

+ 89 - 33
background.js

@@ -81,39 +81,36 @@ browser.runtime.onInstalled.addListener(() => {
     title: "Watching - 2",
     contexts: ["link"],
   });
-
-  // test message
-  //browser.tabs.sendMessage(tab.id, payload);
 });
 
-function findMatchingUrlKey(current, inputUrl) {
-  // Extract the "v" query parameter from input URL
-  let inputVParam;
+function findPlaylist(current, url) {
+  // Extract the video id from URL query params
+  let v;
   try {
-    const urlObj = new URL(inputUrl);
-    inputVParam = urlObj.searchParams.get("v");
+    const urlObj = new URL(url);
+    v = urlObj.searchParams.get("v");
   } catch (e) {
     return false;
   }
 
-  // If no "v" parameter found, return false
-  if (!inputVParam) {
+  // If no video id found, return false
+  if (!v) {
     return false;
   }
 
-  // Check each key in the current object
-  for (const key in current) {
-    const items = current[key];
+  // Check each playlist in the current set
+  for (const pl in current) {
+    const videos = current[pl];
 
-    // Check each item in the array
-    for (const item of items) {
+    // Check each video in the array
+    for (const video of videos) {
       try {
-        const itemUrl = new URL(item.url);
+        const itemUrl = new URL(video.url);
         const itemVParam = itemUrl.searchParams.get("v");
 
-        // If the "v" parameters match, return this key
-        if (itemVParam === inputVParam) {
-          return key;
+        // If the "v" parameters match, return this pl
+        if (itemVParam === v) {
+          return pl;
         }
       } catch (e) {
         // Skip malformed URLs
@@ -130,7 +127,7 @@ async function addLinkToPlaylist(plName, item) {
   const { playlists: currentPlaylists } =
     await browser.storage.local.get("playlists");
 
-  const alreadyHave = findMatchingUrlKey(currentPlaylists, item.linkUrl);
+  const alreadyHave = findPlaylist(currentPlaylists, item.linkUrl);
 
   if (alreadyHave) {
     console.log("already have that link in", alreadyHave);
@@ -214,9 +211,78 @@ async function updateHistory(message) {
   }
 }
 
+function findNext(playlists, url) {
+  // Extract the "v" query parameter from input URL
+  let v;
+  try {
+    const urlObj = new URL(url);
+    v = urlObj.searchParams.get("v");
+  } catch (e) {
+    return false;
+  }
+
+  // If no "v" parameter found, return false
+  if (!v) {
+    return false;
+  }
+
+  // Check each video in the playlists
+  for (const pl in playlists) {
+    const videos = playlists[pl];
+
+    // Check each video in the array
+    for (let i = 0; i < videos.length; i++) {
+      try {
+        const itemUrl = new URL(videos[i].url);
+        const itemVParam = itemUrl.searchParams.get("v");
+
+        // If the "v" parameters match
+        if (itemVParam === v) {
+          console.log("FOUND VIDEO", videos[i]);
+          // Check if this is the last video in the array
+          if (i === videos.length - 1) {
+            return false; // Last video, no next video available
+          } else {
+            return videos[i + 1]; // Return the next video
+          }
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
+      }
+    }
+  }
+
+  // No match found
+  return false;
+}
+
+async function navigateAndWait(tabId, url) {
+  return new Promise((resolve) => {
+    const listener = (details) => {
+      if (details.tabId === tabId && details.frameId === 0) {
+        browser.webNavigation.onCompleted.removeListener(listener);
+        resolve();
+      }
+    };
+
+    browser.webNavigation.onCompleted.addListener(listener);
+    browser.tabs.update(tabId, { url });
+  });
+}
+
+async function advance(tabId, url) {
+  const { playlists } = await browser.storage.local.get("playlists");
+  const nextVideo = await findNext(playlists, url);
+  if (nextVideo) {
+    await navigateAndWait(tabId, nextVideo.url);
+    await browser.tabs.sendMessage(tabId, { type: "autoplay" });
+  }
+}
+
 // Listen for messages from popup or content scripts
-browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  console.log("MESSAGE", message);
+browser.runtime.onMessage.addListener((message, sender, _sendResponse) => {
+  console.log("MESSAGE", message, sender);
   switch (message.type) {
     case "play":
     case "playing":
@@ -225,17 +291,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
       break;
     case "ended":
       updateHistory(message);
-      console.log("ENDED");
+      advance(sender.tab.id, message.url);
       break;
   }
-  if (message.command === "navigate") {
-    if (message.tabId && message.url) {
-      navigateTab(message.tabId, message.url)
-        .then(() => sendResponse({ status: "success" }))
-        .catch((error) =>
-          sendResponse({ status: "error", message: error.message }),
-        );
-      return true; // Required for async sendResponse
-    }
-  }
 });

+ 9 - 26
content_scripts/content.js

@@ -17,47 +17,30 @@
   if (vid) {
     vid.addEventListener("play", function (evt) {
       msgPlayEvt("play", evt.target);
-      console.log("NOW PLAYING (msg here for test)");
     });
     vid.addEventListener("playing", function (evt) {
       msgPlayEvt("playing", evt.target);
-      console.log("playing... (msg here for test)");
     });
     vid.addEventListener("pause", function (evt) {
       msgPlayEvt("pause", evt.target);
-      console.log("paused (msg here for test)");
     });
     vid.addEventListener("ended", function (evt) {
       msgPlayEvt("ended", evt.target);
-      console.log("ON TO THE NEXT ONE! ...but not yet.");
-      setTimeout(async function () {
-        //  const msg = {
-        //    type: "requestNext",
-        //    //currentVideo: 'something',
-        //    //currentList: 'something'
-        //  };
-        //  const resp = await chrome.runtime.sendMessage(msg);
-        //  console.log("NAVIGATING?", resp);
-        //  if( resp.goto ) {
-        //    console.log("NAVIGATING to", resp.goto);
-        //    window.location = resp.goto;
-        //    console.log("AFTER?");
-        //  }
-
-        //TODO: MOVE "NEXT" REQUEST TO BACKGROUND SCRIPT
-        //      (background script will determine if this tab is playing a playlist)
-        msgPlayEvt("next", evt.target);
-      }, 3000);
     });
-    console.log("ATTACHED!");
   }
 
   // Listen for messages from the popup or background script
   browser.runtime.onMessage.addListener((message) => {
-    console.log("MESSAGE", message);
     if (message.type === "print") {
-      console.log(message);
-      return Promise.resolve({ status: "Injection completed" });
+      console.log("MESSAGE", message);
+    }
+    if (message.type === "autoplay") {
+      const play = document.querySelector(
+        'button.ytp-play-button.ytp-button[data-title-no-tooltip="Play"]',
+      );
+      if (play) {
+        play.click();
+      }
     }
   });
 })();

+ 1 - 0
manifest.json

@@ -13,6 +13,7 @@
     "storage",
     "contextMenus",
     "notifications",
+    "webNavigation",
     "<all_urls>"
   ],
   "browser_action": {