initial commit
This commit is contained in:
commit
7f137b46f5
|
@ -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)).
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue