Move the app from C++ to Rust, and fix playlists and drops in the Flatpak #12

Merged
GrumpyStraw merged 22 commits from rust-migration into main 2026-10-11 20:45:13 +02:00
Owner

Move from C++ to Rust

The app's code moves from C++ to Rust, built with CXX-Qt and Corrosion. The QML interface and the CMake build stay. The Rust QObjects form their own QML module, StrawlyMusic.Library.

In Rust now:

  • Reading and saving playlist files, the search, and reading tags, lengths and covers with lofty. The app no longer calls FFmpeg itself.
  • The playlist library, All Music, the open playlist and the Now Playing queue, as list models.
  • The player, which still plays through Qt Multimedia.
  • The media controls (MPRIS) with zbus, and the tray icon with ksni, in place of Qt D-Bus and QSystemTrayIcon. ksni runs on zbus's async-io executor, so tokio is not pulled in.
  • The search filter, and the choice of the four covers on a playlist.

C++ remains where CXX-Qt has no way into Qt: main.cpp, which needs QApplication for the Plasma style, the cover image provider, and three small headers for QSettings, Qt Multimedia and the proxy model's source model.

User-visible changes are listed in CHANGELOG.md under Unreleased. Names now sort the same in every language, tags without a standard name are no longer searched, and MP3 lengths can be a fraction of a second longer.

Fixes

  • Playlists saved with absolute paths by another app, such as Elisa, showed every song as "File not found" in the Flatpak.
  • Folders and songs dropped from the file manager were not added in the Flatpak.
  • Playlists edited outside the Flatpak did not update until opened again, because inotify does not see changes through the document portal. The app now checks modification times. Refresh also reloads the open playlist.
  • The Shuffle and Repeat buttons did not look pressed while on.

Build, licenses and docs

  • The Flatpak builds offline with the crates from cargo-sources.json.
  • LICENSES/rust-crates.txt lists the 77 crates compiled into the app with their copyright holders, and matches cargo tree for the linked crates. Kirigami's LGPL-2.0-or-later text is added.
  • The workflow is renamed from build to Test and package, so the required check in the branch protection for main needs the new name.
  • README, CHANGELOG, comments and the manifest describe the Rust code.

Checked

In org.kde.Sdk//6.11: the Rust unit tests and the two C++ suites pass, and clippy, rustfmt and qmllint are clean. The Flatpak builds offline and was tested by hand on Plasma 6 (Wayland): playlists, search, editing, drops, outside edits, playback, the media controls and the tray.

## Move from C++ to Rust The app's code moves from C++ to Rust, built with CXX-Qt and Corrosion. The QML interface and the CMake build stay. The Rust QObjects form their own QML module, `StrawlyMusic.Library`. In Rust now: - Reading and saving playlist files, the search, and reading tags, lengths and covers with lofty. The app no longer calls FFmpeg itself. - The playlist library, All Music, the open playlist and the Now Playing queue, as list models. - The player, which still plays through Qt Multimedia. - The media controls (MPRIS) with zbus, and the tray icon with ksni, in place of Qt D-Bus and QSystemTrayIcon. ksni runs on zbus's async-io executor, so tokio is not pulled in. - The search filter, and the choice of the four covers on a playlist. C++ remains where CXX-Qt has no way into Qt: `main.cpp`, which needs `QApplication` for the Plasma style, the cover image provider, and three small headers for `QSettings`, Qt Multimedia and the proxy model's source model. User-visible changes are listed in `CHANGELOG.md` under Unreleased. Names now sort the same in every language, tags without a standard name are no longer searched, and MP3 lengths can be a fraction of a second longer. ## Fixes - Playlists saved with absolute paths by another app, such as Elisa, showed every song as "File not found" in the Flatpak. - Folders and songs dropped from the file manager were not added in the Flatpak. - Playlists edited outside the Flatpak did not update until opened again, because inotify does not see changes through the document portal. The app now checks modification times. Refresh also reloads the open playlist. - The Shuffle and Repeat buttons did not look pressed while on. ## Build, licenses and docs - The Flatpak builds offline with the crates from `cargo-sources.json`. - `LICENSES/rust-crates.txt` lists the 77 crates compiled into the app with their copyright holders, and matches `cargo tree` for the linked crates. Kirigami's LGPL-2.0-or-later text is added. - The workflow is renamed from `build` to `Test and package`, so the required check in the branch protection for `main` needs the new name. - README, CHANGELOG, comments and the manifest describe the Rust code. ## Checked In `org.kde.Sdk//6.11`: the Rust unit tests and the two C++ suites pass, and clippy, rustfmt and qmllint are clean. The Flatpak builds offline and was tested by hand on Plasma 6 (Wayland): playlists, search, editing, drops, outside edits, playback, the media controls and the tray.
## Rust in the build

