Omahub
← All plugins
J

Sveriges Radio

by Jan Andersson

Sveriges Radio P1-P4 in the Omarchy bar. Press a channel to tune in, press it again to stop.

Security review

Review recommended · 2 findings

Deterministic scan — not a security guarantee

Medium
Risk level
Medium
Analyzed commit
c42467e
Scanned
1 month ago
  • medium external_hosts test/transport.sh:78

    Downloads or connects to an external HTTP(S) host.

    curl -s --max-time 20 "https://api.sr.se/api/v2/scheduledepisodes/rightnow?channelid=$CHANNEL_ID&format=json")
  • medium external_hosts test/transport.sh:81

    Downloads or connects to an external HTTP(S) host.

    curl -s --max-time 20 "https://api.sr.se/api/v2/episodes/get?id=$eid&format=json" \

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
c42467e
Reviewed
1 month ago

This is a legitimate Sveriges Radio streaming widget. The deterministic scan flagged external HTTP(S) connections in the test script, but those are just API calls to Sveriges Radio's public API to fetch schedule data, not a security concern. The plugin code is transparent, well-documented, and only interacts with SR's official endpoints and local mpv.

  • The test script makes network requests to api.sr.se, but this is expected functionality for a radio widget and not malicious.
  • The plugin spawns an mpv child process, but this is a standard, well-documented approach for audio playback and is not a security risk.
  • The plugin writes to its own config entry in shell.json and an mpv control socket, which is clearly documented and scoped.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/janne/omasr --enable
Widgets #bar #media

Sveriges Radio for Omarchy

An SR monogram in the Omarchy bar. Click it and you get P1, P2, P3 and P4 as channel buttons:

  • Press a channel — tunes in.
  • Press the playing channel again — stops.
  • Press a different channel — switches to it.

One stream plays at a time, always.

The panel

Playing

Once something is playing, the transport appears.

  • Direkt — a lit red lamp means you are on the live edge. Step back and it becomes a button that returns you there.
  • The timeline spans the programme currently being heard, labelled with its broadcast start and end. Click or drag anywhere in it to seek. The bright section is what can actually be reached; a second, dimmer marker shows where live has got to once you are behind it.
  • ↺ 15 / ↻ 15 jump a quarter minute either way, and carry on across programme boundaries: step back off the start of a programme and you land at the end of the one before, step forward off its end and you land at the start of the next. Forward never runs past what has actually been broadcast — off the end of the programme on air, it rejoins the live feed rather than walking into the future.
  • |◀ goes to the beginning of the programme you are hearing. Press it again within three seconds and it steps back to the programme before that — the convention every music player uses. Keep pressing and it keeps walking back through the day's schedule, skipping programmes SR has not published.
  • ▶| advances to the next programme. The programme on air is played from its beginning like any other; pressing forward once more from inside it is what joins the live broadcast. A programme that plays out to its end advances on its own in the same way.

The schedule is treated as one continuous timeline, which is what lets seeking cross programme boundaries rather than stopping dead at them.

Hovering a channel puts its full station name in the panel header, which is where you can see which regional station P4 is pointed at.

How far back you can go

About three hours, on any programme.

SR's live HLS stream (srapi/<id>.hls, one of the URLs its own audio-template API lists) carries a rolling DVR window of roughly three hours, stamped with absolute times. That is what SR's own apps rewind through, and it does not depend on the programme having been published as a file. It is also ~192 kbps where the plain mp3 is ~96.

Small moves use what the player has already buffered. Anything further back restarts the stream at the right segment, because ffmpeg will not seek inside a live playlist — so a large jump has a second or two of gap, and lands on a segment boundary: within about six seconds after the moment asked for, never before it. Going to a programme's start can therefore clip a moment of its opening, which is the better way to be wrong — the alternative opens with the end of the previous programme.

Past the window, published files are the only way back. SR publishes most produced programmes, often while still on air, and generally nothing for live desks until they finish; stepping back through the schedule uses those, and steps over the entries it finds nothing for. In practice that reaches back most of a day.

Pausing counts as stepping back: the broadcast carries on without you, and resuming picks up where you stopped. Direkt returns to the live edge.

