Browse Source

WIP: manually adding functionalities (with limited copying from old playlist project)

Brandon Wong 1 year ago
parent
commit
0f58dd2c34
4 changed files with 560 additions and 132 deletions
  1. 298 0
      2025-03-30-1-claude-function-to-upsert-link.md
  2. 202 94
      background.js
  3. 55 33
      content_scripts/content.js
  4. 5 5
      manifest.json

+ 298 - 0
2025-03-30-1-claude-function-to-upsert-link.md

@@ -0,0 +1,298 @@
+## Me
+
+`current` is a javascript object with string keys and array values. the values themselves are objects that look like this: {url: string, title: string}. write a function that will accept `current` and a url, and return the key if one of the arrays contains a url that has the same query param ("v") as the input url, and false if none of them do.
+
+## CodeCompanion (Anthropic)
+
+### Reasoning
+
+Alright, let's break down this problem:
+
+1. We have a JavaScript object called `current` where:
+   - The keys are strings
+   - The values are arrays of objects, where each object has `url` and `title` properties
+
+2. We need to write a function that takes:
+   - The `current` object
+   - A URL string
+
+3. The function should:
+   - Check if any of the arrays in `current` contain an object with a `url` property that has the same "v" query parameter as the input URL
+   - Return the key of the array if found
+   - Return `false` if not found
+
+To solve this, we need to:
+1. Parse the "v" query parameter from the input URL
+2. Iterate through each key-value pair in the `current` object
+3. For each array, check if any of the objects inside have a URL with the same "v" query parameter
+4. Return the key if found, or `false` otherwise
+
+Let's write pseudocode for this:
+
+```
+FUNCTION findKeyByUrlQueryParam(current, inputUrl)
+    // Extract the "v" query parameter from the input URL
+    inputVParam = extractVQueryParam(inputUrl)
+    
+    // If the input URL doesn't have a "v" parameter, return false
+    IF inputVParam IS empty OR null OR undefined THEN
+        RETURN false
+    END IF
+    
+    // Iterate through each key-value pair in the current object
+    FOR EACH key, array IN current
+        // Check each object in the array
+        FOR EACH item IN array
+            // Extract the "v" query parameter from the item's URL
+            itemVParam = extractVQueryParam(item.url)
+            
+            // If the "v" parameters match, return the key
+            IF itemVParam equals inputVParam THEN
+                RETURN key
+            END IF
+        END FOR
+    END FOR
+    
+    // If no match is found, return false
+    RETURN false
+END FUNCTION
+
+FUNCTION extractVQueryParam(url)
+    // Parse the URL to extract the "v" query parameter
+    // Return the value of the "v" parameter, or null if not found
+END FUNCTION
+```
+
+Now, let's implement this in JavaScript. For extracting query parameters, we can use the built-in `URL` and `URLSearchParams` objects:
+
+```javascript
+function findKeyByUrlQueryParam(current, inputUrl) {
+    // Extract the "v" query parameter from the input URL
+    const inputVParam = extractVQueryParam(inputUrl);
+    
+    // If the input URL doesn't have a "v" parameter, return false
+    if (!inputVParam) {
+        return false;
+    }
+    
+    // Iterate through each key-value pair in the current object
+    for (const key in current) {
+        const array = current[key];
+        
+        // Check each object in the array
+        for (const item of array) {
+            // Extract the "v" query parameter from the item's URL
+            const itemVParam = extractVQueryParam(item.url);
+            
+            // If the "v" parameters match, return the key
+            if (itemVParam === inputVParam) {
+                return key;
+            }
+        }
+    }
+    
+    // If no match is found, return false
+    return false;
+}
+
+function extractVQueryParam(url) {
+    try {
+        const urlObj = new URL(url);
+        return urlObj.searchParams.get('v');
+    } catch (e) {
+        // If the URL is malformed, return null
+        return null;
+    }
+}
+```
+
+This solution should work for most cases, but there are a few considerations:
+
+1. It assumes that the "v" query parameter is present in at least one URL. If none of the URLs have a "v" parameter, the function will return `false`.
+2. It handles malformed URLs by returning `null` from the `extractVQueryParam` function.
+3. It's case-sensitive for the "v" parameter (i.e., "v" is different from "V"). If case-insensitive comparison is needed, we can modify the code.
+
+Let me review this solution once more:
+
+The solution checks if any URL in any of the arrays in the `current` object has the same "v" query parameter as the input URL. If there's a match, it returns the key of the array. If not, it returns `false`.
+
+I think the solution is correct and meets all the requirements. Here's the final code:
+
+```javascript
+function findKeyByUrlQueryParam(current, inputUrl) {
+    // Extract the "v" query parameter from the input URL
+    const inputVParam = extractVQueryParam(inputUrl);
+    
+    // If the input URL doesn't have a "v" parameter, return false
+    if (!inputVParam) {
+        return false;
+    }
+    
+    // Iterate through each key-value pair in the current object
+    for (const key in current) {
+        const array = current[key];
+        
+        // Check each object in the array
+        for (const item of array) {
+            // Extract the "v" query parameter from the item's URL
+            const itemVParam = extractVQueryParam(item.url);
+            
+            // If the "v" parameters match, return the key
+            if (itemVParam === inputVParam) {
+                return key;
+            }
+        }
+    }
+    
+    // If no match is found, return false
+    return false;
+}
+
+function extractVQueryParam(url) {
+    try {
+        const urlObj = new URL(url);
+        return urlObj.searchParams.get('v');
+    } catch (e) {
+        // If the URL is malformed, return null
+        return null;
+    }
+}
+```
+
+Actually, upon further reflection, I'm not sure that I need two separate functions. I can inline the extraction logic to make the code more concise:
+
+```javascript
+function findKeyByUrlQueryParam(current, inputUrl) {
+    // Extract the "v" query parameter from the input URL
+    let inputVParam;
+    try {
+        const urlObj = new URL(inputUrl);
+        inputVParam = urlObj.searchParams.get('v');
+    } catch (e) {
+        // If the URL is malformed, return false
+        return false;
+    }
+    
+    // If the input URL doesn't have a "v" parameter, return false
+    if (!inputVParam) {
+        return false;
+    }
+    
+    // Iterate through each key-value pair in the current object
+    for (const key in current) {
+        const array = current[key];
+        
+        // Check each object in the array
+        for (const item of array) {
+            // Extract the "v" query parameter from the item's URL
+            let itemVParam;
+            try {
+                const urlObj = new URL(item.url);
+                itemVParam = urlObj.searchParams.get('v');
+            } catch (e) {
+                // If the URL is malformed, skip this item
+                continue;
+            }
+            
+            // If the "v" parameters match, return the key
+            if (itemVParam === inputVParam) {
+                return key;
+            }
+        }
+    }
+    
+    // If no match is found, return false
+    return false;
+}
+```
+
+However, using two functions makes the code more readable and easier to maintain. It follows the Single Responsibility Principle, where each function has a specific purpose. So, I prefer the first solution.
+
+Let me finalize the solution and provide the most efficient and readable code:
+
+```javascript
+function findKeyByUrlQueryParam(current, inputUrl) {
+    const inputVParam = extractVQueryParam(inputUrl);
+    
+    if (!inputVParam) {
+        return false;
+    }
+    
+    for (const key in current) {
+        const array = current[key];
+        
+        for (const item of array) {
+            const itemVParam = extractVQueryParam(item.url);
+            
+            if (itemVParam === inputVParam) {
+                return key;
+            }
+        }
+    }
+    
+    return false;
+}
+
+function extractVQueryParam(url) {
+    try {
+        const urlObj = new URL(url);
+        return urlObj.searchParams.get('v');
+    } catch (e) {
+        return null;
+    }
+}
+```
+
+I think this solution is correct, efficient, and readable.
+
+### Response
+
+Let me write a function to check for matching "v" query parameters in URLs:
+
+```javascript
+function findMatchingUrlKey(current, inputUrl) {
+  // Extract the "v" query parameter from input URL
+  let inputVParam;
+  try {
+    const urlObj = new URL(inputUrl);
+    inputVParam = urlObj.searchParams.get('v');
+  } catch (e) {
+    return false;
+  }
+  
+  // If no "v" parameter found, return false
+  if (!inputVParam) {
+    return false;
+  }
+  
+  // Check each key in the current object
+  for (const key in current) {
+    const items = current[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 === inputVParam) {
+          return key;
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
+      }
+    }
+  }
+  
+  // No match found
+  return false;
+}
+```
+
+Would you like me to add any additional validation or error handling to this function?
+
+## Me
+
+

