Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ remote control HTTP API, station profiles, local operator accounts.
**Rules** say what plays (an advert / jingle folder, chosen files, or the Adverts list), how often (after every N
songs, every N minutes or hours, or at set times) and when the rule is in force (dates, days, hours). The **Adverts
list** holds your adverts with the dates and days each may run. Rules can also be used in the playlist generator
(as a pattern step or inserted every N minutes) and run by the Event scheduler or the `RULE <name>` command.
(as a pattern step) and run by the Event scheduler or the `RULE <name>` command.

## Event scheduler (Tools → Event scheduler, F3)
At a set time (on chosen days and dates): generate a new playlist from a template, append one, play a rule's
Expand Down
10 changes: 4 additions & 6 deletions assets/portal/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ $("autoSave").addEventListener("click", () => saveCfg((c) => {
$("tplAdd").addEventListener("click", () => editTemplate(-1));
$("catAdd").addEventListener("click", () => editCategory(-1));

/// Rows of (source, numbers…) with add and remove: template steps and markers.
/// Rows of (source, numbers…) with add and remove: template steps.
function stepRows(items, make, fields, addText) {
const box = el("div");
const draw = () => {
Expand All @@ -595,9 +595,9 @@ function stepRows(items, make, fields, addText) {

function editTemplate(i) {
const t = i >= 0 ? JSON.parse(JSON.stringify(cfg.templates[i]))
: { name: "New template", minutes: 120, pattern: [{ category: (cfg.categories[0] || {}).name || "Music", count: 1 }], markers: [], artist_gap: 5, hour_markers: false };
: { name: "New template", minutes: 120, pattern: [{ category: (cfg.categories[0] || {}).name || "Music", count: 1 }], artist_gap: 5, hour_markers: false };
t.pattern = t.pattern || [];
t.markers = t.markers || [];
delete t.markers; // generator inserts were removed; the station's ad rules play jingles and adverts
const first = (sources()[0] || ["Music"])[0];
const name = el("input", { value: t.name, required: true });
const minutes = num(t.minutes, 1, 1440 * 7);
Expand All @@ -608,8 +608,6 @@ function editTemplate(i) {
el("div", { class: "pair" }, field("Length (minutes)", minutes), field("Songs before an artist repeats", gap)),
el("div", { class: "sub" }, el("strong", {}, "Steps"), el("p", { class: "muted small" }, "Played in order, then again from the top, until the length is reached."),
stepRows(t.pattern, () => ({ category: first, count: 1 }), [["count", "×", 1, 50]], "+ Step")),
el("div", { class: "sub" }, el("strong", {}, "Markers"), el("p", { class: "muted small" }, "Put in every so many minutes, e.g. a station ID every 30 minutes."),
stepRows(t.markers, () => ({ category: first, every_mins: 30, count: 1 }), [["every_mins", "every (min)", 1, 1440], ["count", "×", 1, 20]], "+ Marker")),
hourRow,
], () => {
const n = name.value.trim();
Expand Down Expand Up @@ -653,7 +651,7 @@ function editCategory(i) {
if (k >= 0) x.categories[k] = c; else x.categories.push(c);
// the templates follow a renamed category
if (old !== null && old !== n) {
(x.templates || []).forEach((t) => [...(t.pattern || []), ...(t.markers || [])].forEach((p) => { if (p.category === old) p.category = n; }));
(x.templates || []).forEach((t) => (t.pattern || []).forEach((p) => { if (p.category === old) p.category = n; }));
}
});
});
Expand Down
14 changes: 1 addition & 13 deletions lang/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@
"Assign audio file": "",
"Assign audio file…": "",
"At": "",
"At the top of every hour": "",
"Attack": "",
"Audio FX": "",
"Audio devices": "",
Expand Down Expand Up @@ -242,7 +241,6 @@
"Events": "",
"Every": "",
"Every 30 minutes on the clock = at :00 and :30. Off = 30 minutes after the last time.": "",
"Every 60 minutes on the clock = at the top of each hour. Off = counted from the start of the playlist.": "",
"Every change is listed in cleanup-log.csv in the data folder.": "",
"Every day": "",
"Every file is there and can be read.": "",
Expand Down Expand Up @@ -340,7 +338,6 @@
"Insert tracks from playlist…": "",
"Insert voice track": "",
"Insert voice track…": "",
"Inserts": "",
"Install now": "",
"Interface size": "",
"Intro sound": "",
Expand All @@ -350,7 +347,6 @@
"JINGLE": "",
"Jingles": "",
"Jingles & adverts (their rule's files)": "",
"Jingles, adverts, talk or a time announcement, like the advert / jingle rules: after a number of songs, every so often, or at set times. Timed ones go in after the song that's playing when the time comes.": "",
"Keep": "",
"Keep the music at full level": "",
"Keep the playing track near the top as songs finish (the last one played stays in view)": "",
Expand Down Expand Up @@ -405,7 +401,6 @@
"MIDI only": "",
"MP3 (built in)": "",
"MP3 (compressed, 64–320 kbps)": "",
"Make a jingle rule in the Advertisement scheduler first, or add a Jingles category": "",
"Make a tunnel in the Cloudflare dashboard (Zero Trust → Networks → Tunnels) for your own domain, and point it at http://localhost and the portal port. Without a token you get a random trycloudflare.com address.": "",
"Make everything larger or smaller if the text is hard to read.": "",
"Make this the primary output": "",
Expand Down Expand Up @@ -767,7 +762,7 @@
"The song on air fades out and the break starts on time. A countdown lets the presenter play it now, snooze it or skip it.": "",
"The song on air will fade out.": "",
"The station goes quiet while the server restarts (a few seconds) — best done between shows. This app reconnects by itself.": "",
"The template's times of day and timed inserts follow the clock from where the new tracks start.": "",
"The template's times of day follow the clock from where the new tracks start.": "",
"The track is only": "",
"There's no RadioMaster Server download for the server's system: update it by hand": "",
"These accounts sign in to the server (from the app and the web portal). The server checks every action against them.": "",
Expand Down Expand Up @@ -829,7 +824,6 @@
"Username": "",
"Users & access control": "",
"Users…": "",
"Uses the files of a jingle rule (Advertisement scheduler) every 15 minutes": "",
"Version": "",
"Very slow computer": "",
"View": "",
Expand Down Expand Up @@ -893,7 +887,6 @@
"advert name": "",
"advertiser / company": "",
"adverts": "",
"after every": "",
"after the playlist (instead of replacing it)": "",
"after the track on air": "",
"all day": "",
Expand All @@ -915,7 +908,6 @@
"e.g. station id, news": "",
"egui · rodio · symphonia · LAME": "",
"encoder": "",
"every": "",
"every advert": "",
"failed": "",
"files": "",
Expand Down Expand Up @@ -1016,7 +1008,6 @@
"this band": "",
"this category": "",
"this event": "",
"this insert": "",
"this rule": "",
"this shortcut": "",
"this template": "",
Expand Down Expand Up @@ -1109,12 +1100,9 @@
"➕ Add encoder": "",
"➕ Add event": "",
"➕ Add files": "",
"➕ Add insert": "",
"➕ Add jingles": "",
"➕ Add pattern step": "",
"➕ Add streamer": "",
"➕ Add time": "",
"➕ Add time announcement": "",
"➕ Add time of day": "",
"➕ Advert rule": "",
"➕ Folder…": "",
Expand Down
16 changes: 6 additions & 10 deletions site/guides/playlist-generator/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<meta property="og:type" content="article">
<meta property="og:site_name" content="RadioMaster">
<meta property="og:title" content="How to use the playlist generator">
<meta property="og:description" content="Categories, templates, patterns, jingle inserts and auto-playlist: build hours of programming in one click.">
<meta property="og:description" content="Categories, templates, patterns and auto-playlist: build hours of programming in one click.">
<meta property="og:url" content="https://radiomaster.dovafm.com/guides/playlist-generator/">
<meta property="og:image" content="https://radiomaster.dovafm.com/img/main.png">
<meta property="og:image:width" content="1280">
Expand All @@ -23,7 +23,7 @@
<meta property="og:locale" content="en_GB">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="How to use the playlist generator">
<meta name="twitter:description" content="Categories, templates, patterns, jingle inserts and auto-playlist: build hours of programming in one click.">
<meta name="twitter:description" content="Categories, templates, patterns and auto-playlist: build hours of programming in one click.">
<meta name="twitter:image" content="https://radiomaster.dovafm.com/img/main.png">
<script type="application/ld+json">
{
Expand Down Expand Up @@ -114,7 +114,7 @@
<p class="eyebrow">Guide · Run the station</p>
<h1 class="page-h">How to use the playlist generator</h1>
<p class="lede" style="margin-top:16px">The playlist generator builds hours of programming from your music library in one click. You tell it what pools of songs you have (categories) and how an hour should sound (a template), and it fills the playlist, keeps artists and songs apart, changes the sound for different times of day and drops in jingles, talk and time announcements on the clock.</p>
<nav class="toc" aria-label="On this page"><a href="#before">Before you start</a><a href="#categories">Categories</a><a href="#templates">Templates</a><a href="#dayparts">Times of day</a><a href="#inserts">Inserts</a><a href="#generate">Preview and generate</a><a href="#auto">Auto-playlist</a><a href="#schedule">On a schedule</a><a href="#tips">Tips</a></nav>
<nav class="toc" aria-label="On this page"><a href="#before">Before you start</a><a href="#categories">Categories</a><a href="#templates">Templates</a><a href="#dayparts">Times of day</a><a href="#generate">Preview and generate</a><a href="#auto">Auto-playlist</a><a href="#schedule">On a schedule</a><a href="#tips">Tips</a></nav>
</section>

<section class="block">
Expand Down Expand Up @@ -147,15 +147,12 @@ <h2 id="dayparts">3. Times of day</h2>
<div class="rundown-table"><table><thead><tr><th>Time of day</th><th>Hours and days</th><th>Pattern</th></tr></thead><tbody><tr><td class="it">Breakfast</td><td>06:00–10:00, Mon–Fri</td><td>1 × Talk, 2 × Current</td></tr><tr><td class="it">Overnight</td><td>00:00–06:00, every day</td><td>3 × Gold, 1 × Station IDs</td></tr></tbody></table></div>
<p>The clock follows the new tracks from where they will start on air, so a template that runs from 05:00 switches to the breakfast pattern at about 06:00.</p>

<h2 id="inserts">4. Inserts</h2>
<p>Inserts are things that go in on their own schedule instead of by position in the pattern, set up like <a href="/guides/advertisement-scheduler/">advert and jingle rules</a>. Press <b>➕ Add insert</b>, pick what to insert and how many, then when:</p>
<div class="rundown-table"><table><thead><tr><th>When</th><th>Example</th></tr></thead><tbody><tr><td class="k">after every N songs</td><td>1 × Station IDs after every 4 songs</td></tr><tr><td class="k">every N minutes</td><td>A jingle every 15 minutes. Tick <b>on the clock</b> to count from the top of the hour, so every 60 minutes means at :00.</td></tr><tr><td class="k">at set times</td><td>1 × Talk at 18:00 and 20:30</td></tr></tbody></table></div>
<p>Each insert can be limited to some hours (untick <b>all day</b>). An insert can come from a category, from the files of an advert or jingle rule, or be a <b>⏰ Time announcement</b>: <b>➕ Add time announcement</b> adds one at the top of every hour. <b>➕ Add jingles</b> is a shortcut that adds your first jingle rule every 15 minutes.</p>
<p>Timed inserts go in after the song that's playing when the time comes, so songs are never cut.</p>
<h3>Jingles, adverts and time announcements</h3>
<p>For jingles and IDs in a fixed spot, add them as a pattern step (step 4 in the example above). For anything on its own schedule, such as a jingle every 15 minutes, adverts at set times or after every few songs, use the <a href="/guides/advertisement-scheduler/">advertisement scheduler</a>: its rules fire while the generated playlist plays, so they aren't put in twice. For a time announcement at the top of every hour, use <b>➕ Hourly time announcement</b> in the <a href="/guides/event-scheduler/">event scheduler</a>.</p>
<h3>Hour markers</h3>
<p>Tick <b>Hour marker at the top of every hour</b> and the generator places an <a href="/#playlist">hour marker</a> where each new hour starts, so the station stays on the clock even if the songs run long or short.</p>

<h2 id="generate">5. Preview and generate</h2>
<h2 id="generate">4. Preview and generate</h2>
<p><b>👁 Preview</b> shows the running order with the time each row starts, the number of tracks and the total length, without touching the playlist. Press <b>⟳ Again</b> for another mix, tick <b>after the playlist</b> to see it as an addition, and <b>✔ Put it in the playlist</b> when you like it. Or go straight to:</p>
<ul>
<li><b>▶ Generate (replace playlist)</b> clears the playlist and fills it with the template.</li>
Expand All @@ -175,7 +172,6 @@ <h2 id="tips">Tips</h2>
<ul>
<li>Within one generated playlist, every track in a category plays once before any of them repeats, so big categories stay fresh.</li>
<li>If artist separation can't be met (a small category with few artists), the generator still picks a track rather than leaving a gap.</li>
<li>Timed inserts (jingles, IDs, adverts) don't count towards artist separation.</li>
<li>Times of day let one template cover a whole day. For very different shows you can still make a template each and switch between them with the event scheduler.</li>
<li>While connected to <a href="/server/">RadioMaster Server</a>, the library and folders are the server's, so a category folder is typed the way the server sees it, for example <code>/srv/music/current</code>.</li>
</ul>
Expand Down
10 changes: 2 additions & 8 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,10 @@ fn action_pick(action: ActionKind) -> Pick {
}

/// Where generator tracks come from: a category, or the files of an advert / jingle rule
/// (stored as "rule:<id>"). `rules` = (id, name, adverts). `time` also offers a time announcement
/// (for inserts).
fn source_combo(ui: &mut egui::Ui, id: impl std::hash::Hash, cur: &mut String, cats: &[String], rules: &[(u64, String, bool)], time: bool) {
/// (stored as "rule:<id>"). `rules` = (id, name, adverts).
fn source_combo(ui: &mut egui::Ui, id: impl std::hash::Hash, cur: &mut String, cats: &[String], rules: &[(u64, String, bool)]) {
let shown = match cur.strip_prefix("rule:").and_then(|s| s.parse::<u64>().ok()) {
Some(rid) => rules.iter().find(|r| r.0 == rid).map(|r| format!("⚡ {}", r.1)).unwrap_or_else(|| tr("(deleted rule)").to_string()),
None if cur == crate::generator::TIME_SOURCE => format!("⏰ {}", tr("Time announcement")),
None => cur.clone(),
};
egui::ComboBox::from_id_salt(id).selected_text(shown).width(170.0).show_ui(ui, |ui| {
Expand All @@ -660,10 +658,6 @@ fn source_combo(ui: &mut egui::Ui, id: impl std::hash::Hash, cur: &mut String, c
ui.selectable_value(cur, format!("rule:{rid}"), format!("⚡ {name} ({tag})"));
}
}
if time {
ui.separator();
ui.selectable_value(cur, crate::generator::TIME_SOURCE.to_string(), format!("⏰ {}", tr("Time announcement")));
}
});
}

Expand Down
49 changes: 10 additions & 39 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,43 +539,6 @@ impl Default for PatternItem {
}
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Marker {
/// a category name, "rule:<id>" for the files of an advert / jingle rule, or
/// `generator::TIME_SOURCE` for a time announcement
pub category: String,
/// after every N songs, every N minutes, or at set times (like an advert / jingle rule)
pub trigger: Trigger,
pub every_songs: u32,
pub every_mins: u32,
/// every N minutes counted from the top of the hour (:00, :15…) instead of from the start
pub on_clock: bool,
/// (hour, minute) for `Trigger::Times`
pub times: Vec<(u32, u32)>,
/// how many to insert each time
pub count: u32,
/// only between these hours (from == to: all day)
pub from_hour: u32,
pub to_hour: u32,
}
impl Default for Marker {
fn default() -> Self {
Self {
category: "Music".into(),
// inserts made before 0.2.9 only had "every N minutes"
trigger: Trigger::Minutes,
every_songs: 4,
every_mins: 60,
on_clock: false,
times: vec![],
count: 1,
from_hour: 0,
to_hour: 0,
}
}
}

/// A different pattern for part of the day, e.g. talk and fewer songs on weekday mornings.
#[derive(Serialize, Deserialize, Clone)]
#[serde(default)]
Expand Down Expand Up @@ -620,7 +583,6 @@ pub struct Template {
pub pattern: Vec<PatternItem>,
/// patterns used instead of `pattern` at some times of day (the first that covers the time wins)
pub dayparts: Vec<Daypart>,
pub markers: Vec<Marker>,
pub artist_gap: u32,
/// the same song doesn't come back within this many minutes (0 = whenever its pool runs out)
pub song_gap_mins: u32,
Expand All @@ -636,7 +598,6 @@ impl Default for Template {
until: (0, 0),
pattern: vec![PatternItem::default()],
dayparts: vec![],
markers: vec![],
artist_gap: 5,
song_gap_mins: 0,
hour_markers: false,
Expand Down Expand Up @@ -1101,6 +1062,16 @@ mod rule_tests {
assert!(c.rotations.is_empty() && c.ad_breaks.is_empty());
}

#[test]
fn templates_saved_with_inserts_still_load() {
// generator inserts ("markers") were removed; old settings still load with everything else
let old = r#"{"templates":[{"name":"Day","minutes":90,"artist_gap":3,
"markers":[{"category":"rule:1","trigger":"Minutes","every_mins":15,"count":1}]}]}"#;
let c: Config = serde_json::from_str(old).unwrap();
assert_eq!(c.templates.len(), 1);
assert_eq!((c.templates[0].name.as_str(), c.templates[0].minutes, c.templates[0].artist_gap), ("Day", 90, 3));
}

#[test]
fn cart_pads_keep_their_colour_and_old_pads_have_none() {
let old: CartPad = serde_json::from_str(r#"{"label":"ID","path":"/j/id.mp3"}"#).unwrap();
Expand Down
Loading
Loading