A Browser Extension to highlight your favorite music on r/ListenToThis

How to make a Browser Extension

mkdir music-highlight
cd music-highlight
{
"manifest_version": 2,
"name": "music-highlight",
"version": "1",
"author": "Your Name",
"homepage_url": "https://www.reddit.com/r/listentothis/",
"description": "Highlight your favorite music genres on r/ListenToThis",
"content_scripts": [{
"matches": ["*://*.reddit.com/r/listentothis*"],
"js": ["colors.js"]
}],
"permissions": [
"activeTab",
"https://www.reddit.com/r/listentothis/",
"https://old.reddit.com/r/listentothis/",
"storage"
]
}
  • for each post on the page
  • get the post’s title & url
  • if the title contains a genre on your list
  • change the background color
const getAllPosts = () => {
// old reddit
const allPosts = document.getElementsByClassName('thing');
if (allPosts.length === 0) {
// new reddit
return document.getElementsByClassName('scrollerItem');
}
return allPosts
}
const getTitle = (post) => {
// old reddit
const titleElem = post.querySelector('a.title');
// new reddit
if (!titleElem) {
return post.querySelector('h3');
}
return titleElem
}
const getGenresAsString = (titleElem) => {
const text = titleElem.innerText.toLowerCase()

// Extract the genres from the title
const genresRegex = /\[([^\]]+)\]/g
const match = genresRegex.exec(text)

// Skip over posts that are not properly formatted
if (!match) {
return null
}
return match[0]
}
const favoriteGenres = {
'ambient': '#fa8335',
'blues': '#0df9f9',
'country': '#fad337',
'chill': '#a8f830',
'funk': '#F2EF0C',
'jazz': '#fba19d',
'soul': '#aca2bb',
}
const getBGColor = (allGenresStr, favGenres) => {
let bgColor = null

// Test if the post contains any of our fav. genres
for (const genre of Object.keys(favGenres)) {
const genreRegex = new RegExp('.*' + genre + '.*', "i")
if (!genreRegex.test(allGenresStr)) {
continue
}
bgColor = 'background-color: ' + favGenres[genre] + ' !important'
}

return bgColor
}
const observeDOM = (() => {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const eventListenerSupported = window.addEventListener;

return (obj, callback) => {
if (MutationObserver) {
const obs = new MutationObserver((mutations, observer) => {
if (mutations[0].addedNodes.length)
callback(mutations[0].addedNodes);
});
obs.observe(obj, {
childList: true,
subtree: true
});
} else if (eventListenerSupported) {
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
};

})();

// detect if on the new Reddit before the content is loaded
const targetElem = document.getElementById('2x-container')
if (targetElem) {
observeDOM(targetElem, (addedNodes) => {
// whenever new content is added
addColorsOnSongs(favoriteGenres);
});
}
const favoriteGenres = {
'ambient': '#fa8335',
'blues': '#0df9f9',
'country': '#fad337',
'chill': '#a8f830',
'funk': '#F2EF0C',
'jazz': '#fba19d',
'soul': '#aca2bb',
}

const getAllPosts = () => {
// old reddit
const allPosts = document.getElementsByClassName('thing');
if (allPosts.length === 0) {
// new reddit
return document.getElementsByClassName('scrollerItem');
}
return allPosts
}

const getTitle = (post) => {
// old reddit
const titleElem = post.querySelector('a.title');
// new reddit
if (!titleElem) {
return post.querySelector('h3');
}
return titleElem
}

const getGenresAsString = (titleElem) => {
const text = titleElem.innerText.toLowerCase()

// Extract the genres from the title
const genresRegex = /\[([^\]]+)\]/g
const match = genresRegex.exec(text)

// Skip over posts that are not properly formatted
if (!match) {
return null
}
return match[0]
}


const getBGColor = (allGenresStr, favGenres) => {
let bgColor = null

// Test if the post contains any of our fav. genres
for (const genre of Object.keys(favGenres)) {
const genreRegex = new RegExp('.*' + genre + '.*', "i")
if (!genreRegex.test(allGenresStr)) {
continue
}
bgColor = 'background-color: ' + favGenres[genre] + ' !important'
}

return bgColor
}

const changePostColor = (post, bgColor) => {
post.setAttribute('style', bgColor);
for (let child of post.children) {
child.setAttribute('style', bgColor);
for (let child2 of child.children) {
child2.setAttribute('style', bgColor);
}
}
}

