|
| 1 | +# Integrating a music library: an example with Beets |
| 2 | + |
| 3 | +Liquidsoap's native sources can read from files and folders, |
| 4 | +but if your radio uses an important music library |
| 5 | +(more than a thousand tracks) |
| 6 | +sorting by folders may not be enough. |
| 7 | +You will also need to adjust the playout gain per track (ReplayGain). |
| 8 | +In that case you would better have a music library |
| 9 | +queried by Liquidsoap. |
| 10 | +In this section we'll do this with [Beets](http://beets.io/). |
| 11 | +Beets holds your music catalog, |
| 12 | +cleans tracks' tags before importing, |
| 13 | +can compute each track's ReplayGain, |
| 14 | +and most importantly has a command-line interface we can leverage from Liquidsoap. |
| 15 | +The following examples may also inspire you to integrate another library or your own scripts. |
| 16 | + |
| 17 | +After installing Beets, |
| 18 | +enable the `random` plug-in |
| 19 | +(see [Beets documentation on plug-ins](https://beets.readthedocs.io/en/stable/plugins/index.html#using-plugins)). |
| 20 | +To enable gain normalization, install and configure the |
| 21 | +[`replaygain`](https://beets.readthedocs.io/en/stable/plugins/replaygain.html) plug-in. |
| 22 | +To easily add single tracks to you library, |
| 23 | +you might also be interested in the |
| 24 | +[drop2beets](https://github.com/martinkirch/drop2beets#drop2beets) plug-in. |
| 25 | +The following examples suppose you defined a `BEET` constant, |
| 26 | +which contains the complete path to your `beet` executable (on UNIX systems, find it with `which beet`). For example: |
| 27 | + |
| 28 | +``` |
| 29 | +BEET = "/home/radio/.local/bin/beet" |
| 30 | +``` |
| 31 | + |
| 32 | +Before creating a Liquidsoap source, |
| 33 | +let's see why Beets queries are interesting for a radio. |
| 34 | + |
| 35 | +## Beets queries |
| 36 | + |
| 37 | +Queries are parameters that you usually provide to the `beet ls` command : |
| 38 | +Beets will find matching tracks. |
| 39 | +The `random` plug-in works the same, except that it returns only one track matching the query |
| 40 | +(see [the plug-in's documentation](https://beets.readthedocs.io/en/stable/plugins/random.html)). |
| 41 | +Once your library is imported, |
| 42 | +you can try the following queries on the command line |
| 43 | +by typing `beet ls [query]` or `beet random [query]`. |
| 44 | +To test quickly, add the `-t 60` option to `beet random` |
| 45 | +so it will select an hour worth of tracks matching your query. |
| 46 | + |
| 47 | +Without selectors, queries search in a track’s title, artist, album name, |
| 48 | +album artist, genre and comments. Typing an artist name or a complete title |
| 49 | +usually match the exact track, and you could do a lovely playlist just by querying `love`. |
| 50 | + |
| 51 | +But in a radio you'll usually query on other fields. |
| 52 | +You can select tracks by genre with the `genre:` selector. |
| 53 | +Be careful that `genre:Rock` also matches `Indie Rock`, `Punk Rock`, etc. |
| 54 | +To select songs having english lyrics, use `language:eng`. |
| 55 | +Or pick 80s songs with `year:1980..1990`. |
| 56 | + |
| 57 | +Beets also holds internal meta-data, like `added`: |
| 58 | +the date and time when you imported each song. |
| 59 | +You can use it to query tracks inserted over the past month with `added:-1m..`. |
| 60 | +Or you can query track imported more than a year ago with `added:..-1y`. |
| 61 | +Beets also lets you |
| 62 | +[set your own tags](https://beets.readthedocs.io/en/stable/guides/advanced.html#store-any-data-you-like). |
| 63 | + |
| 64 | +You can use the `info` plug-in to see everything Beets knows about title(s) matching a query |
| 65 | +by typing `beet info -l [query]`. |
| 66 | +See also [the Beets' documentation](https://beets.readthedocs.io/en/stable/reference/query.html) |
| 67 | +for more details on queries operators. |
| 68 | +All these options should allow you to create both general and specialized Liquidsoap sources. |
| 69 | + |
| 70 | +## A source querying each next track from Beets |
| 71 | + |
| 72 | +As of Liquidsoap 2.x we can create a function that creates a dynamic source, |
| 73 | +given its `id` and a Beet query. |
| 74 | +We rely on `request.dynamic` to call `beet random` |
| 75 | +(with `-f '$path'` option so beets only returns the matching track's path) |
| 76 | +every time the source must prepare a new track: |
| 77 | + |
| 78 | +```{.liquidsoap include="beets-source.liq" from="BEGIN" to="END"} |
| 79 | +
|
| 80 | +``` |
| 81 | + |
| 82 | +Note that |
| 83 | + |
| 84 | +- `query` can be empty, it will match all tracks in the library. |
| 85 | +- we set `retry_delay` to a second, to avoid looping on `beet` calls if something goes wrong. |
| 86 | +- The final type hint (`:source`) will avoid false typing errors when the source is integrated in complex operators. |
| 87 | + |
| 88 | +## Applying ReplayGain |
| 89 | + |
| 90 | +When the [`replaygain` plug-in](https://beets.readthedocs.io/en/stable/plugins/replaygain.html) |
| 91 | +is enabled, all tracks will have an additional metadata field called `replaygain_track_gain`. |
| 92 | +Check that Beet is configured to |
| 93 | +[write ID3 tags](https://beets.readthedocs.io/en/stable/reference/config.html#importer-options) |
| 94 | +so Liquidsoap will be able to read this metadata - |
| 95 | +your Beet configuration should include something like: |
| 96 | + |
| 97 | +``` |
| 98 | +import: |
| 99 | + write: yes |
| 100 | +``` |
| 101 | + |
| 102 | +Then we only need to add `amplify` to our source creation function. In the example below we also add `blank.eat`, to automatically cut silence at the beginning or end of tracks. |
| 103 | + |
| 104 | +```{.liquidsoap include="beets-amplify.liq" from="BEGIN"} |
| 105 | +
|
| 106 | +``` |
| 107 | + |
| 108 | +This is the recommended Beets integration ; |
| 109 | +such source will provide music continuously, |
| 110 | +at a regular volume. |
| 111 | + |
| 112 | +## Beets as a requests protocol |
| 113 | + |
| 114 | +If you're queueing tracks with `request.queue`, |
| 115 | +you may prefer to integrate Beets as a protocol. |
| 116 | +In that case, |
| 117 | +the list of paths returned by `beet random -f '$path'` fits directly |
| 118 | +what's needed by protocol resolution: |
| 119 | + |
| 120 | +```{.liquidsoap include="beets-protocol.liq" from="BEGIN"} |
| 121 | +
|
| 122 | +``` |
| 123 | + |
| 124 | +Once this is done, |
| 125 | +you can push a beets query from [the telnet server](server.html): |
| 126 | +if you created `request.queue(id="userrequested")`, |
| 127 | +the server command |
| 128 | +`userrequested.push beets:All along the watchtower` |
| 129 | +will push the Jimi Hendrix's song. |
| 130 | + |
| 131 | +With this method, you can benefit from replay gain metadata too, by wrapping |
| 132 | +the recipient queue in an `amplify` operator, like |
| 133 | + |
| 134 | +```liquidsoap |
| 135 | +userrequested = amplify(override="replaygain_track_gain", 1.0, |
| 136 | + request.queue(id="userrequested") |
| 137 | +) |
| 138 | +``` |
0 commit comments