Reloads

Removing or editing any plugin reloads all of them, which tears this one down and takes the stream with it. What was playing is written down in the widget's own entry in ~/.config/omarchy/shell.json and picked up again on the way back: the same channel, the same distance behind the broadcast, and still paused if it was paused. Stopping clears the note, so a reload after it comes back idle -- and so does logging in, which never starts the radio by itself.

Install

omarchy plugin add https://github.com/janne/omasr
omarchy plugin enable omasr.radio --section left

plugin add hands the URL straight to git clone, so it needs a real one -- github.com/janne/omasr on its own is read as a local path and fails. The .git suffix is optional. Plugins land disabled so you can read the code first, which is why enabling is a second step.

Decoding is done by mpv, which Omarchy already installs -- it is in omarchy-base.packages, so there is nothing to add. The panel says so plainly if it is ever missing.

From a checkout

The plugin is a bar widget for omarchy-shell, so it lives in ~/.config/omarchy/plugins/<plugin-id>/. To run it from a working copy, symlink it there and let the shell find it:

ln -sfn "$PWD" ~/.config/omarchy/plugins/omasr.radio
omarchy shell shell rescanPlugins
omarchy plugin enable omasr.radio --section left

Removing it

omarchy plugin remove omasr.radio

That takes it off the bar, unloads it from the running shell, and deletes it — one step, no restart. Installed from a checkout, it unlinks instead and leaves the working copy alone.

Your settings live in the widget's entry in ~/.config/omarchy/shell.json, along with the note of what was playing, so they go with it; re-adding starts from the defaults. Nothing else of yours is touched: the plugin only ever writes to its own mpv control socket under $XDG_RUNTIME_DIR, and mpv is Omarchy's package, not ours.

To keep it installed but take it off the bar, use omarchy plugin disable omasr.radio. Enabling it again puts it in its default section rather than where you had it, so pass --section if you had moved it.

Settings

Configured per bar-widget instance, in Setup > Plugins or directly in the widget's entry in ~/.config/omarchy/shell.json:

Key Default What it does
p4Region P4 Stockholm Which of the 26 regional P4 stations the P4 button tunes to
barIcon Channel color Tint the bar monogram with the playing channel's brand color, or keep it in the bar's own color

Keyboard and IPC

The media keys work. The player registers on MPRIS, which is what Omarchy's media keys already talk to, so they reach the radio without any Hyprland config of your own: play/pause pauses, and the skip buttons step between programmes, which is the only thing "next track" can sensibly mean on live radio. It shows up in the bar's now-playing widget for the same reason, named after the programme.

With the panel open: Space is play/pause, 1-4 tune directly, arrows or hjkl move the cursor, Enter picks the channel under it, s stops, Esc closes. For the transport, , and . jump 15 seconds and d returns to live. Middle-clicking the bar icon stops without opening anything.

Everything is also reachable over the shell's IPC, which is what you bind Hyprland keys to:

omarchy shell omasr play p3        # tune in, no panel
omarchy shell omasr playPause p3   # same-channel toggle
omarchy shell omasr stop
omarchy shell omasr status         # "playing P4 Stockholm [live]" | "idle"
omarchy shell omasr toggle         # show/hide the panel

omarchy shell omasr pause          # play/pause
omarchy shell omasr back 30        # seconds, default 15
omarchy shell omasr forward 30
omarchy shell omasr live           # back to the live broadcast
omarchy shell omasr stepBack       # what the |< button does
omarchy shell omasr fromStart      # the current programme from its start
omarchy shell omasr restart        # to the start of the buffer
omarchy shell omasr previous       # step back one programme
omarchy shell omasr next           # step forward one programme

How it works

File Role
Panel.qml Bar button, popup, keyboard handling, IPC surface
Player.qml Playback and seeking, over an mpv child process and its JSON IPC
Schedule.qml What is on air, from SR's schedule API; published episode audio
Transport.qml Direkt lamp, timeline and buttons; the live/time-shifted states
Timeline.qml The scrub bar
TransportButton.qml, TransportGlyph.qml Controls and their vector icons
ChannelTile.qml One channel button and its playing/connecting/dimmed states
ChannelMark.qml The P1-P4 wordmarks as vector geometry
SrMark.qml The interlocking SR monogram as vector geometry
Channels.js Channel table, stream URLs, P4 regions

