~aleteoryx/lfm_embed

ref: e5b8bfaba2360a9a701f6db0830798a09e13bee0 lfm_embed/README.md -rw-r--r-- 10.0 KiB
e5b8bfabalyx Remove redundant HTML escaping helpers. 10 months ago

#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 scrobble.observer.

Release builds available here.

#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.

  • (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.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.