+ 202 - 94
background.js

@@ -1,101 +1,153 @@
 // Background script for the extension
 
+browser.storage.local.get("playlists").then(function (data) {
+  if (!data.playlists) {
+    console.log("pre-populating playlists");
+    browser.storage.local.set({
+      playlists: {
+        "listening-1": [],
+        "listening-2": [],
+        "listening-3": [],
+        "watching-1": [],
+        "watching-2": [],
+      },
+    });
+  } else {
+    console.log("no need to pre-populate playlists");
+  }
+});
+
+browser.storage.local.get("history").then(function (data) {
+  if (!data.history) {
+    console.log("pre-populating history");
+    browser.storage.local.set({
+      history: {},
+    });
+  } else {
+    console.log("no need to pre-populate history");
+  }
+});
+
 // Create context menu items when the extension is installed
 browser.runtime.onInstalled.addListener(() => {
   // Parent menu item
   browser.contextMenus.create({
-    id: "my-extension-menu",
-    title: "My Firefox Extension",
-    contexts: ["all"]
+    id: "my-playlist-menu",
+    title: "Add to Playlist",
+    contexts: ["link"],
+    targetUrlPatterns: ["https://*.youtube.com/watch*"],
   });
-  
+
   // Sub-menu items
   browser.contextMenus.create({
-    id: "inject-script",
-    parentId: "my-extension-menu",
-    title: "Inject Script",
-    contexts: ["all"]
+    id: "listening-1",
+    parentId: "my-playlist-menu",
+    title: "Listening - 1",
+    contexts: ["link"],
   });
-  
+
   browser.contextMenus.create({
-    id: "save-selection",
-    parentId: "my-extension-menu",
-    title: "Save Selection",
-    contexts: ["selection"]
+    id: "listening-2",
+    parentId: "my-playlist-menu",
+    title: "Listening - 2",
+    contexts: ["link"],
   });
-  
+
   browser.contextMenus.create({
-    id: "navigate-to-homepage",
-    parentId: "my-extension-menu",
-    title: "Go to Homepage",
-    contexts: ["all"]
+    id: "listening-3",
+    parentId: "my-playlist-menu",
+    title: "Listening - 3",
+    contexts: ["link"],
   });
 
   // Separator
   browser.contextMenus.create({
     id: "separator-1",
-    parentId: "my-extension-menu",
+    parentId: "my-playlist-menu",
     type: "separator",
-    contexts: ["all"]
+    contexts: ["link"],
   });
-  
+
   browser.contextMenus.create({
-    id: "custom-action",
-    parentId: "my-extension-menu",
-    title: "Custom Action",
-    contexts: ["link", "image"]
+    id: "watching-1",
+    parentId: "my-playlist-menu",
+    title: "Watching - 1",
+    contexts: ["link"],
   });
+
+  browser.contextMenus.create({
+    id: "watching-2",
+    parentId: "my-playlist-menu",
+    title: "Watching - 2",
+    contexts: ["link"],
+  });
+
+  // test message
+  //browser.tabs.sendMessage(tab.id, payload);
 });
 