A Rust crate in `rust/` now builds with the app. CMake still drives the build: CXX-Qt 0.10 and Corrosion 0.5.2 run Cargo and link the crate's static library into the app. QML is unchanged.

The C++ side calls Rust through `rust/src/bridge.rs`, which takes and returns `QString` and `QStringList`. `playlistparser.h` and `songsearch.h` keep their functions, so no caller changed. Each now forwards to Rust.

## What moved

- `rust/src/playlist.rs`: reading, parsing, writing and serializing .m3u8 files, the audio file extensions, and "Artist - Title" file names. `playlistparser.cpp` is gone.
- `rust/src/search.rs`: folding case and accents, reading a query with its fields, and matching a song's search text. Reading the tags stays in C++ in `songsearch.cpp` for now, because it goes through FFmpeg.

The tests moved with the code. `cargo test` runs 19 Rust tests as a new `rust` test in ctest. The C++ tests now cover the calls into Rust and the FFmpeg tag reading, with non-ASCII paths.

## Flatpak and CI

- The manifest adds the `rust-stable` SDK extension, builds Corrosion and cxx-qt-cmake as build-only modules, and gets the crates from `cargo-sources.json`, made by flatpak-cargo-generator, because the build has no network.
- CI installs the Rust extension and shares the network with the test build, so CMake and Cargo can download what they need.

## Licenses

The Rust crates compiled into the app are under MIT or zlib. `LICENSES/rust-crates.txt` lists them and is installed with the app. The README's third-party section says so.

## Checked

`cargo clippy -D warnings` and `cargo fmt --check` are clean. The C++ builds without warnings. All four ctest tests pass in `org.kde.Sdk//6.11`, and the Flatpak builds offline with `org.flatpak.Builder`.
## Rust code in src/

The `.rs` files, `build.rs` and `rustfmt.toml` move from `rust/` into `src/`, next to the C++ and QML. `Cargo.toml` and `Cargo.lock` move to the top of the repository, where Cargo looks for them, next to `CMakeLists.txt`. `rust/` is gone.

The C++ still includes `strawly/src/bridge.cxxqt.h`, because that path is relative to `Cargo.toml`. The CMake files, the manifest, the README, the license notice and the comments point to the new paths. Git ignores `/target` instead of `/rust/target`.

## Workflow

`.forgejo/workflows/build.yml` is now `test-and-package.yml`. The workflow is named "Test and package", and its jobs "Run the tests and build the Flatpak" and "Publish the release".

## Checked

`cargo fmt --check` and `cargo clippy -D warnings` are clean from the top of the repository. All four ctest tests pass in `org.kde.Sdk//6.11`, and the Flatpak builds offline with `org.flatpak.Builder`.
flatpak-cargo-generator writes cargo/config, a name Cargo still reads but warns is deprecated. A shell step in the manifest renames it after the sources are in place, so cargo-sources.json stays exactly as the generator makes it.

Checked: the Flatpak builds offline with org.flatpak.Builder and prints no warnings.
## What moved

- `src/tags.rs`: reads a song's text tags, its length and its cover with lofty 0.25. The format comes from the file's contents, so a FLAC file named .mp3 still reads. A panic while parsing a damaged file counts as a file that cannot be read, so it cannot take the app down.
- `src/search.rs`: builds a song's search text from the tags and keeps it in memory until the file changes, as `songsearch.cpp` did.
- `coverart.h` and `songsearch.h` now forward to Rust. `audiofile.cpp`, `audiofile.h`, `coverart.cpp` and `songsearch.cpp` are gone, and so is the FFmpeg check in CMake. The app binary links only Qt and the C and C++ libraries. Qt Multimedia still plays the songs through FFmpeg.

## Differences from FFmpeg

