initial commit

This commit is contained in:
gome 2023-10-21 12:26:35 -05:00
commit 7f137b46f5
2 changed files with 174 additions and 0 deletions

57
README.md Normal file
View File

@ -0,0 +1,57 @@
# Last.fm album art fetcher
You can include album art on your site
without having to look up URLs or host it yourself!
This script uses the Last.fm API
to pull down album art and insert it into a static site.
## Usage
- In the `<head>` of your HTML, add a `<script>` tag with:
- a `src` attribute with the path to the `album-art.js` file,
- a `defer` attribute present, and
- a `data-api-key` attribute with your [Last.fm API](https://www.last.fm/api/account/create) key.
- In the `<body>`, use `<img>` tags wherever you want album art, with:
- a `data-album` attribute containing the name of the album you want,
- a `data-artist` attribute containing the name of the artist, and
- a `width` and `height` defined, preferrably square.
With this info supplied, `album-art.js` will
use the album and artist supplied on each `<img>`
to search for the albums on Last.fm.
If it finds the album,
it will insert the album art for it into the page,
and you're good to go.
Usage looks something like this:
```html
<head>
...
<script defer src='example/path/to/album-art.js' data-api-key='YOUR-API-KEY'></script>
...
</head>
<body>
...
<img data-album='Heaven or Las Vegas' data-artist='Cocteau Twins' width='64' height='64' />
...
</body>
```
## Setup
1. First, you need to have a normal [Last.fm account](https://www.last.fm/join).
2. Once logged in, create a [Last.fm API account](https://www.last.fm/api/account/create) to get an API key.
- The API call for `album-art.js` doesn't require any special authentication beyond the API key, so you don't need to fill in the "Callback URL" or "Application homepage" fields.
3. Download a copy of `album-art.js` and put it somewhere on your site.
4. Follow the **Usage** instructions above to include the script on any page on your site you want to include album art on.
## License
If you want to credit me on your site,
that would be awesome.
You can credit me as [gome](https://ctrl-c.club/~gome/).
If you modify (or just enjoy!) this software, [please let me know](https://ctrl-c.club/~gome/contact.html), and share any useful improvements ([pull requests are a welcome way to do so](https://tildegit.org/gome/last-fm-album-art/compare/main...main)).

117
album-art.js Normal file
View File

@ -0,0 +1,117 @@
{
// API key is provided to the script via the `data-api-key` attribute on the <script> element
const api_key = document.currentScript.dataset.apiKey;
if (!api_key) {
throw new Error('album-art: provide an API key to the <script> tag with the "data-api-key" attribute');
}
const API_URL = 'https://ws.audioscrobbler.com/2.0/';
// utility function to make a key for use in localStorage, unique to a given album/artist query
function make_key(artist, album) {
return `${album}\n${artist}`;
}
// obtain the lastfm entry for the given album, either via fetch
// or from cached entry in localStorage
async function get_lastfm_entry(artist, album) {
const local_storage_key = make_key(artist, album);
let entry = null;
try {
// get the JSON from localStorage and parse it into an object
const json = window.localStorage.getItem(local_storage_key);
entry = JSON.parse(json);
} catch {
// pass (if parse fails, we just refetch)
}
if (entry) {
// successfully pulled entry from localStorage
return entry;
} else {
// query parameters for our request to last.fm API
const search = new URLSearchParams({
method: 'album.getinfo',
format: 'json',
api_key,
artist,
album,
}).toString();
// call last.fm API with these query parameters
return fetch(`${API_URL}?${search}`)
.then(response => response.json())
.then(lastfm => {
if (lastfm.error) {
throw lastfm.error;
} else {
// pull the specific property we care about from the response object (listing of image URLs for the album)
const entry = lastfm.album.image;
// cache it in localStorage for next time this album/artist pair gets called
window.localStorage.setItem(local_storage_key, JSON.stringify(entry));
return entry;
}
});
}
}
// fetch album art according to artist/album query and put it into the given <img> element
async function fetch_album_art(artist, album, img, is_retry) {
try {
// get the array of image URLs from last.fm or cache
const arts = await get_lastfm_entry(artist, album);
// choose a size appropriate to the <img> element
// fallthrough case is 'mega'
let target_size = 'mega';
let img_size = Math.max(img.width, img.height);
// we want the image to be slightly higher-resolution than the screen size,
// to look OK on high-density displays,
// so we set our thresholds at 0.75 times the respective image size
if (img_size <= 25) { // 34 * 0.75
target_size = 'small';
} else if (img_size <= 48) { // 64 * 0.75
target_size = 'medium';
} else if (img_size <= 130) { // 174 * 0.75
target_size = 'large';
} else if (img_size <= 225) { // 300 * 0.75
target_size = 'extralarge';
} // otherwise 'mega'
// find the corresponding image, or just use the last one if that fails
const art = arts.find(({size}) => size === target_size) ?? arts[arts.length - 1];
const url = art['#text'];
if (url) {
// set URL on <img> element
img.src = url;
// if this image fails to load, the cached last.fm entry is consdered no longer valid
img.addEventListener('error', () => {
// get rid of the bad cache entry
window.localStorage.removeItem(make_key(artist, album));
// we'll retry fetching it one time
if (!is_retry) {
// call this function again, with the retry flag set so we don't recurse forever
fetch_album_art(album, artist, img, true);
} else {
// if we've already retried, set the error on the <img> element
img.dataset.error = 'Failed to load image source after retry';
}
});
} else {
throw new Error('Image unavailable');
}
} catch (error) {
if (error.message === 'Image unavailable') {
// this is our error message, so we want to keep it as-is
throw error;
}
throw new Error('Image fetch failed: ' + error.message);
}
}
// the actual entrypoint of the script
// iterate through all <img> elements with the needed data attributes defined
for (const img of document.querySelectorAll('img[data-album][data-artist]')) {
// run the function to populate this <img> element
// upon error, put the error message in the "data-error" attribute on the <img> element
fetch_album_art(img.dataset.artist, img.dataset.album, img)
.catch(e => img.dataset.error = e.message);
}
}