aboutsummaryrefslogtreecommitdiffstats

lfm_embed

A simple webserver for rendering a last.fm embed.

More specifically, this displays a simple webpage with info about your current or most recent last.fm scrobble. Its intended use is to be put in an iframe on a personal site or whatever. While it is self-hostable, there is an official public instance at https://lfm.aleteoryx.me.

Usage

lfm_embed serves one thing: Requests to /user/<last.fm username> will generate a page displaying your current or last last.fm scrobble. The look and feel of this is down to the configured theme for your request.

As the user, you may select a theme supported by the server by passing ?theme= query string. Check with your host for a list of supported themes.

Individual themes may also support parameters, via additional query parameters.

Builtin Themes

plain

A minimal theme, displaying just the username, album art, and track info. It is the default fallback for an unset or unknown theme.

A screenshot of the plain theme, displaying album art, and the text "@vvinrg is scrobbling Sweet Tooth from Sleepyhead by Cavetown".

Parameters

  • ?dark: toggle the theme's dark mode.

Self-Hosting

lfm_embed is, as it stands, designed to use a reverse proxy. A future release may include HTTPS support, but currently, it will only listen on localhost, prohibiting it from public access. Once configured, a request of the form http://localhost:9999/user/<last.fm username> will render out an embed with the default theme.

As it stands, there are no plans to support displaying users with private listen history.

Configuration

Configuration should be done via the environment. lfm_embed also supports reading from a standard .env file. The following are the environment variables which lfm_embed understands.

Core Config

LMFE_API_KEY (Required)

Your last.fm API key. You'll need to create one here for self-hosting.

LFME_PORT (Default: 9999)

The port to serve on locally.

LFME_NO_REFRESH (Default: 0)

If set to 1, disable outputting of the HTTP Refresh: <cache refresh> header, used to provide live status updates.

Logging

LFME_LOG_LEVEL (Default: "warn")

The loglevel. This is actually parsed as an env_logger filter string. Read the docs for more info.

LFME_LOG_FILE

If set, logs will be written to the specified file. Otherwise, logs are written to stderr.

User Perms

LFME_WHITELIST_MODE (Default: "open")

The following(case-insensitive) values are supported:

  • open: Allow requests for all users.

  • exclusive: Only allow requests for users in LFME_WHITELIST, returning HTTP 403 for all others. LFME_WHITELIST must be set if this mode is enabled.

If the user requested has their listen history private, a 403 will be returned.

LFME_WHITELIST (Default: "")

This is expected to be a sequence of comma-separated usernames. Leading/trailing whitespace will be stripped, and unicode is fully supported.

LFME_WHITELIST_REFRESH (Default: "1m")

The amount of time to cache whitelisted user info for. It is interpreted as a sequence of <num><suffix> values, where num is a positive integer, and suffix is one of y,mon,w,d,h,m,s, ms, µs, or ns, each of which corresponds to a self-explanatory unit of time. For most practical applications, one should only use m and s, as caching your current listen for more than that time has the potential to omit most songs. Parsing is delegated to the duration_str crate, and further info may be found there.

LFME_DEFAULT_REFRESH (Default: "5m")

The amount of time to cache non-whitelisted user info for. See LFME_WHITELIST_REFRESH for more info.

Themes

LFME_THEME_DIR

If set, must be a valid path to a directory containing Handlebars files, ending in LFME_THEME_EXT. They will be registered as themes on top of the builtin ones, with each theme's name being their filename minus the extension. Same-named themes will override builtin ones.

See Theming for details on writing custom themes.

Theme names are the same as their path, minus the extension. Given an extension of .hbs, a directory like:

themes/
  mytheme.hbs
  myothertheme.hbs
  myunrelatedfile.css
  alices-themes/
    mytheme.hbs
    mysuperawesometheme.hbs

results in the following themes:

mytheme
myothertheme
alices-themes/mytheme
alices-themes/mysuperawesometheme

By default, these are loaded and compiled once, at startup.

LFME_THEME_EXT (Default: .hbs)

The file extension for themes in LFME_THEME_DIR.

Note: This behaves more like a suffix than a file extension. You must include the leading dot for a file extension.