const addColorsOnSongs = (colorData) => {
const allPosts = getAllPosts();

for (const post of allPosts) {

const titleElem = getTitle(post)

if (!titleElem) continue

const genresStr = getGenresAsString(titleElem)
const bgColor = getBGColor(genresStr, favoriteGenres)

if (!bgColor) continue

// Change the post's & its children bg color
changePostColor(post, bgColor)

}

}

addColorsOnSongs(favoriteGenres)

const observeDOM = (() => {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const eventListenerSupported = window.addEventListener;

return (obj, callback) => {
if (MutationObserver) {
const obs = new MutationObserver((mutations, observer) => {
if (mutations[0].addedNodes.length)
callback(mutations[0].addedNodes);
});
obs.observe(obj, {
childList: true,
subtree: true
});
} else if (eventListenerSupported) {
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
};

})();

// detect if on the new Reddit before the content is loaded
const targetElem = document.getElementById('2x-container')
if (targetElem) {
observeDOM(targetElem, (addedNodes) => {
addColorsOnSongs(favoriteGenres);
});
}
  • Navigate to chrome://extensions
  • Toggle Developer Mode (top right)
  • click on Load Unpacked
  • Select the extension’s folder music-highlight
  • click Open
  • Navigate to about:debugging
  • click on This Firefox (top left)
  • click on Load Temporary Add-on
  • go to the extension’s folder
  • select manifest.json
  • click Open

Solving an UX Problem

  • create a hyperlink HTML Element
  • with the href pointing to the source
  • and all the matched genres as the inner text
  • then append it to the reddit’s post
const getSongURL = (titleElem, post) => {
// old reddit
let href = titleElem.href
// new reddit
if (!href) {
const extLink = post.querySelector('a.styled-outbound-link')
if (extLink) {
return extLink.href
}
}
return href
}
const createSongLink = (titleElem, post, genresText) => {
post.style.position = 'relative'
let linkElem = document.createElement('a')
linkElem.className = "favGenresLink"
linkElem.style.position = 'absolute'
linkElem.style.right = '20px'
linkElem.style.bottom = '0'
linkElem.style.height = '50px'
linkElem.style.color = 'black'
linkElem.style.fontSize = '50px'
linkElem.style.zIndex = '999999'
linkElem.innerText = genresText
linkElem.href = getSongURL(titleElem, post)
return linkElem
}
const getBGColor = (allGenresStr, favGenres) => {
let bgColor = null
let favGenresStr = ''

// Test if the post contains any of our fav. genres
for (const genre of Object.keys(favGenres)) {
const genreRegex = new RegExp('.*' + genre + '.*', "i")
if (!genreRegex.test(allGenresStr)) {
continue
}
bgColor = 'background-color: ' + favGenres[genre] + ' !important'
favGenresStr += genre + ' '
}

return {bgColor, favGenresStr}
}
const addColorsOnSongs = (colorData) => {
const allPosts = getAllPosts();

for (const post of allPosts) {

// Ingnore this post if it already
// contains a favGenresLink
let colorObj = post.querySelector('a.favGenresLink');
if (colorObj) continue

const titleElem = getTitle(post)

if (!titleElem) continue

const genresStr = getGenresAsString(titleElem)
const {bgColor, favGenresStr} = getBGColor(genresStr, favoriteGenres)

if (!bgColor) continue

// Change the post's & its children bg color
changePostColor(post, bgColor)

// Create the genres link and add it to the post
const linkElem = createSongLink(titleElem, post, favGenresStr)
post.insertAdjacentElement('afterbegin', linkElem)
}

}
if (targetElem) {
observeDOM(targetElem, (addedNodes) => {
// ignore favGenresLink to avoid an infinite loop
for (let addedNode of addedNodes) {
if (addedNode.classList.contains('favGenresLink')) {
return
}
}

// whenever new content is added
addColorsOnSongs(favoriteGenres);
});
}
const favoriteGenres = {
'ambient': '#fa8335',
'blues': '#0df9f9',
'country': '#fad337',
'chill': '#a8f830',
'funk': '#F2EF0C',
'jazz': '#fba19d',
'soul': '#aca2bb',
}

const getAllPosts = () => {
// old reddit
const allPosts = document.getElementsByClassName('thing');
if (allPosts.length === 0) {
// new reddit
return document.getElementsByClassName('scrollerItem');
}
return allPosts
}

const getTitle = (post) => {
// old reddit
const titleElem = post.querySelector('a.title');
// new reddit
if (!titleElem) {
return post.querySelector('h3');
}
return titleElem
}

const getGenresAsString = (titleElem) => {
const text = titleElem.innerText.toLowerCase()

// Extract the genres from the title
const genresRegex = /\[([^\]]+)\]/g
const match = genresRegex.exec(text)

// Skip over posts that are not properly formatted
if (!match) {
return null
}
return match[0]
}