- lofty gives the standard tags one name each, and `tags.rs` maps those names to the search fields. Tags without a standard name, such as a custom TXXX frame in an MP3, are no longer searched.
- lofty counts an MP3 encoder's silent frames in the length, so the 2.5 s test MP3 reads as 2664 ms. Only search uses this length. The player bar's length comes from Qt Multimedia.

## Tests

Before the C++ tests were cut down, they passed unchanged against the Rust code. The detailed cases now live in Rust: covers from MP3, FLAC, M4A and Opus, the front cover after the back cover, a FLAC file named .mp3, tags and lengths in three formats, and the cache. The C++ tests check the calls into Rust.

## Licenses and Flatpak

`cargo-sources.json` is regenerated for lofty. `LICENSES/rust-crates.txt` lists lofty and the ten crates it brings in, all under MIT. The README's third-party section now lists FFmpeg for decoding through Qt Multimedia, and the Arch paragraph says what is still GPL there.

## Checked

`cargo fmt --check` and `cargo clippy -D warnings` are clean. All four ctest tests pass in `org.kde.Sdk//6.11`, with 27 Rust tests. The Flatpak builds offline with `org.flatpak.Builder` with no warnings, and `readelf` shows no FFmpeg library in the binary.
## What moved

- `src/playlist_library.rs`: the PlaylistLibrary singleton. It scans the music folder for playlists, keeps the music folder setting, adds songs to playlists, and turns dropped files and folders into songs.
- `src/song_model.rs`: SongModel, every audio file in the music folder. A Rust thread reads the tags for search and hands them back to the Qt thread, as the QThreadPool did.
- `src/track_model.rs`: TrackModel, the songs of one playlist, with add, remove, move and save.
- `playlistlibrary`, `songmodel` and `trackmodel` (.cpp and .h) are gone.

The three are CXX-Qt QObjects that subclass QAbstractListModel, with the same properties, methods and roles as before. CXX-Qt builds them into a QML module of their own, `StrawlyMusic.Library`, so the QML files that use them import it. `src/list_model.rs` declares QAbstractListModel once, because each bridge that declared it generated the same casting functions and the test binary failed to link.

New helpers:

- `src/watch.rs` replaces QFileSystemWatcher with the notify crate. It watches the same folders as before, ignores reads, and reports a burst of changes once, so the library still waits for a sync to settle for half a second.
- `src/collate.rs` replaces QCollator: names compare with case and accents ignored, and digits by value. It no longer follows the rules of the system's language.
- `src/settings.h` gives Rust the app's QSettings, which cxx-qt-lib has no binding for.

## Still C++

SearchFilterModel is Qt glue around a QSortFilterProxyModel, and its matching is already Rust. QueueModel belongs to the C++ Player and moves with it.

## .qmlls.ini

cxx-qt-build writes `.qmlls.ini` into the source folder when it is missing, and fails when that folder is read-only, as in the README's test command and in the CI. The committed file keeps it from trying.

## Checked

- `cargo fmt --check` and `cargo clippy -D warnings` are clean. All four ctest tests pass in `org.kde.Sdk//6.11`, with 32 Rust tests, and `all_qmllint` is clean. The Flatpak builds offline with `org.flatpak.Builder` with no warnings.
- The rebuilt app ran in a nested KWin with its own D-Bus bus and settings, on a test music folder. Playlists and All Music show in natural order with their song counts and covers. A playlist added while the app runs appears, and an edited one shows its new song count and cover.
- That run also logs three `PlaylistMenu.qml` TypeErrors when the playlist list reloads. `main`'s C++ build logs the same three for the same changes, so they come from the QML and are not part of this change.

`cargo-sources.json` is regenerated, and `LICENSES/rust-crates.txt` lists notify, inotify, walkdir and the crates they bring in.
Elisa saves playlists with absolute host paths, such as /home/andre/Music/Listas/../2026/A.mp3. The Flatpak reaches the music folder through the document portal at /run/user/1000/doc/<id>/Music, so every song showed as "File not found". Playlists synced from a phone or another computer have the same problem.