LFME_THEME_DEV (Default: 0)

If set to 1, existing themes will be reloaded on edit.

Note: Even with this mode, adding a new theme requires a full reload. Themes are only enumerated once, at startup. (This is a limitation of the handlebars implementation in use, and may change.)

LFME_THEME_DEFAULT (Default: "plain")

The theme to use when no query string is present.

Example Configuration

LFME_API_KEY=0123456789abcdef0123456789abcdef
LFME_LOG_LEVEL=error
LFME_PORT=3000

LFME_WHITELIST_REFRESH=30s
LFME_WHITELIST_MODE=exclusive
LFME_WHITELIST=a_precious_basket_case, realRiversCuomo, Pixiesfan12345

Theming {#theming}

Custom themes are, as stated above, Handlebars files. They are expected to produce a complete HTML doc, which should contain info about a user's scrobbles.

See src/themes for examples of implementation.

Writing a Theme

Themes should have, roughly, the structure below:

<!DOCTYPE html>
<html lang="en"> <!-- Or really whatever you want, but I speak English. -->
    <head>
        <meta charset="UTF-8">
        <style> /* styles */ </style>
    </head>
    <body>
        {{#if error}}<p>{{error}}</p>{{else}}
            <!-- theme contents here -->
        {{/if}}
    </body>
</html>

You will be passed one of 2 context objects, depending on if there was an error processing the request. If there was an error, the object will just be { error: String }, otherwise it will be the following:

{
    // The user the request was made on the behalf of.
    user: {
        // Their username.
        name: String,
        // Their "Display Name".
        realname: String,
        // Link to user's profile picture.
        image_url: String,
        // Link to user's profile.
        url: String,

        // Total scrobbles.
        scrobble_count: Number,
        // Number of artists in library.
        artist_count: Number,
        // Number of tracks in library.
        track_count: Number,
        // Number of albums in library.
        album_count: Number,

        // True if user subscribes to last.fm pro.
        pro_subscriber: Boolean
    },

    // The user's most current, or most scrobble.
    scrobble: {
        // The name of the track.
        name: String,
        // The name of its album.
        album: String,
        // The artist who made it.
        artist: {
            // Their name.
            name: String,
            // Link to their profile image.
            image_url: String,
            // Link to the artist's last.fm page.
            url: String
        },
        // A link to the track image.
        image_url: String,
        // A link to the track's last.fm page.
        url: String,

        // True if the user has loved the track.
        loved: Boolean
        // True if the user is currently scrobbling it, false if it's just
        // the most recently played track.
        now_playing: Boolean,
    },

    // A set of extraneous query parameters.
    //
    // This should be considered UNTRUSTED, as the requester has full
    // control over it. Use the provided `html_escape`,
    // `html_attr_escape`, and `uri_encode` helpers when inlining
    // any contained text.
    query: Object
}

In addition, the following helpers are provided, on top of the handlebars builtins:

  • (eq Object Object) => Boolean: Check equality between its args.

  • (ne Object Object) => Boolean: Check inequality between its args.

  • (gt Number|String Number|String) => Boolean: Check if the first arg is greater than the second.

  • (gte Number|String Number|String) => Boolean: Check if the first arg is greater than or equal to the second.

  • (lt Number|String Number|String) => Boolean: Check if the first arg is less than the second.

  • (lte Number|String Number|String) => Boolean: Check if the first arg is less than or equal to the second.

  • (and Boolean Boolean) => Boolean: Boolean AND gate.

  • (or Boolean Boolean) => Boolean: Boolean OR gate.

  • (not Boolean) => Boolean: Boolean NOT gate.

  • (html_escape String) => String: Escape HTML special characters from the input string, replacing them with HTML entities in the output. For use in standard markdown.

  • (html_attr_escape String) => String: Escape HTML special characters, as well as quotation marks, in the input, replacing them with HTML entities and escaped quotes in the output. For use in HTML tag attributes.

  • (uri_encode String) => String: URI-encode input text, making the output suitable to be included as part of a link or other URL.

Contributing

E-Mail me if you'd like to help out, submit a custom theme, or request a feature.