const getBGColor = (allGenresStr, favGenres) => {
let bgColor = null
let favGenresStr = ''

// Test if the post contains any of our fav. genres
for (const genre of Object.keys(favGenres)) {
const genreRegex = new RegExp('.*' + genre + '.*', "i")
if (!genreRegex.test(allGenresStr)) {
continue
}
bgColor = 'background-color: ' + favGenres[genre] + ' !important'
favGenresStr += genre + ' '
}

return {bgColor, favGenresStr}
}

const changePostColor = (post, bgColor) => {
post.setAttribute('style', bgColor);
for (let child of post.children) {
child.setAttribute('style', bgColor);
for (let child2 of child.children) {
child2.setAttribute('style', bgColor);
}
}
}

const getSongURL = (titleElem, post) => {
// old reddit
let href = titleElem.href
// new reddit
if (!href) {
const extLink = post.querySelector('a.styled-outbound-link')
if (extLink) {
return extLink.href
}
}
return href
}

const createSongLink = (titleElem, post, genresText) => {
post.style.position = 'relative'
let linkElem = document.createElement('a')
linkElem.className = "favGenresLink"
linkElem.style.position = 'absolute'
linkElem.style.right = '20px'
linkElem.style.bottom = '0'
linkElem.style.height = '50px'
linkElem.style.color = 'black'
linkElem.style.fontSize = '50px'
linkElem.style.zIndex = '999999'
linkElem.innerText = genresText
linkElem.href = getSongURL(titleElem, post)
return linkElem
}

const addColorsOnSongs = (colorData) => {
const allPosts = getAllPosts();

for (const post of allPosts) {

// ignore
let colorObj = post.querySelector('a.favGenresLink');
if (colorObj) continue //TODO

const titleElem = getTitle(post)

if (!titleElem) continue

const genresStr = getGenresAsString(titleElem)
const {bgColor, favGenresStr} = getBGColor(genresStr, favoriteGenres)

if (!bgColor) continue

// Change the post's & its children bg color
changePostColor(post, bgColor)

// Create the genres link and add it to the post
const linkElem = createSongLink(titleElem, post, favGenresStr)
post.insertAdjacentElement('afterbegin', linkElem)
}

}

addColorsOnSongs(favoriteGenres)