When an absolute entry (a /path, a file:// URL or a Windows C:/ path) does not exist, the song is looked for in the playlist's folder and the folders above it, dropping folders from the front of the path until one matches. The first match gives the mapping for the rest of the playlist, so the other entries usually take one check each. Relative entries, which Strawly Music writes, are never checked. The file is not rewritten until it is edited in Strawly Music, which saves relative paths.

Checked: a new Rust test covers host, file://, Windows and phone paths, a missing song and a relative entry. All four ctest tests pass in org.kde.Sdk//6.11, clippy is clean, and the Flatpak builds offline. In a nested KWin, a playlist with /home/andre/Music paths shows its songs and their cover.
A drop from Dolphin reaches the sandbox through the document portal with a path of its own, such as /run/user/1000/doc/<id>/2016, even when the folder is inside the music folder, which the app reaches at /run/user/1000/doc/<other id>/Music. songsFromUrls compared the two paths as text, so it left every drop out as outside the music folder.

The portal gives each file's path outside the Flatpak in the user.document-portal.host-path extended attribute. src/portal.rs reads it for the drop and for the music folder, and when the drop is inside the music folder there, it maps the drop to the same path inside the app's music folder. Drops from outside the music folder are still left out.

Checked: in the installed app's sandbox, the attribute reads /home/andre/Music for the music folder, and /home/andre/Music/2016 for an earlier drop that had been left out. A new Rust test covers a drop inside the music folder, one outside it, a path already inside, and a path without the attribute. All four ctest tests pass in org.kde.Sdk//6.11, clippy and qmllint are clean, and the Flatpak builds offline.
## Changes made by other apps

An edit in Kate did not show in the open playlist, and the library did not notice Elisa's save either. inotify on a document portal path reports only changes made through the portal. A test on a throwaway portal export showed it: a file written through the portal path gave an event, and a file written straight into the exported folder gave none. Kate, Elisa and Nextcloud all write to the folder itself, so in the Flatpak the watchers never fired, before the Rust models too.

`src/watch.rs` now checks when the watched files and folders were last modified: every second for the open playlist, and every two seconds for the music folder, the folders with playlists and the playlists. It reports a change once the paths have stayed the same for one more check, so a sync that writes many files reloads once. The notify crate is gone, and with it six crates from the build and from `LICENSES/rust-crates.txt`.

## Refresh

Refresh in the top bar now also reads the open playlist again, not only the playlist list and All Music.

## Drops

A folder dropped from Dolphin was still left out. Besides the portal path, a drop can give the folder's path outside the Flatpak, such as /home/andre/Music/2016, which does not exist in the sandbox. `portal::path_inside` now also maps such a path into the music folder when it starts with the music folder's path outside the Flatpak. Every dropped path that is left out is written to the journal, so a drop that adds nothing says why.

## Checked

`cargo fmt --check` and `cargo clippy -D warnings` are clean. All four ctest tests pass in `org.kde.Sdk//6.11`, and `all_qmllint` is clean. The Flatpak builds offline. In a nested KWin, a playlist added and a playlist edited while the app runs show up within a few seconds.
## Player

`src/player.rs` is the Player QML singleton, now a CXX-Qt QObject in StrawlyMusic.Library with the same properties, methods and signals as before. The queue logic moved as it was: play order, shuffle with the chosen song first, repeat, Previous restarting the song in its first three seconds, and skipping files that fail until every song has failed.

Playback still goes through Qt Multimedia. The player drives QMediaPlayer and QAudioOutput, which it declares in its bridge, and their signals come back to it through the Qt event loop. `src/media.h` creates the two objects, gives Rust names for the enums nested in QMediaPlayer, and passes positions as qint64, since cxx turns i64 into std::int64_t, which is long on Linux where qint64 is long long. For the signals that carry a qint64, `QInt64` in player.rs is that type.

The title, artist and cover of the playing song now come from the tags, read with lofty on another thread, instead of QMediaMetaData, whose cover Qt 6.11 leaves empty. The cover file for media controls holds the picture's own bytes, so it is no longer decoded and saved again as JPEG.

## Queue

`src/queue_model.rs` is QueueModel, a Rust list model. QML creates it in QueueView and sets its `paths` to the player's new `queue` property, the songs in play order, so the player no longer owns a model.

## The rest

- player.cpp, player.h, queuemodel.cpp and queuemodel.h are gone. main.cpp, the tray and MPRIS use the class CXX-Qt generates. MPRIS asks `isStopped()` to tell paused from stopped, and `coverPath()` for the cover.
- `settings.h` is now `appdata.h`, and also gives the cache folder. `src/appdata.rs` is the one bridge to it, since two bridges declaring the same functions link them twice.
- `player` is a public module, because its bridge declares the signals of QMediaPlayer and QAudioOutput only for their connect methods, and CXX-Qt does not allow `#[allow(dead_code)]` on a bridge.

## Checked

- `cargo fmt --check` and `cargo clippy -D warnings` are clean. All five ctest tests pass in `org.kde.Sdk//6.11`, and `all_qmllint` is clean.
- New `tests/tst_player.cpp` runs the Rust player with the real QMediaPlayer: play order, shuffle, repeat, the title and artist from the tags, and the cover file being written and removed. It keeps its cache in a temporary folder.
- The Flatpak builds offline. In a nested KWin with its own D-Bus bus, the app starts without QML errors, and MPRIS answers with the player's state.
## Media controls

`src/mpris.rs` publishes the player as an MPRIS media player with zbus, in place of the two QDBusAbstractAdaptor classes. It serves the same two interfaces with the same behaviour: LoopStatus Track reads as None, a rate of 0 pauses, the volume stays between 0 and 1, Seek past the end acts like Next, SetPosition ignores old track ids, and OpenUri answers NotSupported. A second copy of the app takes the .instance<pid> name.

zbus answers on a thread of its own. It reads a snapshot of the player's state, and passes calls on as commands, which the player carries out on the Qt thread. The player refreshes the snapshot from queued connections to its own signals, so it never re-enters itself in the middle of a change, and sends the changes of one action in one PropertiesChanged, as before.

## Tray icon

`src/tray.rs` is the Tray QML singleton, now a StatusNotifierItem served by ksni in place of QSystemTrayIcon. It goes by the connection's own name, as Qt's icon did, since the sandbox may not own StatusNotifierItem-<pid>-<n>. `available` asks the bus whether org.kde.StatusNotifierWatcher runs. ksni and zbus share the async-io executor, so tokio, which ksni pulls in by default, stays out.

## The window

The tray and the media controls no longer get the window. Main.qml shows and hides it, raises it for Raise, quits for Quit, and hides it instead of closing while the tray icon is in use. It tells the tray what plays through bindings, and the tray asks for Play, Previous and Next through signals.

mpris.cpp, mpris.h, tray.cpp and tray.h are gone, and the app no longer links Qt D-Bus. main.cpp only sets up the app and loads the QML.

## Build and licenses

- `appdata.h` also gives the app's display name and desktop file name, so the Rust bridge needs Qt Gui.
- The test command in the README and the CI builds the Rust code without debug information. With zbus and ksni, the two debug builds outgrew the 3.1 GB the SDK's /tmp has in RAM, and now take 1.8 GB.
- `cargo-sources.json` is regenerated. `LICENSES/rust-crates.txt` lists all 77 crates compiled into the app, and adds the Unlicense (ksni) and the Unicode License V3 (unicode-ident).
- PlaySlot.qml no longer imports StrawlyMusic, which it used only for the Player.

## Checked

- `cargo fmt --check` and `cargo clippy -D warnings` are clean. All five ctest tests pass in `org.kde.Sdk//6.11`, and `all_qmllint` reports nothing.
- The Flatpak builds offline. On a nested KWin with its own D-Bus bus, dbus-send read and set the media controls: Identity, DesktopEntry, Shuffle, LoopStatus Track and Playlist, a volume of 1.7 clamped to 1, 0.4 applied and saved to the settings, OpenUri refused, and Quit closing the app. Each change sent a PropertiesChanged.
SearchFilterModel is now a Rust QObject on QSortFilterProxyModel, in
src/search_filter_model.rs. src/filter.h reaches the parts of the proxy
model CXX-Qt has no bindings for: the source model's roles and data,
the Direction enum and the private row signals behind countChanged.
The QML module depends on org.kde.kitemmodels for qmllint only, since
it is the one module in the KDE runtime that describes
QSortFilterProxyModel.

tags::playlist_covers picks the first four different covers of a
playlist, and coverprovider.cpp calls it and cover_read straight
through the bridge. The bridge keeps only those two functions.

The C++ wrapper headers playlistparser.h, songsearch.h and coverart.h
are gone with their tests, which only tested the calls into Rust. The
Rust tests take over the non-ASCII names and the failed write, and a
new C++ test drives the search filter over a QStandardItemModel.
Show Shuffle and Repeat as on, and bring the docs and licenses up to date
Some checks failed
Test and package / Publish the release (pull_request) Has been cancelled
Test and package / Run the tests and build the Flatpak (pull_request) Has been cancelled
c595a8719b
The Plasma style draws a ToolButton as on only when it is checkable,
so the Shuffle and Repeat buttons looked off even with shuffle or
repeat on. They are checkable now, and each toggle binds checked to
the player again, so they still follow changes from the media
controls.

Docs and licenses after the move to Rust:
- The changelog has one entry for the move to Rust, with what changes
  for the user, instead of one per step
- README: Qt is no longer used for D-Bus, qqc2-desktop-style is
  LGPL-2.0-or-later and LGPL-3.0-only, the FFmpeg note no longer talks
  about Arch builds, and the tray menu lists all its entries
- LICENSES/LGPL-2.0-or-later.txt, Kirigami's license, is added and
  installed with the others
- Comments in the manifest, CMake files, appdata.rs and rustfmt.toml
  no longer describe the C++ code or Arch builds
- Paths under /run/user use <uid> or $XDG_RUNTIME_DIR in place of a
  user id
- International English: "license" and "grayed"
GrumpyStraw scheduled this pull request to auto merge when all checks succeed 2026-10-11 18:38:14 +02:00
Retry the Rust extension download in CI, and stop a stalled job after 90 minutes
Some checks failed
Test and package / Publish the release (pull_request) Has been cancelled
Test and package / Run the tests and build the Flatpak (pull_request) Has been cancelled
07407a6f85
The install of org.freedesktop.Sdk.Extension.rust-stable hung for over
25 minutes without output on the runner. Each try now stops after
8 minutes, the step tries three times and shows the download progress,
and the job fails after 90 minutes instead of holding the runner.
Turn off HTTP/2 for Flathub in CI, and retry stalled downloads sooner
Some checks failed
Test and package / Publish the release (pull_request) Has been cancelled
Test and package / Run the tests and build the Flatpak (pull_request) Has been cancelled
6d5656709f
On the runner, every try to install the Rust extension downloaded for a
few seconds and then received no more data without failing. ostree uses
HTTP/2 by default, and its http2 remote option turns that off. Each try
now stops after 3 minutes, and up to six tries carry on from the files
already downloaded.
Log why the Rust extension download stalls in CI, instead of retrying
Some checks failed
Test and package / Run the tests and build the Flatpak (pull_request) Failing after 5m5s
Test and package / Publish the release (pull_request) Has been skipped
ee939a836c
Turning off HTTP/2 did not help: the download stalled at 28% in every
try. One try now stops after 5 minutes and prints the end of ostree's
debug output, the free space and memory, and the size of the partial
download.
Fetch the Rust extension without static deltas in CI
Some checks failed
Test and package / Run the tests and build the Flatpak (pull_request) Failing after 5m5s
Test and package / Publish the release (pull_request) Has been skipped
1bacece667
On the runner the download stalled on two static delta parts from
Flathub, which download in under a second elsewhere. The disk had
1.6 TB free and memory 7.6 GB available.
Install the Rust extension without static deltas in CI, with 30 minutes for it
Some checks failed
Test and package / Run the tests and build the Flatpak (pull_request) Failing after 30m7s
Test and package / Publish the release (pull_request) Has been skipped
8c251d69e8
Fetched without static deltas, the download no longer stalls on the
runner. It runs at about 1.2 MB/s, so it needs about 15 minutes.
Downloading org.freedesktop.Sdk.Extension.rust-stable on the runner
stalled with static deltas, and without them slowed to 237 kB/s and ran
out of 30 minutes at 80%. The image in .forgejo/ci-image adds the
extension to Flathub's KDE 6.11 image. It is built on a desktop, where
the install took 12 seconds, and pushed to the Forgejo registry. The
README says how to rebuild it.
Use generic examples in place of Lifehouse in the search code and tests
All checks were successful
Test and package / Run the tests and build the Flatpak (pull_request) Successful in 15m13s
Test and package / Publish the release (pull_request) Has been skipped
a04b04fe88
GrumpyStraw deleted branch rust-migration 2026-10-11 20:45:13 +02:00
Sign in to join this conversation.
No description provided.