# `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](https://scrobble.observer). Release builds available [here](https://aleteoryx.me/downloads2/lfm_embed). # Usage `lfm_embed` serves one thing: Requests to `/user/` 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".](screenshots/plain-light.png) #### 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/` 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](https://www.last.fm/api/account/create) 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: ` header, used to provide live status updates. ## Logging ### `LFME_LOG_LEVEL` (Default: `"warn"`) The loglevel. This is actually parsed as an [`env_logger`](https://docs.rs/env_logger/latest/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 `` 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`](https://docs.rs/duration-str/latest/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](https://handlebarsjs.com/guide/#language-features) 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](#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`](https://docs.rs/handlebars/latest/handlebars) implementation in use, and may change.) ### `LFME_THEME_DEFAULT` (Default: `"plain"`) The theme to use when no query string is present. ## Example Configuration ```ini 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](https://handlebarsjs.com/guide/#language-features) 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: ```html {{#if error}}

{{error}}

{{else}} {{/if}} ``` 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: ```js { // 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](https://handlebarsjs.com/guide/expressions.html#helpers) are provided, on top of the handlebars [builtins](https://handlebarsjs.com/guide/builtin-helpers.html): - `(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](mailto:alyx@aleteoryx.me) if you'd like to help out, submit a custom theme, or request a feature. # Legal ``` 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 . ```