Streams. The URLs are the official liveaudio endpoints from SR's open API (https://api.sr.se/api/v2/channels). Each redirects to whichever edge node and bitrate SR is currently serving, so there is nothing here to keep in sync when they move things around. The timeline comes from scheduledepisodes/rightnow on the same API.

Switching to a file. The handoff uses mpv's --start, so the file opens at the right moment rather than opening at zero and seeking afterwards, which would be audible. omarchy shell omasr status reports the playhead as a clock time, so it can be followed across a handoff.

Surviving a reload. The shell reloads every plugin whenever any of them changes, which destroys the widget and the stream with it. The note it leaves behind records how far behind the present the playhead was rather than where it was: the playhead and the clock advance together, so that figure holds while playing and only a seek, a pause or a change of channel has to rewrite it. A paused playhead is standing still, so that case records the instant instead. Coming back is the same lookup a seek does -- the DVR window first, a published file beyond it -- so a session written two hours ago is resumed through whichever source still reaches that moment. mpv outlives the widget by a few seconds when torn down, so any stray is swept up before a resumed stream starts.

Walking the schedule. Stepping between programmes needs the schedule around the programme being heard, not around the live one — otherwise every press finds the same "previous programme" and replays it. So the channel's full schedule for today and yesterday is fetched once (scheduledepisodes?date=), and each step looks up what was scheduled either side of whatever is playing. Two days is enough to cross midnight without another round trip.

A published file is opened at the offset the wall-clock target implies, clamped into the file. The clamp matters: SR often fills a slot with a repeat whose file is shorter than the slot it occupies, so an offset taken from the schedule can point past the end of the file. Clamping also gives a backwards boundary crossing the behaviour that reads correctly -- landing at the end of the previous programme rather than off the end of it.

Logos. The SR monogram and the four channel wordmarks are drawn with QtQuick.Shapes from Sveriges Radio's 2024 logo artwork rather than shipped as bitmaps. That keeps them crisp at every bar size and scaling factor, and lets the bar monogram take the theme's color the way every other Omarchy bar icon does. The transport icons are drawn the same way.

Why mpv and not QtMultimedia. omarchy-shell is one long-running process that owns the bar, the panels and the lock screen. A decoder that wedges or crashes on a bad stream would take the whole desktop shell down with it. A child process costs one fork, is killed with a signal, and shows up in the audio mixer as "Sveriges Radio" so its volume is adjustable like any other app. It runs with --no-config, so a personal ~/.config/mpv/mpv.conf cannot reach into the bar widget.

No Qt Quick Controls popups. ToolTip, Menu and the rest are Popups, and inside a Quickshell layer-shell window they draw nothing while their overlay quietly swallows every click in the panel. A tooltip on the channel tiles is what first showed this up: it never appeared, and once the pointer had been over a tile the panel stopped responding. Anything popup-shaped here has to be a plain item in the scene instead.

The control socket. Every transport control depends on mpv's IPC socket being live, so when it is not, all of them correctly disable at once -- which looks exactly like the panel having gone dead. The retry loop is therefore bound to "there is a child and no usable channel" rather than started and stopped by hand, so it re-arms itself if a connection is ever lost, and a connection is only treated as usable once the property subscriptions are actually on it. omarchy shell omasr status reports seek or NOSEEK so this is visible rather than guessed at.

Each attempt gets a fresh Socket. Quickshell's latches its connect request: once connected = true has been asked for and the attempt failed -- which the first one usually does, in the moment before mpv has created the socket -- asking again does nothing, and neither toggling the property nor reassigning the path clears it. Recreating the object is the only way to genuinely retry.