-// Context menu click handler
-browser.contextMenus.onClicked.addListener((info, tab) => {
-  switch (info.menuItemId) {
-    case "inject-script":
-      browser.tabs.sendMessage(tab.id, { command: 'injectCode' })
-        .catch(error => {
-          // If content script isn't loaded yet, inject it first
-          browser.tabs.executeScript(tab.id, { file: '/content_scripts/content.js' })
-            .then(() => {
-              browser.tabs.sendMessage(tab.id, { command: 'injectCode' });
-            });
-        });
-      break;
-      
-    case "save-selection":
-      if (info.selectionText) {
-        browser.storage.local.set({
-          'lastSelection': info.selectionText,
-          'selectionTime': Date.now()
-        }).then(() => {
-          // Optional: Show a notification
-          browser.notifications.create({
-            type: "basic",
-            iconUrl: browser.runtime.getURL("icons/icon-48.png"),
-            title: "Selection Saved",
-            message: "Text has been saved to extension storage"
-          });
-        });
-      }
-      break;
-      
-    case "navigate-to-homepage":
-      const homepage = "https://example.com";
-      browser.tabs.update(tab.id, { url: homepage });
-      break;
-      
-    case "custom-action":
-      // Handle different context types
-      if (info.linkUrl) {
-        // Custom action for links
-        console.log("Link URL:", info.linkUrl);
-        // For example, copy link to clipboard or open in new tab with modifications
-      } else if (info.srcUrl) {
-        // Custom action for images
-        console.log("Image URL:", info.srcUrl);
-        // For example, save image or process it
+function findMatchingUrlKey(current, inputUrl) {
+  // Extract the "v" query parameter from input URL
+  let inputVParam;
+  try {
+    const urlObj = new URL(inputUrl);
+    inputVParam = urlObj.searchParams.get("v");
+  } catch (e) {
+    return false;
+  }
+
+  // If no "v" parameter found, return false
+  if (!inputVParam) {
+    return false;
+  }
+
+  // Check each key in the current object
+  for (const key in current) {
+    const items = current[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 === inputVParam) {
+          return key;
+        }
+      } catch (e) {
+        // Skip malformed URLs
+        continue;
       }
-      break;
+    }
   }
+
+  // No match found
+  return false;
+}
+
+async function addLinkToPlaylist(plName, item) {
+  const { playlists: currentPlaylists } =
+    await browser.storage.local.get("playlists");
+
+  const alreadyHave = findMatchingUrlKey(currentPlaylists, item.linkUrl);
+
+  if (alreadyHave) {
+    console.log("already have that link in", alreadyHave);
+  } else {
+    const { [plName]: playlist, ...others } = currentPlaylists;
+    await browser.storage.local.set({
+      playlists: {
+        [plName]: [...playlist, { url: item.linkUrl, title: item.linkText }],
+        ...others,
+      },
+    });
+  }
+}
+
+// Context menu click handler
+browser.contextMenus.onClicked.addListener(async (item, _tab) => {
+  addLinkToPlaylist(item.menuItemId, item);
 });
 
 // Function to navigate a tab to a new URL
@@ -103,33 +155,89 @@ function navigateTab(tabId, url) {
   return browser.tabs.update(tabId, { url: url });
 }
 
+async function updateHistory(message) {
+  const { history: currentHistory } =
+    await browser.storage.local.get("history");
+  console.log("currentHistory", currentHistory);
+  const q = new URL(message.url);
+  const v = q.searchParams.get("v");
+  console.log("v??", message.url, q, q.searchParams.get("v"));
+  console.log("history?", currentHistory[v]);
+  if (currentHistory[v]) {
+    const { [v]: existing, ...rest } = currentHistory;
+    await browser.storage.local.set({
+      history: {
+        [v]: {
+          ...existing,
+          duration:
+            !isNaN(existing.duration) && isFinite(existing.duration)
+              ? Math.max(message.duration, existing.duration)
+              : message.duration,
+          history: [
+            ...existing.history,
+            {
+              action: message.type,
+              position: message.timestamp,
+              timestamp: Date.now(),
+            },
+          ],
+        },
+        ...rest,
+      },
+    });
+  } else {
+    await browser.storage.local.set({
+      history: {
+        [v]: {
+          url: message.url,
+          title: message.title,
+          duration: message.duration,
+          history: [
+            {
+              action: message.type,
+              position: message.timestamp,
+              timestamp: Date.now(),
+            },
+          ],
+        },
+        ...currentHistory,
+      },
+    });
+  }
+  console.log("proposed:", {
+    url: message.url,
+    title: message.title,
+    duration: message.duration,
+    history: [
+      {
+        action: message.type,
+        position: message.timestamp,
+        timestamp: Date.now(),
+      },
+    ],
+  });
+}
+
 // Listen for messages from popup or content scripts
 browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.command === 'navigate') {
+  console.log("MESSAGE", message);
+  switch (message.type) {
+    case "ended":
+      console.log("ENDED");
+    case "play":
+    case "playing":
+    case "pause":
+      updateHistory(message);
+      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 }));
+        .then(() => sendResponse({ status: "success" }))
+        .catch((error) =>
+          sendResponse({ status: "error", message: error.message }),
+        );
       return true; // Required for async sendResponse
     }
   }
 });
