[READ-ONLY] Mirror of https://github.com/probablykasper/thumbnail-grabber. Chrome extension for grabbing thumbnails/covers
browser-extension chrome chrome-extension chrome-extention extension
0

Configure Feed

Select the types of activity you want to include in your feed.

Improve types

Kasper (Oct 15, 2022, 3:50 AM +0200) aa5de826 b12af4e2

+127 -118
+127 -118
src/content-script.ts
··· 1 1 var thumbnailGrabber = document.createElement('div'); 2 2 thumbnailGrabber.id = 'thumbnail-grabber'; 3 3 4 + const buttonText = { 5 + download: 'Download', 6 + copy: 'Copy', 7 + options: 'Options', 8 + }; 4 9 thumbnailGrabber.innerHTML = ` 5 10 <div id="thumbnail-grabber-card"> 6 11 <img tabindex="0"> 7 12 <div id="thumbnail-grabber-buttons"> 8 - <button tabindex="0">Download</button> 9 - <button tabindex="0">Copy</button> 10 - <button tabindex="0">Options</button> 13 + <button tabindex="0">${buttonText.download}</button> 14 + <button tabindex="0">${buttonText.copy}</button> 15 + <button tabindex="0">${buttonText.options}</button> 11 16 </div> 12 17 </div> 13 18 `; ··· 17 22 <p></p> 18 23 `; 19 24 20 - function notify(msg) { 25 + /** Returns the `msg` */ 26 + function notify(msg: string) { 21 27 var notificationElement = document.createElement('div'); 22 28 notificationElement.innerHTML = notificationInnerHTML; 23 29 notificationElement.classList.add('thumbnail-grabber-notification'); 24 30 const lastChild = notificationElement.querySelector('p:last-child'); 25 31 if (!(lastChild instanceof HTMLElement)) { 26 - throw alert('No p:last-child'); 32 + throw notify('No p:last-child'); 27 33 } 28 34 lastChild.textContent = msg; 29 35 30 36 const firstChild = notificationElement.querySelector('p:first-child'); 31 37 if (!(firstChild instanceof HTMLElement)) { 32 - throw alert('No p:first-child'); 38 + throw notify('No p:first-child'); 33 39 } 34 40 firstChild.addEventListener('click', function () { 35 41 notificationElement.style.animation = 'none'; ··· 48 54 var icon = chrome.runtime.getURL('icon48.png'); 49 55 const iconImgElement = notificationElement.querySelector('img'); 50 56 if (!(iconImgElement instanceof HTMLElement)) { 51 - throw alert('No iconImgElement'); 57 + throw notify('No iconImgElement'); 52 58 } 53 59 iconImgElement.src = icon; 54 60 ··· 58 64 notificationElement.parentElement.removeChild(notificationElement); 59 65 } 60 66 }, 5000); 67 + return msg; 61 68 } 62 69 63 - async function getYouTubeThumbnail(id) { 70 + async function getYouTubeThumbnail(id: string) { 64 71 const urls = [ 65 72 `https://img.youtube.com/vi/${id}/maxresdefault.jpg`, 66 73 `https://img.youtube.com/vi/${id}/hqdefault.jpg`, ··· 78 85 throw 'No thumbnail found'; 79 86 } 80 87 81 - function googleUserContentUrl(urlObj) { 88 + function googleUserContentUrl(urlObj: URL) { 82 89 // This is a `googleusercontent.com` url, which ends in something 83 90 // like `=w60-h60-l90-rj`. Not sure what `l90` or `rj` mean, but it seems 84 91 // to be the same without them. We replace all of this with `=s0` to get ··· 89 96 return urlObj.origin + imgPathname + urlObj.search + urlObj.hash; 90 97 } 91 98 92 - var img = thumbnailGrabber.querySelector('img'); 99 + let img = thumbnailGrabber.querySelector('img'); 100 + if (!img) { 101 + throw notify('Error: No img element'); 102 + } 93 103 94 - let lastImageUrl; 95 - async function getImageUrl(newUrl) { 96 - let imageUrl; 104 + let lastImageUrl: string; 105 + async function getImageUrl(url: string) { 106 + let imageUrl: string | undefined; 97 107 try { 98 - imageUrl = await getImageUrlCustom(newUrl); 108 + imageUrl = await getImageUrlCustom(url); 99 109 } catch (error) { 100 110 try { 101 - imageUrl = await getOembedImageUrl(newUrl); 111 + imageUrl = await getOembedImageUrl(url); 102 112 } catch (_oembedError) { 103 - notify(`Error getting thumbnail: ${error}`); 104 - console.error(`Error getting thumbnail: ${error}`); 105 - return; 113 + throw notify(`Error getting thumbnail: ${error}`); 106 114 } 107 115 } 108 116 if (!imageUrl) { 109 - imageUrl = await getOembedImageUrl(newUrl); 117 + imageUrl = await getOembedImageUrl(url); 110 118 } 111 119 if (!imageUrl) { 112 - throw 'Could not find any thumbnail'; 120 + throw notify('Could not find any thumbnail'); 113 121 } 114 122 lastImageUrl = imageUrl; 115 123 return imageUrl; 116 124 } 117 125 118 - function getFilename(site, imageUrl) { 126 + function getFilename(site: Site | null, imageUrl?: string) { 119 127 console.log(site, imageUrl); 120 - let filename = 'Thumbnail'; 121 - if ( 122 - site.soundcloud || 123 - site.youtubeMusic || 124 - site.youtubeMusicPlaylist || 125 - site.spotify 126 - ) { 127 - filename = 'Cover'; 128 + let filename = 'Cover'; 129 + if (site === 'youtube' || site === null) { 130 + filename = 'Thumbnail'; 128 131 } 132 + imageUrl = imageUrl || ''; 129 133 if (imageUrl.endsWith('.jpg')) { 130 - filename = `${filename}.jpg`; 134 + return `${filename}.jpg`; 131 135 } else if (imageUrl.endsWith('.png')) { 132 - filename = `${filename}.png`; 136 + return `${filename}.png`; 133 137 } else { 134 - filename = `${filename}.jpg`; 138 + return `${filename}.jpg`; 135 139 } 136 - 137 - return filename; 138 140 } 139 141 140 - function keydownEventListener(e) { 142 + function keydownEventListener(e: KeyboardEvent) { 141 143 if ( 142 144 e.key === 'Escape' && 143 145 e.metaKey === false && ··· 149 151 close(); 150 152 } 151 153 } 152 - async function copyEventListener(e) { 154 + async function copyEventListener(e: ClipboardEvent) { 153 155 e.preventDefault(); 154 156 try { 155 157 const result = await copy(lastImageUrl); 156 - if (result && result.as) { 157 - notify(`Copied as ${result.as}`); 158 - } 158 + notify(`Copied as ${result?.imageType}`); 159 159 close(); 160 160 } catch (error) { 161 - console.error('Error copying thumbnail', error); 162 - notify(`Error copying thumbnail: ${error}`); 161 + throw notify(`Error copying thumbnail: ${error}`); 163 162 } 164 163 } 165 164 async function open() { ··· 177 176 } 178 177 close(); 179 178 180 - let lastFilename; 181 - async function setup(newUrl) { 182 - const imageUrl = await getImageUrl(newUrl); 183 - const site = getSite(newUrl); 179 + let lastFilename: string; 180 + async function setup(url: string) { 181 + const imageUrl = await getImageUrl(url); 182 + const site = getSite(url); 184 183 lastFilename = getFilename(site, imageUrl); 185 184 if (!img) { 186 - throw alert('No img'); 185 + throw notify('No img'); 187 186 } 188 187 img.src = imageUrl; 189 188 return imageUrl; ··· 203 202 const imageUrl = await setup(msg.externalUrl || location.href); 204 203 open(imageUrl); 205 204 } catch (error) { 206 - notify(`Error opening thumbnail: ${error}`); 207 - console.error(`Error opening thumbnail: ${error}`); 205 + throw notify(`Error opening thumbnail: ${error}`); 208 206 } 209 207 } else if (msg.type === 'download') { 210 208 try { ··· 212 210 console.log('DOWNLOAD', imageUrl); 213 211 await download(imageUrl, lastFilename); 214 212 } catch (error) { 215 - notify(`Error downloading thumbnail: ${error}`); 216 - console.error(`Error downloading thumbnail: ${error}`); 213 + throw notify(`Error downloading thumbnail: ${error}`); 217 214 } 218 215 if (thumbnailGrabber.style.display !== 'none') { 219 216 close(); ··· 222 219 try { 223 220 const imageUrl = await setup(msg.externalUrl || location.href); 224 221 const result = await copy(imageUrl); 225 - if (result && result.as) { 226 - notify(`Copied as ${result.as}`); 227 - } else { 228 - notify('Copied'); 229 - } 222 + notify(`Copied as ${result?.imageType}`); 230 223 } catch (error) { 231 - console.error('Error copying thumbnail', error); 232 - notify(`Error copying thumbnail: ${error}`); 224 + throw notify(`Error copying thumbnail: ${error}`); 233 225 } 234 226 if (thumbnailGrabber.style.display !== 'none') { 235 227 close(); ··· 243 235 }); 244 236 thumbnailGrabber.addEventListener('click', async function (e) { 245 237 if (!(e.target instanceof HTMLElement)) { 246 - alert('No thumbnailGrabber click target'); 238 + throw notify('No thumbnailGrabber click target'); 247 239 } else if (e.target === this) { 248 240 // backdrop clicked 249 241 close(); 250 - } else if (e.target.textContent === 'DOWNLOAD') { 242 + } else if (e.target.textContent === buttonText.download) { 251 243 try { 252 244 await download(lastImageUrl, lastFilename); 253 245 } catch (err) { 254 - console.error(`Error downloading thumbnail: ${err}`); 255 - notify(`Error downloading thumbnail: ${err}`); 246 + throw notify(`Error downloading thumbnail: ${err}`); 256 247 } 257 248 close(); 258 - } else if (e.target.textContent === 'COPY') { 249 + } else if (e.target.textContent === buttonText.copy) { 259 250 try { 260 251 const result = await copy(lastImageUrl); 261 - if (result && result.as) { 262 - notify(`Copied as ${result.as}`); 263 - } else { 264 - notify('Copied'); 265 - } 252 + notify(`Copied as ${result?.imageType}`); 266 253 } catch (error) { 267 - console.error('Error copying thumbnail', error); 268 - notify(`Error copying thumbnail: ${error}`); 254 + throw notify(`Error copying thumbnail: ${error}`); 269 255 } 270 256 close(); 271 - } else if (e.target.textContent === 'OPTIONS') { 257 + } else if (e.target.textContent === buttonText.options) { 272 258 chrome.runtime.sendMessage({ type: 'options' }); 273 259 } 274 260 }); 275 261 276 262 document.body.appendChild(thumbnailGrabber); 277 263 278 - async function download(url, filename) { 264 + async function download(url: string, filename: string) { 279 265 const imageResponse = await fetch(url); 280 266 const imgBlob = await imageResponse.blob(); 281 267 const a = document.createElement('a'); ··· 296 282 return img; 297 283 } 298 284 299 - async function convertToPng(imgBlob): Promise<void> { 285 + async function convertToPng(imgBlob: Blob): Promise<void> { 300 286 return new Promise((resolve, reject) => { 301 287 const imageUrl = window.URL.createObjectURL(imgBlob); 302 288 const canvas = document.createElement('canvas'); ··· 315 301 canvas.toBlob( 316 302 async function (blob) { 317 303 try { 304 + if (!blob) { 305 + throw notify('Browser blob error'); 306 + } 318 307 await copyToClipboard(blob); 319 308 resolve(); 320 309 } catch (error) { ··· 334 323 }); 335 324 } 336 325 337 - async function copyToClipboard(pngBlob) { 326 + async function copyToClipboard(pngBlob: Blob) { 338 327 await navigator.clipboard.write([ 339 328 // eslint-disable-next-line no-undef 340 329 new ClipboardItem({ ··· 343 332 ]); 344 333 } 345 334 346 - async function firefoxCopy(url) { 335 + async function firefoxCopy(url: string) { 347 336 const response = await fetch(url); 348 337 const arrayBuffer = await response.arrayBuffer(); 349 338 const contentType = response.headers.get('Content-Type'); 350 - let imageType; 339 + let imageType: string; 351 340 if (contentType === 'image/jpeg' || contentType === 'image/jpg') { 352 341 imageType = 'jpeg'; 353 342 } else if (contentType === 'image/png') { ··· 367 356 if (!error?.success) { 368 357 throw new Error('setImageData not supported'); 369 358 } 359 + return { imageType: imageType.toUpperCase() }; 370 360 } 371 361 372 - async function copy(url) { 362 + async function copy(url: string) { 373 363 try { 374 - await firefoxCopy(url); 364 + const result = await firefoxCopy(url); 365 + return { imageType: result.imageType.toUpperCase() }; 375 366 } catch (_error) { 376 367 const imageResponse = await fetch(url); 377 368 const imgBlob = await imageResponse.blob(); 378 - if (url.endsWith('.png')) { 369 + let imageType = imgBlob.type.replace(/^image\//, ''); 370 + try { 379 371 await copyToClipboard(imgBlob); 380 - } else { 381 - await convertToPng(imgBlob); 382 - return { as: 'PNG' }; 372 + return { imageType: imageType.toUpperCase() }; 373 + } catch (_e) { 374 + convertToPng(imgBlob); 375 + return { imageType: 'PNG', converted: true }; 383 376 } 384 377 } 385 378 } 386 379 387 - function getSite(newUrl) { 380 + type Site = 381 + | 'soundcloud' 382 + | 'youtubeMusic' 383 + | 'youtubeMusicPlaylist' 384 + | 'youtube' 385 + | 'spotify'; 386 + 387 + function getSite(newUrl: string): Site | null { 388 388 const url = new URL(newUrl); 389 - return { 390 - soundcloud: url.hostname.endsWith('soundcloud.com'), 391 - youtubeMusic: 392 - url.hostname === 'music.youtube.com' && 393 - url.pathname === '/watch' && 394 - url.searchParams.has('v'), 395 - youtubeMusicPlaylist: 396 - url.hostname === 'music.youtube.com' && 397 - url.pathname === '/playlist' && 398 - url.searchParams.has('list'), 399 - youtube: 400 - url.hostname.endsWith('youtube.com') && 401 - !url.hostname.includes('music') && 402 - url.pathname === '/watch' && 403 - url.searchParams.has('v'), 404 - spotify: url.hostname === 'open.spotify.com', 405 - }; 389 + if (url.hostname.endsWith('soundcloud.com')) { 390 + return 'soundcloud'; 391 + } else if ( 392 + url.hostname === 'music.youtube.com' && 393 + url.pathname === '/watch' && 394 + url.searchParams.has('v') 395 + ) { 396 + return 'youtubeMusic'; 397 + } else if ( 398 + url.hostname === 'music.youtube.com' && 399 + url.pathname === '/playlist' && 400 + url.searchParams.has('list') 401 + ) { 402 + return 'youtubeMusicPlaylist'; 403 + } else if ( 404 + url.hostname.endsWith('youtube.com') && 405 + !url.hostname.includes('music') && 406 + url.pathname === '/watch' && 407 + url.searchParams.has('v') 408 + ) { 409 + return 'youtube'; 410 + } else if (url.hostname === 'open.spotify.com') { 411 + return 'spotify'; 412 + } else { 413 + return null; 414 + } 406 415 } 407 416 408 - async function getImageUrlCustom(newUrl) { 409 - const site = getSite(newUrl); 410 - if (site.soundcloud && newUrl === location.href) { 417 + async function getImageUrlCustom(url: string) { 418 + const site = getSite(url); 419 + if (site === 'soundcloud' && url === location.href) { 411 420 // would be easier to grab the <meta og:image> element, but that does 412 421 // not update when we navigate to new pages 413 422 var coverEl = document.querySelector('.interactive.sc-artwork > span'); 414 423 if (!(coverEl instanceof HTMLElement)) { 415 - throw alert('Artwork element not found'); 424 + throw notify('Artwork element not found'); 416 425 } 417 426 var bgImg = window.getComputedStyle(coverEl).backgroundImage; 418 427 var bgImgUrl = bgImg.slice(4, -1); ··· 420 429 bgImgUrl = bgImgUrl.slice(1, -1); 421 430 } 422 431 return bgImgUrl; 423 - } else if (site.youtubeMusic) { 424 - if (newUrl !== location.href) { 432 + } else if (site === 'youtubeMusic') { 433 + if (url !== location.href) { 425 434 throw 'For YouTube Music, you need to be at the URL'; 426 435 } 427 436 const coverImg = document.querySelector('.ytmusic-player-bar.image'); 428 437 if (!(coverImg instanceof HTMLImageElement)) { 429 - throw alert('Player bar image not found'); 438 + throw notify('Player bar image not found'); 430 439 } 431 440 const iurl = new URL(coverImg.src); 432 441 if (iurl.hostname === 'i.ytimg.com') { 433 442 if (!iurl.pathname.startsWith('/vi/')) { 434 443 throw "i.ytimg.com url doesn't start with /vi/"; 435 444 } 436 - const id = iurl.pathname.substr(4, 11); 445 + const id = iurl.pathname.substring(4, 4 + 11); 437 446 return await getYouTubeThumbnail(id); 438 447 } else { 439 448 return googleUserContentUrl(iurl); 440 449 } 441 - } else if (site.youtubeMusicPlaylist) { 442 - if (newUrl !== location.href) { 450 + } else if (site === 'youtubeMusicPlaylist') { 451 + if (url !== location.href) { 443 452 throw 'For YouTube Music, you need to be at the URL'; 444 453 } 445 454 const coverImg = document.querySelector('#img'); 446 455 if (!(coverImg instanceof HTMLImageElement)) { 447 - throw alert('Image element not found'); 456 + throw notify('Image element not found'); 448 457 } 449 458 const iurl = new URL(coverImg.src); 450 459 if (iurl.hostname === 'i.ytimg.com') { 451 460 if (iurl.pathname.startsWith('/vi/')) { 452 - const id = iurl.pathname.substr(4, 11); 461 + const id = iurl.pathname.substring(4, 4 + 11); 453 462 return await getYouTubeThumbnail(id); 454 463 } 455 464 } else { 456 465 return googleUserContentUrl(iurl); 457 466 } 458 - } else if (site.youtube) { 459 - const id = new URL(newUrl).searchParams.get('v'); 467 + } else if (site === 'youtube' && new URL(url).searchParams.has('v')) { 468 + const id = new URL(url).searchParams.get('v')!; 460 469 return await getYouTubeThumbnail(id); 461 - } else if (site.spotify && newUrl === location.href) { 470 + } else if (site === 'spotify' && url === location.href) { 462 471 const coverEl = 463 472 document.querySelector( 464 473 'img._5d10f53f6ab203d3259e148b9f1c2278-scss[srcset]', ··· 473 482 ) || 474 483 document.querySelector('.os-content img'); 475 484 if (!(coverEl instanceof HTMLImageElement)) { 476 - throw alert('Image element not found'); 485 + throw notify('Image element not found'); 477 486 } 478 487 if (coverEl && coverEl.srcset) { 479 488 // For /album/ urls. ··· 489 498 } 490 499 } 491 500 492 - async function getOembedImageUrl(newUrl) { 493 - const origin = new URL(newUrl).origin; 494 - const oembedUrl = `${origin}/oembed?format=json&url=${newUrl}`; 501 + async function getOembedImageUrl(url: string) { 502 + const origin = new URL(url).origin; 503 + const oembedUrl = `${origin}/oembed?format=json&url=${url}`; 495 504 const response = await fetch(oembedUrl); 496 505 const oembed = await response.json(); 497 506 if (!oembed) {