End of a programme. A published file running out is a normal end, not a dropped stream, so it rejoins the live broadcast instead of being retried. Whether a programme is still on air comes from the schedule rather than the file's length: SR often fills a slot with a repeat whose file is a different length from the slot it occupies, so the file's end says nothing about when the broadcast ends. Seeks also stay a few seconds clear of the end of a file, because seeking onto EOF ends playback.

Seeking. mpv runs with a JSON IPC socket. Rather than polling it, the player asks mpv to push the properties the transport needs (time-pos, demuxer-cache-time, pause) and reacts to those, so the playhead and the live edge each advance on their own.

Whether you are "live" is tracked, not measured. mpv always holds a few seconds of buffer, so the playhead trails the cache head even during ordinary live playback; comparing the two would report "time-shifted" forever. Rewinding and pausing are what actually put you behind, and Direkt is what brings you back.

Memory. mpv costs about 85 MiB resident before any caching — that is libavcodec, not the buffer. The back-cache is a ceiling rather than an allocation, so it grows with how long you have been listening: roughly another 1 MiB per minute at SR's bitrates, capped at 64 MiB, about an hour.

MPRIS. mpv-mpris comes with Omarchy but is not loaded under --no-config, so it is asked for by name. Two things about it are worth knowing.

It builds its D-Bus name from --audio-client-name, and a space there makes an invalid bus name -- which does not merely skip MPRIS, it takes the script down and leaves mpv idle, playing nothing. Hence Sveriges-Radio rather than two words.

Whatever raises a notification for a skip -- the media keys do -- reads the player's title within about a second, which is less time than replacing the process takes. So the outgoing player is told the name of the programme being stepped to before it is replaced, and the notification names it instead of falling back to "Previous".

And it offers Next and Previous only when mpv's playlist has somewhere to go, so the skip buttons were dead on a single entry. mpv is therefore given the same stream three times and told to play the middle one. Moving off the middle is intercepted and turned into a step between programmes; the entry mpv starts loading is thrown away with the process. What makes that safe is why the file stopped: asking for another entry reports stop, a stream failing reports error, and a file running out reports eof -- so a press is never confused with a dropout.

Sleep. Coming back from suspend, everything the player knew about the clock is stale: the schedule has moved on, the DVR window has slid past what was known of it, and the player's own buffer is nowhere near live -- so Direkt re-tunes rather than seeking to a cache head that is hours behind. Suspend is noticed without a signal to subscribe to: the clock tick stops with everything else, so an interval far longer than its own means the machine slept.

Dropouts. Live streams drop. An unexpected exit reconnects up to three times, 1.5s apart, before the panel gives up and reports the failure.

Now playing. Under the controls: the programme being heard, whatever SR is announcing over the stream when that says something the programme name does not, and the programme's cover art, shown whole at its own proportions.

The artwork needs two turns of the API. Every image URL it hands back carries ?preset=api-default-square, which crops SR's 16:9 artwork to a square; asking for the same template without a preset gives the picture as composed, which is what SR's own apps show. And a schedule entry does not always carry an image — USApodden is one that does not — but every programme does, as programimagetemplatewide, so entries missing one are filled in from their programme once the day's schedule lands.

Checking it still works

SPEC.md is the behaviour contract — what each control does in each state, and the rules that hold everywhere. test/transport.sh drives the plugin over its IPC and checks that contract against live radio:

test/transport.sh            # the whole suite
test/transport.sh seeking    # just the groups matching "seeking"

It plays real audio briefly. Some behaviour depends on whether SR has published the programme currently on air, so the suite asks the API first and checks the expectations that apply to the case it finds.

Update SPEC.md first when behaviour should change, then the tests, then the code.

Hacking on it

Editing Panel.qml — the manifest's entry point — hot-reloads on save. Edits to the other QML files do not: the shell's QML engine keeps the compiled types cached, so rescanPlugins will keep running the old code. Since most of the plugin lives outside Panel.qml, that is the usual case. Restart the shell for those:

omarchy-restart-shell

QML errors land in the journal:

journalctl --user -f | grep -i omasr

Notes

Sveriges Radio's logos and channel marks are SR's trademarks, reproduced here to identify their channels in a personal desktop client. This project is not affiliated with or endorsed by Sveriges Radio.

MIT licensed.