-
-// Listen for tab updates to potentially inject scripts on certain URLs
-browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
-  if (changeInfo.status === 'complete' && tab.url) {
-    // Check if the URL matches your patterns and inject scripts as needed
-    const urlPatterns = [
-      /example\.com/,
-      /another-site\.org/
-      // Add more URL patterns as needed
-    ];
-    
-    const shouldInject = urlPatterns.some(pattern => pattern.test(tab.url));
-    
-    if (shouldInject) {
-      browser.tabs.executeScript(tabId, { file: '/content_scripts/content.js' })
-        .catch(error => console.error('Error injecting script:', error));
-    }
-  }
-});

+ 55 - 33
content_scripts/content.js

@@ -1,41 +1,63 @@
-(function() {
-  console.log('Content script loaded');
-  
-  /**
-   * Check if the current URL matches the pattern
-   * @param {string} urlPattern - URL pattern to match
-   * @returns {boolean} - Whether the current URL matches the pattern
-   */
-  function matchesUrlPattern(urlPattern) {
-    const regex = new RegExp(urlPattern);
-    return regex.test(window.location.href);
+(function () {
+  console.log("HELLO FROM PLAYLIST!", Math.floor(Math.random() * 999));
+
+  function msgPlayEvt(type, target) {
+    const payload = {
+      type,
+      url: window.location.href,
+      timestamp: target.currentTime,
+      duration: target.duration,
+      title: document.title.replace(/ - YouTube$/, ""),
+    };
+    return browser.runtime.sendMessage(payload);
   }
 
-  /**
-   * Function to inject and execute custom code on the page
-   */
-  function injectCustomCode() {
-    console.log('Custom code injected into the page');
-    
-    // Example: Add a banner to the top of the page
-    const banner = document.createElement('div');
-    banner.textContent = 'This page has been modified by My Firefox Extension';
-    banner.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; background: #ffcc00; padding: 10px; text-align: center; z-index: 9999;';
-    document.body.prepend(banner);
-    
-    // You can add more functionality here
+  const vid = document.querySelector("video");
+
+  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) => {
-    if (message.command === 'injectCode') {
-      injectCustomCode();
-      return Promise.resolve({status: 'Injection completed'});
+    console.log("MESSAGE", message);
+    if (message.type === "print") {
+      console.log(message);
+      return Promise.resolve({ status: "Injection completed" });
     }
   });
-  
-  // Check if we should automatically inject code based on URL
-  if (matchesUrlPattern('example\\.com')) {
-    injectCustomCode();
-  }
-})();
+})();

+ 5 - 5
manifest.json

@@ -1,8 +1,8 @@
 {
   "manifest_version": 2,
-  "name": "My Firefox Extension",
+  "name": "My Playlist Extension",
   "version": "1.0",
-  "description": "A Firefox extension with code injection, navigation, and storage capabilities",
+  "description": "An extension to manage youtube videos",
   "icons": {
     "48": "icons/icon-48.png",
     "96": "icons/icon-96.png"
@@ -17,7 +17,7 @@
   ],
   "browser_action": {
     "default_icon": "icons/icon-48.png",
-    "default_title": "My Firefox Extension",
+    "default_title": "My Playlist Extension",
     "default_popup": "popup/popup.html"
   },
   "background": {
@@ -25,8 +25,8 @@
   },
   "content_scripts": [
     {
-      "matches": ["*://*.example.com/*"],
+      "matches": ["https://*.youtube.com/*"],
       "js": ["content_scripts/content.js"]
     }
   ]
-}
+}