const observeDOM = (() => {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const eventListenerSupported = window.addEventListener;

return (obj, callback) => {
if (MutationObserver) {
const obs = new MutationObserver((mutations, observer) => {
if (mutations[0].addedNodes.length)
callback(mutations[0].addedNodes);
});
obs.observe(obj, {
childList: true,
subtree: true
});
} else if (eventListenerSupported) {
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
};

})();

// detect if on the new Reddit before the content is loaded
const targetElem = document.getElementById('2x-container')
if (targetElem) {
observeDOM(targetElem, (addedNodes) => {
// ignore favGenresLink to avoid an infinite loop
for (let addedNode of addedNodes) {
if (addedNode.classList.contains('favGenresLink')) {
return
}
}

// whenever new content is added
addColorsOnSongs(favoriteGenres);
});
}
...
"options_page": "options.html",
"options_ui": {
"page": "options.html"
}
...
<html>
<head></head>
<body>
<h1>Music Highlight Options</h1>

<div id="root">
<div id="container">
</div>
<button id="add" class="button" type="button" name="button">Add genre</button>
<hr />
<div id="buttons">
<button id="save" class="button" type="button" name="button">Save</button>
<div id="status"></div>
</div>
</div>

<script src="options.js"></script>
</body>
</html>
const defaultGenres = {
'ambient': '#fa8335',
'blues': '#0df9f9',
'country': '#fad337',
'chill': '#a8f830',
'funk': '#F2EF0C',
'jazz': '#fba19d',
'soul': '#aca2bb',
}

const restoreOptions = () => {
chrome.storage.local.get('colors', (data) => {
if (!data
|| Object.keys(data).length === 0
|| Object.keys(data.colors).length === 0) {
createColorsUI(defaultGenres);
} else {
createColorsUI(data.colors);
}
});
}

document.addEventListener('DOMContentLoaded', restoreOptions);
const createColorInput = (genre, color, id) => {
let genreInputLabel = document.createElement('span')
genreInputLabel.innerText = 'Genre:'
genreInputLabel.className = 'genreNameLabel'
let genreInput = document.createElement('input')
genreInput.className = 'genreName'
genreInput.type = 'text'
genreInput.value = genre
let colorInputLabel = document.createElement('span')
colorInputLabel.innerText = 'Color:'
colorInputLabel.className = 'colorNameLabel'
let colorInput = document.createElement('input')
colorInput.className = 'colorName'
colorInput.type = 'color'
colorInput.value = color
let removeButton = document.createElement('button')
removeButton.innerText = 'Remove'
removeButton.className = 'removeButton button'
removeButton.addEventListener('click', ((e) => {
let tmpElem = document.getElementById(e.target.parentElement.id)
if (tmpElem && tmpElem.parentElement) {
tmpElem.parentElement.removeChild(tmpElem)
}
}))

let group = document.createElement('div')
group.id = 'data' + id
group.className = 'genreColorGroup'
group.appendChild(genreInputLabel)
group.appendChild(genreInput)
group.appendChild(colorInputLabel)
group.appendChild(colorInput)
group.appendChild(removeButton)

let container = document.getElementById('container')
container.appendChild(group)
}
const createColorsUI = (data) => {
let index = 0
for (let variable in data) {
if (data.hasOwnProperty(variable)) {
createColorInput(variable, data[variable], index)
index++
}
}
}
const addOption = () => {
let index = Math.floor(Math.random() * 1000000)
createColorInput('misc', '#000000', index)
}

document.getElementById('add').addEventListener('click', addOption);
const saveOptions = () => {
let allGenreNames = document.getElementsByClassName('genreName')
let allColorNames = document.getElementsByClassName('colorName')

let data = {}

for (let i = 0; i < allGenreNames.length; i++) {
let name = allGenreNames[i].value
let color = allColorNames[i].value
data[name] = color
}

chrome.storage.local.set({
colors: data
}, () => {
let status = document.getElementById('status');
status.textContent = 'Options saved.';
setTimeout(() => {
status.textContent = '';
}, 2750);
});
}

document.getElementById('save').addEventListener('click', saveOptions);
  • click on the Extesions button, on the toolbar
  • click on music-highligt’s More Actions menu
  • click on Options
  • click on the extension’s icon, on the toolbar
  • click on Manage Extension
  • go to the Preferences menu

Last part

addColorsOnSongs(favoriteGenres)
chrome.storage.local.get('colors', (data) => {
if (!data || Object.keys(data).length === 0 || Object.keys(data.colors).length === 0) {
addColorsOnSongs(favoriteGenres);
} else {
console.log(data)
addColorsOnSongs(data.colors);
}
});
// detect if on the new Reddit before the content is loaded
const targetElem = document.getElementById('2x-container')
if (targetElem) {
observeDOM(targetElem, (addedNodes) => {
// ignore favGenresLink to avoid an infinite loop
for (let addedNode of addedNodes) {
if (addedNode.classList.contains('favGenresLink')) {
return
}
}

// whenever new content is added
//addColorsOnSongs(favoriteGenres);

chrome.storage.local.get('colors', (data) => {
if (!data || Object.keys(data).length === 0 || Object.keys(data.colors).length === 0) {
addColorsOnSongs(favoriteGenres);
} else {
console.log(data)
addColorsOnSongs(data.colors);
}
});
});
}

Create a browser action & icon

"icons": {
"48": "icons/logo.png"
},
"browser_action": {
"default_icon": {
"48": "icons/logo.png"
},
"default_title": "Music-Highlight",
"browser_style": true,
"default_popup": "action.html"
}
<html>
<body>
<button id="openOptions">Options...</button>
<script src="action.js" charset="utf-8"></script>
</body>
</html>
document.getElementById('openOptions').addEventListener('click', (e) => {
let optionsUrl = chrome.extension.getURL("./options.html");
chrome.tabs.create({
url: optionsUrl
});
})

--

--

--

https://alexadam.dev/

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

9 Hidden Features of Chrome DevTools

Avoid Terrible Mistakes when Deploying Websites with Git and Node

[Tech Blog] Building shared-components library with Radix UI

LEGO storm-trooper next to easel

Composing Micro Frontends — hosting in Azure

Ionic React: One App that runs everywhere

NodeJS API-Part 1 / Starting our server using express

React Native Project with NodeJS and MongoDB- Part 1

Create a twitch clone using React -4

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
alex adam

alex adam

https://alexadam.dev/

More from Medium

Routing security — Making internet a safer and cleaner place to live, work and play.

Digging Deep into Gutenberg WordPress Block Editor

Legacy of the Treasure Hunter

A very beginners guide to Malware analysis/Reverse Engineering P.1