diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..25b5769 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,34 @@ +name: Build and Test + +on: + push: + branches: + - development + paths-ignore: + - '.github/**' + - 'docs/**' + - '**.md' + - '**.mm' + pull_request: + branches: + - development + paths-ignore: + - '.github/**' + - 'docs/**' + - '**.md' + - '**.mm' + +jobs: + flatpak: + name: "Flatpak" + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08 + options: --privileged + steps: + - uses: actions/checkout@v4 + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: io.github.louis77.tuner.flatpak + manifest-path: io.github.louis77.tuner.yml + cache-key: flatpak-builder-${{ github.sha }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59517ed..6f72cc7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,6 @@ on: push: branches: - main - - development paths-ignore: - '.github/**' - 'docs/**' @@ -14,7 +13,6 @@ on: pull_request: branches: - main - - development paths-ignore: - '.github/**' - 'docs/**' @@ -30,7 +28,7 @@ jobs: # This job runs in a special container designed for building Flatpaks for AppCenter container: - image: ghcr.io/elementary/flatpak-platform/runtime:8 + image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-24.08 options: --privileged # Steps represent a sequence of tasks that will be executed as part of the job @@ -44,7 +42,7 @@ jobs: # This is the name of the Bundle file we're building and can be anything you like bundle: Tuner.flatpak # This uses your app's RDNN ID - manifest-path: com.github.louis77.tuner.yml + manifest-path: io.github.louis77.tuner.yml # You can automatically run any of the tests you've created as part of this workflow run-tests: true diff --git a/.gitignore b/.gitignore index dfb266f..2a9f7cd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build builddir build-aux *~ +apidocs/* .vscode .buildconfig .flatpak-builder @@ -9,3 +10,9 @@ build-aux code.sh favicon.ico tuner.code-workspace +.~lock.tags.ods# +.~lock.tags.ods# +labelexpand.ods +tuner-starred.m3u8 +uncrustify.cfg +repo/* \ No newline at end of file diff --git a/DEVELOP.md b/DEVELOP.md index c131ceb..cb10222 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -7,6 +7,7 @@ Discover and Listen to your favourite internet radio stations, and add improve t - [Tuner Development](#tuner-development) - [Dependencies](#dependencies) - [Building the Tuner App From Source](#building-the-tuner-app-from-source) +- [Valadoc](#valadoc) - [Building the Tuner Flatpak](#building-the-tuner-flatpak) - [Readying code for a Pull Request](#readying-code-for-a-pull-request) - [NamingConventions](#namingconventions) @@ -49,7 +50,7 @@ sudo apt install libgtk-3-dev libgee-0.8-dev libgranite-dev libgstreamer1.0-dev ``` ### Building the Tuner App From Source -There are two build configurations: _debug_ and _release_. The _debug_ build (manifest _com.github.louis77.tuner.debug.yml_) is recommended for development, while the _release_ build (manifest _com.github.louis77.tuner.yml_) is for distribution. Build instructions will focus on the _debug_ build. Copy the required manifest to _com.github.louis77.tuner.xml_ before building. +There are two build configurations: _debug_ and _release_. The _debug_ build (manifest _io.github.louis77.tuner.debug.yml_) is recommended for development, while the _release_ build (manifest _io.github.louis77.tuner.yml_) is for distribution. Build instructions will focus on the _debug_ build. Copy the required manifest to _io.github.louis77.tuner.xml_ before building. Clone the repo and drop into the Tuner directory: @@ -63,7 +64,7 @@ Configure Meson for development debug build, build Tuner with Ninja, and run the meson setup --buildtype=debug builddir meson compile -C builddir meson install -C builddir # only needed once to get the gschema in place -./builddir/com.github.louis77.tuner +./builddir/io.github.louis77.tuner ``` Tuner can be deployed to the local system to bypass flatpak if required, however it is _recommended to use flatpak_.To do deploy locally, run the following command: @@ -72,6 +73,9 @@ meson configure -Dprefix=/usr sudo ninja install ``` +## Valadoc +valadoc --force --pkg gtk+-3.0 --pkg glib-2.0 --pkg gee-0.8 --pkg gio-2.0 --pkg libsoup-3.0 --pkg json-glib-1.0 --pkg gstreamer-1.0 --pkg gstreamer-player-1.0 --pkg granite --package-name=Tuner -o apidocs --verbose src/**/*.vala + ### Building the Tuner Flatpak Tuner uses the __elementary.io__ platform, version __8__. To build the tuner flatpak, install the elementry SDK and Platform: @@ -83,19 +87,19 @@ flatpak install elementary io.elementary.Sdk//8 io.elementary.Platform//8 Build the flatpak in the _user_ scope: ```bash -flatpak-builder --force-clean --user --sandbox --install build-dir com.github.louis77.tuner.debug.yml +flatpak-builder --force-clean --user --sandbox --install build-dir io.github.louis77.tuner.debug.yml ``` Run the Tuner flatpack: ```bash -flatpak --user run com.github.louis77.tuner +flatpak --user run io.github.louis77.tuner ``` Check the app version to ensure that it matches the version in the manifest. ### Readying code for a Pull Request Before a pull request can be accepted, the code must pass linting. This is done by running the following command: ```bash -flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest com.github.louis77.tuner.yml +flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest io.github.louis77.tuner.yml ``` Linting currently produces the following issues (adddressed in ticket #140): @@ -139,7 +143,7 @@ Debugging from VSCode using GDB, set up the launch.json file as follows: "name": "Debug Vala with Meson", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/builddir/com.github.louis77.tuner", + "program": "${workspaceFolder}/builddir/io.github.louis77.tuner", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -172,7 +176,7 @@ After checking out the required version, build and run the app as described abov ## Release Process -Releasing _Tuner_ comprises cutting a release of the code in [tuner github](https://github.com/louis77/tuner) and then updating the [flathub repo](https://github.com/flathub/com.github.louis77.tuner) which will automatically have the flatpak generated and rolled to Flathub for distribution. +Releasing _Tuner_ comprises cutting a release of the code in [tuner github](https://github.com/louis77/tuner) and then updating the [flathub repo](https://github.com/flathub/io.github.louis77.tuner) which will automatically have the flatpak generated and rolled to Flathub for distribution. ### Beta Releases Beta releases should be tagged from the Tuner _development_ branch in with a version number format of _v1.\*.\*-beta.\*_ @@ -186,6 +190,6 @@ Once a beta roll is deamed a success its pull request can be merged, and a produ ### Production Releases Production releases are generated from _development_ pull requests into _main_. The updated _main_ branch should be tagged with a version number format of _v1.\*.\*_ -Once a release has been tagged, the [flathub repo](https://github.com/flathub/com.github.louis77.tuner) _main_ branch can be updated with the _release_ tag going into the manifest _.json_, and any patches and documentation updated as needed. Updates from the _main_ branch should be copied in from a direct _pull request_ of the _main_ branch. The _main_ branch **should not** come from a merge _beta_ branch to avoid triggering subsequent builds in _beta_ . +Once a release has been tagged, the [flathub repo](https://github.com/flathub/io.github.louis77.tuner) _main_ branch can be updated with the _release_ tag going into the manifest _.json_, and any patches and documentation updated as needed. Updates from the _main_ branch should be copied in from a direct _pull request_ of the _main_ branch. The _main_ branch **should not** come from a merge _beta_ branch to avoid triggering subsequent builds in _beta_ . Once the main production release is built by flathub it will be available for installation and automatically distributed to user community. \ No newline at end of file diff --git a/PACKAGING.md b/PACKAGING.md index 6086b65..54f8cb7 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -22,14 +22,14 @@ $ flatpak run org.freedesktop.appstream-glib validate [path-to-xml] ### Test different languages ``` -$ LANGUAGE=de_DE ./com.github.louis77.tuner +$ LANGUAGE=de_DE ./io.github.louis77.tuner ``` ## Flatpak The flathub build manifest can be found here: -https://github.com/louis77/flathub/tree/com.github.louis77.tuner +https://github.com/louis77/flathub/tree/io.github.louis77.tuner - [ ] Move over to a non-elementary base image @@ -45,7 +45,7 @@ https://github.com/louis77/pacstall-programs https://docs.elementary.io/develop/writing-apps/logging ```bash -$ G_MESSAGES_DEBUG=all ./com.github.louis77.tuner +$ G_MESSAGES_DEBUG=all ./io.github.louis77.tuner ``` ### Available elementary Icons diff --git a/README.md b/README.md index 8e1f47c..fa77cd1 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,12 @@ Things I need help with: ### Flathub Tuner is available asa Flatpak on Flathub: -https://flathub.org/apps/details/com.github.louis77.tuner +https://flathub.org/apps/details/io.github.louis77.tuner ### elementary OS Install Tuner via elementary's App store: -https://appcenter.elementary.io/com.github.louis77.tuner +https://appcenter.elementary.io/io.github.louis77.tuner ### Arch Linux / AUR Arch-based GNU/Linux users can find `Tuner` under the name [tuner-git](https://aur.archlinux.org/packages/tuner-git/) in the **AUR**: @@ -100,7 +100,7 @@ While I hacked on this App, I discovered so many cool and new stations, which ma ## Environment Variables * `TUNER_API` - a `:` separated list of API servers to read from, e.g. - * `export TUNER_API="de1.api.radio-browser.info:nl1.api.radio-browser.info"; com.github.louis77.tuner` + * `export TUNER_API="de1.api.radio-browser.info:nl1.api.radio-browser.info"; io.github.louis77.tuner` ## Build, Maintance and Development of Tuner @@ -121,7 +121,7 @@ $ sudo apt install gstreamer1.0-libav ``` #### 'Failed to load module "xapp-gtk3-module"' -Running Tuner from the CLI with `flatpak run com.github.louis77.tuner` may produce a message like the following: +Running Tuner from the CLI with `flatpak run io.github.louis77.tuner` may produce a message like the following: `Gtk-Message: 10:01:00.561: Failed to load module "xapp-gtk3-module"` diff --git a/com.github.louis77.tuner.yml.release b/com.github.louis77.tuner.yml.release deleted file mode 100644 index 2763f28..0000000 --- a/com.github.louis77.tuner.yml.release +++ /dev/null @@ -1,48 +0,0 @@ ---- -app-id: com.github.louis77.tuner -runtime: io.elementary.Platform -runtime-version: '8' -sdk: io.elementary.Sdk -command: com.github.louis77.tuner -finish-args: -- "--share=ipc" -- "--socket=fallback-x11" -- "--socket=system-bus" -- "--socket=wayland" -- "--talk-name=org.gtk.vfs.*" -- "--share=network" -- "--metadata=X-DConf=migrate-path=/com/github/louis77/tuner/" -- "--socket=pulseaudio" -- "--talk-name=org.freedesktop.Notifications" -- "--own-name=org.mpris.MediaPlayer2.Tuner" -cleanup: -- "/include" -- "/lib/pkgconfig" -- "/share/pkgconfig" -- "/share/aclocal" -- "/man" -- "/share/man" -- "/share/gtk-doc" -- "/share/vala" -- "*.la" -- "*.a" -modules: -- name: taglib - buildsystem: cmake-ninja - config-opts: - - "-DBUILD_SHARED_LIBS=ON" - - "-DCMAKE_BUILD_TYPE=Release" - sources: - - type: archive - # taglib updated in v1.5.3 - url: https://github.com/taglib/taglib/releases/download/v1.13.1/taglib-1.13.1.tar.gz - sha256: c8da2b10f1bfec2cd7dbfcd33f4a2338db0765d851a50583d410bacf055cfd0b -- name: tuner - buildsystem: meson - config-opts: - - "--buildtype=release" - post-install: - - install -Dm644 /app/share/icons/hicolor/64x64/apps/${FLATPAK_ID}.svg -t /app/share/icons/hicolor/128x128/apps/ - sources: - - type: dir - path: . diff --git a/data/Application.css b/data/Application.css deleted file mode 100644 index df0fcc7..0000000 --- a/data/Application.css +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -.h2 { - font-weight: 300; - font-size: 18pt; - letter-spacing: -0.05em; -} - -.h3 { - font-weight: 500; -} - -.station-button-description { - font-size: 14px; - color: rgb(100, 100, 100); -} - -.station-button:hover { - background-color: darker(@bg_color); -} - -.station-button image { - border-width: 0; - border-style: solid; - border-color: #dddddd; - box-shadow: 2px 2px 3px grey; - background-color: #eee; - margin-right: 5px; - min-width: 48px; -} - -/* Used in Station Button for Codec/Bitrate Tag */ -.tag { - border-radius: 5px; - box-shadow: none; - min-width: 20px; - min-height: 18px; - font-size: 11px; - font-weight: 400; - margin-top: 3px; - margin-left: -2px; - padding-bottom: 0px; -} - diff --git a/data/com.github.louis77.tuner.gresource.xml b/data/com.github.louis77.tuner.gresource.xml deleted file mode 100644 index bab6d40..0000000 --- a/data/com.github.louis77.tuner.gresource.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - Application.css - - - icons/16/playlist-queue.svg - icons/16/playlist-similar.svg - icons/16/playlist.svg - icons/16/playlist-symbolic.svg - icons/16/internet-radio-symbolic.svg - icons/16/internet-radio.svg - - - diff --git a/data/css/Tuner-dark.css b/data/css/Tuner-dark.css new file mode 100644 index 0000000..7e94641 --- /dev/null +++ b/data/css/Tuner-dark.css @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + */ + + @import url("Tuner-system.css"); + +* { + background-color: rgba(71, 71, 71, 0.9); /* Dark background with 80% opacity */ + color: #ffffff; /* White text for contrast */ +} + +button { + background-color: rgba(46, 46, 46, 0.0); +} \ No newline at end of file diff --git a/data/css/Tuner-light.css b/data/css/Tuner-light.css new file mode 100644 index 0000000..c9392e8 --- /dev/null +++ b/data/css/Tuner-light.css @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + */ + + @import url("Tuner-system.css"); + +* { + background-color: rgba(200, 200, 200, 0.9); + color: #000000; +} + +button { + background-color: rgba(200, 200, 200, 0.0); +} \ No newline at end of file diff --git a/data/css/Tuner-system.css b/data/css/Tuner-system.css new file mode 100644 index 0000000..6c1b8c8 --- /dev/null +++ b/data/css/Tuner-system.css @@ -0,0 +1,86 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file Tuner-system.css + * + * Base CSS for Tuner - import this into superceding CSS + * + * Nominal order is top-to-bottom, left-to-right + */ + +/* + Station Label - top center top line +*/ +.station-label { + font-weight: 500; + font-size: 12pt; + letter-spacing: -0.05em; +} + +/* + Track Info - top center bottom line +*/ +.track-info { + /* font-weight: 150; */ + font-size: 11pt; + letter-spacing: -0.05em; +} + + + +/* + Stack + The Lower right quadrant that makes up 3/4 of the real estate +*/ +.stack-label { + font-weight: 300; + font-size: 18pt; + letter-spacing: -0.05em; +} + + +/* + Station Button +*/ +.station-button { + font-weight: 500; +} + +/* Pops the Station button on hover with a transitioning border */ +.station-button:hover { + background-color: lighter(@bg_color); + border: 0.1px; + border-style: ridge; +} + +/* Spaces the station button image */ +.station-button image { + background-color: darker(@bg_color); + margin-right: 5px; + min-width: 48px; +} + +/* Description on 2nd line RHS of the station button */ +.station-button-description { + font-size: 13px; + color: gray; + background-color: transparent; + border: none; +} + +/* Used in Station Button for Codec/Bitrate Tag, 2nd line, LHS */ +.tag { + border-radius: 5px; + box-shadow: none; + min-width: 20px; + min-height: 18px; + font-size: 11px; + font-weight: 400; + margin-top: 3px; + margin-left: -2px; + padding-bottom: 0px; +} + diff --git a/data/icons/128/com.github.louis77.tuner-symbolic.png b/data/icons/128/com.github.louis77.tuner-symbolic.png deleted file mode 100644 index 2f50e1b..0000000 Binary files a/data/icons/128/com.github.louis77.tuner-symbolic.png and /dev/null differ diff --git a/data/icons/128/com.github.louis77.tuner.png b/data/icons/128/com.github.louis77.tuner.png deleted file mode 100644 index f2a4106..0000000 Binary files a/data/icons/128/com.github.louis77.tuner.png and /dev/null differ diff --git a/data/icons/128/io.github.louis77.tuner-jukebox.svg b/data/icons/128/io.github.louis77.tuner-jukebox.svg new file mode 100644 index 0000000..5769d5d --- /dev/null +++ b/data/icons/128/io.github.louis77.tuner-jukebox.svg @@ -0,0 +1,655 @@ + + + + + Jukebox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jukebox + 2024-12-15 + + + technosf <https://github.com/technosf> + + + + + Copyright © 2024 technosf <https://github.com/technosf> + + + https://github.com/louis77/tuner + Jukebox icon for Tuner app + + + + + + + + + + + + diff --git a/data/icons/128/io.github.louis77.tuner-symbolic.png b/data/icons/128/io.github.louis77.tuner-symbolic.png new file mode 100644 index 0000000..42fcf3d Binary files /dev/null and b/data/icons/128/io.github.louis77.tuner-symbolic.png differ diff --git a/data/icons/128/io.github.louis77.tuner-symbolic.svg b/data/icons/128/io.github.louis77.tuner-symbolic.svg new file mode 100644 index 0000000..3b9d839 --- /dev/null +++ b/data/icons/128/io.github.louis77.tuner-symbolic.svg @@ -0,0 +1,501 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/128/io.github.louis77.tuner.png b/data/icons/128/io.github.louis77.tuner.png new file mode 100644 index 0000000..53ec2e6 Binary files /dev/null and b/data/icons/128/io.github.louis77.tuner.png differ diff --git a/data/icons/128/com.github.louis77.tuner.svg b/data/icons/128/io.github.louis77.tuner.svg similarity index 86% rename from data/icons/128/com.github.louis77.tuner.svg rename to data/icons/128/io.github.louis77.tuner.svg index d78a8b0..677fad0 100644 --- a/data/icons/128/com.github.louis77.tuner.svg +++ b/data/icons/128/io.github.louis77.tuner.svg @@ -1,12 +1,5 @@ image/svg+xml - - - - - - - - - - - - - - - - - - - - - + - \ No newline at end of file + diff --git a/data/icons/16/com.github.louis77.tuner-symbolic.png b/data/icons/16/com.github.louis77.tuner-symbolic.png deleted file mode 100644 index 00a771a..0000000 Binary files a/data/icons/16/com.github.louis77.tuner-symbolic.png and /dev/null differ diff --git a/data/icons/16/com.github.louis77.tuner.png b/data/icons/16/com.github.louis77.tuner.png deleted file mode 100644 index 5a06bf0..0000000 Binary files a/data/icons/16/com.github.louis77.tuner.png and /dev/null differ diff --git a/data/icons/16/io.github.louis77.tuner-symbolic.png b/data/icons/16/io.github.louis77.tuner-symbolic.png new file mode 100644 index 0000000..7348755 Binary files /dev/null and b/data/icons/16/io.github.louis77.tuner-symbolic.png differ diff --git a/data/icons/16/io.github.louis77.tuner-symbolic.svg b/data/icons/16/io.github.louis77.tuner-symbolic.svg new file mode 100644 index 0000000..dd703eb --- /dev/null +++ b/data/icons/16/io.github.louis77.tuner-symbolic.svg @@ -0,0 +1,501 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/16/io.github.louis77.tuner.png b/data/icons/16/io.github.louis77.tuner.png new file mode 100644 index 0000000..aafbb4b Binary files /dev/null and b/data/icons/16/io.github.louis77.tuner.png differ diff --git a/data/icons/16/com.github.louis77.tuner.svg b/data/icons/16/io.github.louis77.tuner.svg similarity index 76% rename from data/icons/16/com.github.louis77.tuner.svg rename to data/icons/16/io.github.louis77.tuner.svg index c50b3a2..ae9cf7c 100644 --- a/data/icons/16/com.github.louis77.tuner.svg +++ b/data/icons/16/io.github.louis77.tuner.svg @@ -1,12 +1,5 @@ image/svg+xml \ No newline at end of file + diff --git a/data/icons/24/com.github.louis77.tuner-symbolic.png b/data/icons/24/com.github.louis77.tuner-symbolic.png deleted file mode 100644 index f28a6b0..0000000 Binary files a/data/icons/24/com.github.louis77.tuner-symbolic.png and /dev/null differ diff --git a/data/icons/24/com.github.louis77.tuner.png b/data/icons/24/com.github.louis77.tuner.png deleted file mode 100644 index 8f73574..0000000 Binary files a/data/icons/24/com.github.louis77.tuner.png and /dev/null differ diff --git a/data/icons/24/io.github.louis77.tuner-symbolic.png b/data/icons/24/io.github.louis77.tuner-symbolic.png new file mode 100644 index 0000000..7d5765a Binary files /dev/null and b/data/icons/24/io.github.louis77.tuner-symbolic.png differ diff --git a/data/icons/24/io.github.louis77.tuner-symbolic.svg b/data/icons/24/io.github.louis77.tuner-symbolic.svg new file mode 100644 index 0000000..5eb6ca4 --- /dev/null +++ b/data/icons/24/io.github.louis77.tuner-symbolic.svg @@ -0,0 +1,501 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/24/io.github.louis77.tuner.png b/data/icons/24/io.github.louis77.tuner.png new file mode 100644 index 0000000..442e354 Binary files /dev/null and b/data/icons/24/io.github.louis77.tuner.png differ diff --git a/data/icons/24/com.github.louis77.tuner.svg b/data/icons/24/io.github.louis77.tuner.svg similarity index 74% rename from data/icons/24/com.github.louis77.tuner.svg rename to data/icons/24/io.github.louis77.tuner.svg index 5b83f33..0ada15a 100644 --- a/data/icons/24/com.github.louis77.tuner.svg +++ b/data/icons/24/io.github.louis77.tuner.svg @@ -1,12 +1,5 @@ image/svg+xml - - - - - - - - - - - - - - - - - - - - - + transform="matrix(0.99679598,0,0,1.0018267,-0.01136237,-2.0274002)"> + + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#7e8087;stroke-width:0.3352;stroke-miterlimit:10" /> + style="fill:none;stroke:#de3e80;stroke-width:0.4108;stroke-miterlimit:10" /> @@ -416,4 +346,4 @@ style="fill:#0e141f" /> - \ No newline at end of file + diff --git a/data/icons/32/com.github.louis77.tuner-symbolic.png b/data/icons/32/com.github.louis77.tuner-symbolic.png deleted file mode 100644 index 477fd7e..0000000 Binary files a/data/icons/32/com.github.louis77.tuner-symbolic.png and /dev/null differ diff --git a/data/icons/32/com.github.louis77.tuner.png b/data/icons/32/com.github.louis77.tuner.png deleted file mode 100644 index a765a68..0000000 Binary files a/data/icons/32/com.github.louis77.tuner.png and /dev/null differ diff --git a/data/icons/32/io.github.louis77.tuner-symbolic.png b/data/icons/32/io.github.louis77.tuner-symbolic.png new file mode 100644 index 0000000..1edeb42 Binary files /dev/null and b/data/icons/32/io.github.louis77.tuner-symbolic.png differ diff --git a/data/icons/32/io.github.louis77.tuner-symbolic.svg b/data/icons/32/io.github.louis77.tuner-symbolic.svg new file mode 100644 index 0000000..27b90ef --- /dev/null +++ b/data/icons/32/io.github.louis77.tuner-symbolic.svg @@ -0,0 +1,501 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/32/io.github.louis77.tuner.png b/data/icons/32/io.github.louis77.tuner.png new file mode 100644 index 0000000..dd111cb Binary files /dev/null and b/data/icons/32/io.github.louis77.tuner.png differ diff --git a/data/icons/32/com.github.louis77.tuner.svg b/data/icons/32/io.github.louis77.tuner.svg similarity index 84% rename from data/icons/32/com.github.louis77.tuner.svg rename to data/icons/32/io.github.louis77.tuner.svg index 72967dd..992f23b 100644 --- a/data/icons/32/com.github.louis77.tuner.svg +++ b/data/icons/32/io.github.louis77.tuner.svg @@ -1,12 +1,5 @@ image/svg+xml - - - - - - - - - - - - - - - - - - - - - + transform="matrix(0.96980569,0,0,1.0000914,0.52927299,-2.5017727)"> + + style="fill:url(#SVGID_8_);stroke:#abacae;stroke-width:0.1686;stroke-linecap:round;stroke-linejoin:round" /> - \ No newline at end of file + diff --git a/data/icons/48/com.github.louis77.tuner-symbolic.png b/data/icons/48/com.github.louis77.tuner-symbolic.png deleted file mode 100644 index ec7872d..0000000 Binary files a/data/icons/48/com.github.louis77.tuner-symbolic.png and /dev/null differ diff --git a/data/icons/48/com.github.louis77.tuner.png b/data/icons/48/com.github.louis77.tuner.png deleted file mode 100644 index 636475c..0000000 Binary files a/data/icons/48/com.github.louis77.tuner.png and /dev/null differ diff --git a/data/icons/48/io.github.louis77.tuner-jukebox.svg b/data/icons/48/io.github.louis77.tuner-jukebox.svg new file mode 100644 index 0000000..95d2aa9 --- /dev/null +++ b/data/icons/48/io.github.louis77.tuner-jukebox.svg @@ -0,0 +1,655 @@ + + + + + Jukebox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jukebox + 2024-12-15 + + + technosf <https://github.com/technosf> + + + + + Copyright © 2024 technosf <https://github.com/technosf> + + + https://github.com/louis77/tuner + Jukebox icon for Tuner app + + + + + + + + + + + + diff --git a/data/icons/48/io.github.louis77.tuner-symbolic.png b/data/icons/48/io.github.louis77.tuner-symbolic.png new file mode 100644 index 0000000..b4fa8ad Binary files /dev/null and b/data/icons/48/io.github.louis77.tuner-symbolic.png differ diff --git a/data/icons/48/io.github.louis77.tuner-symbolic.svg b/data/icons/48/io.github.louis77.tuner-symbolic.svg new file mode 100644 index 0000000..3a857bd --- /dev/null +++ b/data/icons/48/io.github.louis77.tuner-symbolic.svg @@ -0,0 +1,500 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/48/io.github.louis77.tuner.png b/data/icons/48/io.github.louis77.tuner.png new file mode 100644 index 0000000..87c338f Binary files /dev/null and b/data/icons/48/io.github.louis77.tuner.png differ diff --git a/data/icons/48/com.github.louis77.tuner.svg b/data/icons/48/io.github.louis77.tuner.svg similarity index 79% rename from data/icons/48/com.github.louis77.tuner.svg rename to data/icons/48/io.github.louis77.tuner.svg index 9dad66f..0ce3bc1 100644 --- a/data/icons/48/com.github.louis77.tuner.svg +++ b/data/icons/48/io.github.louis77.tuner.svg @@ -1,12 +1,5 @@ image/svg+xml \ No newline at end of file + diff --git a/data/icons/64/com.github.louis77.tuner-symbolic.png b/data/icons/64/com.github.louis77.tuner-symbolic.png deleted file mode 100644 index 41a5aed..0000000 Binary files a/data/icons/64/com.github.louis77.tuner-symbolic.png and /dev/null differ diff --git a/data/icons/64/com.github.louis77.tuner.png b/data/icons/64/com.github.louis77.tuner.png deleted file mode 100644 index 148a4ae..0000000 Binary files a/data/icons/64/com.github.louis77.tuner.png and /dev/null differ diff --git a/data/icons/64/io.github.louis77.tuner-symbolic.png b/data/icons/64/io.github.louis77.tuner-symbolic.png new file mode 100644 index 0000000..352711b Binary files /dev/null and b/data/icons/64/io.github.louis77.tuner-symbolic.png differ diff --git a/data/icons/64/io.github.louis77.tuner-symbolic.svg b/data/icons/64/io.github.louis77.tuner-symbolic.svg new file mode 100644 index 0000000..ae12f98 --- /dev/null +++ b/data/icons/64/io.github.louis77.tuner-symbolic.svg @@ -0,0 +1,499 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/64/io.github.louis77.tuner.png b/data/icons/64/io.github.louis77.tuner.png new file mode 100644 index 0000000..dfb8af3 Binary files /dev/null and b/data/icons/64/io.github.louis77.tuner.png differ diff --git a/data/icons/64/com.github.louis77.tuner.svg b/data/icons/64/io.github.louis77.tuner.svg similarity index 77% rename from data/icons/64/com.github.louis77.tuner.svg rename to data/icons/64/io.github.louis77.tuner.svg index dc5454f..6be78e8 100644 --- a/data/icons/64/com.github.louis77.tuner.svg +++ b/data/icons/64/io.github.louis77.tuner.svg @@ -1,12 +1,5 @@ image/svg+xml - - - - - - - - - - - - - - - - - - - - - + transform="matrix(0.99827915,0,0,0.99999956,0.00559248,-5.9999825)"> + + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#abacae;stroke-width:0.8938;stroke-miterlimit:10" /> + style="fill:none;stroke:#de3e80;stroke-width:1.0954;stroke-miterlimit:10" /> @@ -368,7 +298,7 @@ cy="49" r="7" id="circle992" - style="fill:#273445;stroke:#de3e80;stroke-width:0.45680001;stroke-linecap:round;stroke-linejoin:round" /> + style="fill:#273445;stroke:#de3e80;stroke-width:0.4568;stroke-linecap:round;stroke-linejoin:round" /> + style="fill:url(#SVGID_8_);stroke:#abacae;stroke-width:0.3279;stroke-linecap:round;stroke-linejoin:round" /> + style="fill:#d4d4d4;stroke:#abacae;stroke-width:0.1334" /> @@ -599,4 +529,4 @@ - \ No newline at end of file + diff --git a/data/com.github.louis77.tuner.appdata.xml.in b/data/io.github.louis77.tuner.appdata.xml.in similarity index 76% rename from data/com.github.louis77.tuner.appdata.xml.in rename to data/io.github.louis77.tuner.appdata.xml.in index a5b3a95..b2c39f8 100644 --- a/data/com.github.louis77.tuner.appdata.xml.in +++ b/data/io.github.louis77.tuner.appdata.xml.in @@ -1,18 +1,22 @@ - + - com.github.louis77.tuner + io.github.louis77.tuner FSFAP tuner-elementary GPL-3.0+ Tuner Louis Brauer - com.github.louis77.tuner.desktop - Discover and listen to internet radio stations - + technosf + io.github.louis77.tuner.desktop + + #fafafa + #abacae + + Internet Radio -

Make listening to internet radio stations fun again!

-

Instead of providing you with all the stations you already know, +

Make finding and listening to internet radio stations fun again!

+

Instead of showing all the stations you already know, Tuner presents you a new selection of stations from all over the world every time you hit the Shuffle button.

Tuner uses the community-driven station catalog radio-browser.info.

@@ -25,17 +29,40 @@ - https://raw.githubusercontent.com/louis77/tuner/master/docs/screen_light_1.4.2.png + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_discover.png + Tuner with dark theme, discovery + + + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_starred.png + Tuner with light theme, starred stations + + + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_search.png + Tuner search for stations + + + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_saved.png + Tuner saved searches + + + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_meta.png + Tuner playing station metadata - https://raw.githubusercontent.com/louis77/tuner/master/docs/screen_light-2_1.4.2.png + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_pref.png + Tuner preferences - https://raw.githubusercontent.com/louis77/tuner/master/docs/screen_dark_1.4.2.png + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_context.png + Tuner station context information + + + https://raw.githubusercontent.com/louis77/tuner/master/docs/Tuner_2.0_jukebox.png + Tuner jukebox station shuffle - louis_AT_brauer.family + tuner.10.technomation_AT_xoxy.net https://github.com/louis77/tuner http://github.com/louis77/tuner/issues http://github.com/louis77/tuner/issues @@ -56,23 +83,37 @@ - com.github.louis77.tuner + io.github.louis77.tuner org.mpris.MediaPlayer2 org.mpris.MediaPlayer2.Player + + com.github.louis77.tuner + + - + -

Intermediate release prior to new Version:

+

New Version!

    -
  • Starred Stations can be exported to an M3U file from the Preferences menu
  • -
  • Export your starred stations prior to upgrading to forthcoming v2 release
  • -
  • Starred Stations and other settings need to be reapplied to v2 due to breaking changes
  • +
  • Saved Searches - new feature
  • +
  • Explore Genres - new feature
  • +
  • Jukebox Shuffle - Play Random Station - new feature
  • +
  • New Default Genres added
  • +
  • Updated look and feel
  • +
  • Station and Stream History - new feature
  • +
  • More Station and Stream Metadata - new feature
  • +
  • Save, import Starred Stations to playlists - new feature
  • +
  • Faster responsiveness and image loading
  • +
  • New data provider API, plus better support for Radio Browser
  • +
  • Refactored for separation of concerns, OO, maintainability
  • +
  • Renamed, updated icons, description and screenshots per Flathub quality directives
-
+
+ https://github.com/louis77/tuner/releases/tag/v1.5.5

Maintanance release:

    @@ -80,7 +121,6 @@
  • Increased Search re-fire interval to reduce number of concurrent searches
  • Made Search asynchronous so UI remains responsive during searches
  • Ignore request to load failing favicons URLs
  • -
  • Removed shadow from.svg images per Flathub quality directives
  • Removed org.gnome.SettingsDaemon.MediaKeys from manifest per flathub
diff --git a/data/com.github.louis77.tuner.desktop.in b/data/io.github.louis77.tuner.desktop.in similarity index 80% rename from data/com.github.louis77.tuner.desktop.in rename to data/io.github.louis77.tuner.desktop.in index 36a9e17..3ae7b15 100644 --- a/data/com.github.louis77.tuner.desktop.in +++ b/data/io.github.louis77.tuner.desktop.in @@ -4,7 +4,7 @@ Name=Tuner GenericName=Internet Radio Player Comment=Listen to Radio Stations all over the world Categories=AudioVideo;Audio;Video;Tuner;Player;Music; -Exec=com.github.louis77.tuner -Icon=com.github.louis77.tuner +Exec=io.github.louis77.tuner +Icon=io.github.louis77.tuner Terminal=false Keywords=Radio;Audio;Listen;Stations;Receiver; diff --git a/data/io.github.louis77.tuner.gresource b/data/io.github.louis77.tuner.gresource new file mode 100644 index 0000000..4973a52 Binary files /dev/null and b/data/io.github.louis77.tuner.gresource differ diff --git a/data/io.github.louis77.tuner.gresource.xml b/data/io.github.louis77.tuner.gresource.xml new file mode 100644 index 0000000..f1e7f51 --- /dev/null +++ b/data/io.github.louis77.tuner.gresource.xml @@ -0,0 +1,23 @@ + + + + css/Tuner-system.css + css/Tuner-light.css + css/Tuner-dark.css + + + icons/16/playlist-queue.svg + icons/16/playlist-similar.svg + icons/16/playlist.svg + icons/16/playlist-symbolic.svg + icons/16/internet-radio-symbolic.svg + icons/16/internet-radio.svg + icons/48/io.github.louis77.tuner.svg + icons/48/io.github.louis77.tuner-symbolic.svg + icons/48/io.github.louis77.tuner-jukebox.svg + icons/128/io.github.louis77.tuner.svg + icons/128/io.github.louis77.tuner-jukebox.svg + icons/128/io.github.louis77.tuner.svg + + + diff --git a/data/com.github.louis77.tuner.gschema.xml b/data/io.github.louis77.tuner.gschema.xml similarity index 67% rename from data/com.github.louis77.tuner.gschema.xml rename to data/io.github.louis77.tuner.gschema.xml index a1cca9f..267ef27 100644 --- a/data/com.github.louis77.tuner.gschema.xml +++ b/data/io.github.louis77.tuner.gschema.xml @@ -1,14 +1,14 @@ - + - 360 + 0 Horizontal position Saved horizontal position of main window - 640 + 0 Vertical position Saved vertical position of main window @@ -22,16 +22,26 @@ Window height Saved height of main window - - [] - Starred stations - All station starred by the user + + false + Do not participate in Station voting + If enabled, do not send voting and popularity information to metadata provider - + false - Do not track - If enabled, do not send usage information to radio-browser.org + Open to the Starred Stations view + If enabled, opens to the starred stations view + + true + Show stream info when playing + Cycle through the metadata from the playing stream + + + false + Faster cycling through stream info + Fast cycle through the metadata from the playing stream if show stream info is enabled + [] Excluded Countries @@ -53,7 +63,7 @@ Station UUID of station the was last played. Used for auto-play - 1.0 + 0.5 Volume control The last volume setting. Value from 0.0 to 1.0 diff --git a/docs/Tuner_2.0_context.png b/docs/Tuner_2.0_context.png new file mode 100644 index 0000000..04992e3 Binary files /dev/null and b/docs/Tuner_2.0_context.png differ diff --git a/docs/Tuner_2.0_discover.png b/docs/Tuner_2.0_discover.png new file mode 100644 index 0000000..e29e8e6 Binary files /dev/null and b/docs/Tuner_2.0_discover.png differ diff --git a/docs/Tuner_2.0_jukebox.png b/docs/Tuner_2.0_jukebox.png new file mode 100644 index 0000000..f83b0b8 Binary files /dev/null and b/docs/Tuner_2.0_jukebox.png differ diff --git a/docs/Tuner_2.0_meta.png b/docs/Tuner_2.0_meta.png new file mode 100644 index 0000000..10ed91e Binary files /dev/null and b/docs/Tuner_2.0_meta.png differ diff --git a/docs/Tuner_2.0_pref.png b/docs/Tuner_2.0_pref.png new file mode 100644 index 0000000..61e10c3 Binary files /dev/null and b/docs/Tuner_2.0_pref.png differ diff --git a/docs/Tuner_2.0_saved.png b/docs/Tuner_2.0_saved.png new file mode 100644 index 0000000..75a74c6 Binary files /dev/null and b/docs/Tuner_2.0_saved.png differ diff --git a/docs/Tuner_2.0_search.png b/docs/Tuner_2.0_search.png new file mode 100644 index 0000000..6d68836 Binary files /dev/null and b/docs/Tuner_2.0_search.png differ diff --git a/docs/Tuner_2.0_starred.png b/docs/Tuner_2.0_starred.png new file mode 100644 index 0000000..8c67e05 Binary files /dev/null and b/docs/Tuner_2.0_starred.png differ diff --git a/docs/logo_01.png b/docs/logo_01.png index b97f587..4a9198f 100644 Binary files a/docs/logo_01.png and b/docs/logo_01.png differ diff --git a/docs/org-mpris.vala b/docs/org-mpris.vala index be042b2..6c3986a 100644 --- a/docs/org-mpris.vala +++ b/docs/org-mpris.vala @@ -1,3 +1,11 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file org-mpris.vala + * @brief + */ + /* Generated by vala-dbus-binding-tool 0.4.0. Do not modify! */ /* Generated with: vala-dbus-binding-tool --api-path=../data --directory=../src */ using DBus; diff --git a/docs/screen_dark_1.4.2.png b/docs/screen_dark_1.4.2.png deleted file mode 100644 index 326334b..0000000 Binary files a/docs/screen_dark_1.4.2.png and /dev/null differ diff --git a/docs/screen_light-2_1.4.2.png b/docs/screen_light-2_1.4.2.png deleted file mode 100644 index 7099a33..0000000 Binary files a/docs/screen_light-2_1.4.2.png and /dev/null differ diff --git a/docs/screen_light_1.4.2.png b/docs/screen_light_1.4.2.png deleted file mode 100644 index f583166..0000000 Binary files a/docs/screen_light_1.4.2.png and /dev/null differ diff --git a/io.github.louis77.tuner.yml b/io.github.louis77.tuner.yml new file mode 100644 index 0000000..545ed5d --- /dev/null +++ b/io.github.louis77.tuner.yml @@ -0,0 +1,117 @@ +--- +app-id: io.github.louis77.tuner + +runtime: org.freedesktop.Platform +runtime-version: '24.08' + +sdk: org.freedesktop.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.vala + +#base: io.elementary.BaseApp +#base-version: 'circe-24.08' + +command: io.github.louis77.tuner + +build-options: + prepend-path: /usr/lib/sdk/vala/bin/ + prepend-ld-library-path: /usr/lib/sdk/vala/lib + +add-extensions: + org.freedesktop.Sdk.Extension.vala: + version: '24.08' + directory: vala + autodelete: true + +finish-args: + - "--share=ipc" + - "--socket=fallback-x11" + - "--socket=wayland" + - "--talk-name=org.gtk.vfs.*" + - "--filesystem=xdg-run/gvfsd" + - "--filesystem=xdg-desktop" + - "--share=network" + - "--metadata=X-DConf=migrate-path=/com/github/louis77/tuner/" + - "--socket=pulseaudio" + - "--talk-name=org.freedesktop.Notifications" + - "--own-name=org.mpris.MediaPlayer2.Tuner" + - "--env=PATH=/usr/bin:/app/bin:/app/vala/bin" + - "--env=LD_LIBRARY_PATH=/app/vala/lib:/app/lib:/lib" + +cleanup: + - "/include" + - "/lib/pkgconfig" + - "/share/pkgconfig" + - "/share/aclocal" + - "/man" + - "/share/man" + - "/share/gtk-doc" + - "/share/vala" + - "*.la" + - "*.a" + +modules: +- name: vala + buildsystem: simple + build-commands: + - install -d /app/vala + +- name: libsoup + buildsystem: meson + builddir: true + config-opts: + - "--buildtype=release" + - "-Dvapi=enabled" + - "-Dtests=false" + - "-Dgssapi=disabled" + - "-Dsysprof=disabled" + sources: + - type: git + url: https://gitlab.gnome.org/GNOME/libsoup.git + tag: 3.6.1 + commit: 8b46a93bc1cbadb22dcdbb6844d9616723376535 + +- name: taglib + buildsystem: cmake-ninja + config-opts: + - "-DBUILD_SHARED_LIBS=ON" + - "-DCMAKE_BUILD_TYPE=Release" + sources: + - type: git + url: https://github.com/taglib/taglib.git + tag: v2.0.2 + commit: e3de03501ff66221d1f1f971022b248d5b38ba06 + +- name: gee + buildsystem: autotools + build-options: + cflags: -Wno-error=incompatible-pointer-types + cleanup: ["*"] + sources: + - type: archive + url: https://download.gnome.org/sources/libgee/0.20/libgee-0.20.6.tar.xz + sha256: 1bf834f5e10d60cc6124d74ed3c1dd38da646787fbf7872220b8b4068e476d4d + +- name: granite + buildsystem: meson + config-opts: + - "--buildtype=release" + sources: + - type: archive + url: https://github.com/elementary/granite/archive/refs/tags/6.2.0.tar.gz + sha256: 067d31445da9808a802fca523630c3e4b84d2d7c78ae547ced017cb7f3b9c6b5 + +- name: tuner + buildsystem: meson + build-options: + cflags: "-g" + cxxflags: "-g" + buildtype: "debug" + config-opts: + - "--buildtype=debug" + + post-install: + - install -Dm644 /app/share/icons/hicolor/64x64/apps/${FLATPAK_ID}.svg -t /app/share/icons/hicolor/128x128/apps/ + sources: + - type: dir + path: . diff --git a/com.github.louis77.tuner.yml.debug b/io.github.louis77.tuner.yml.debug similarity index 94% rename from com.github.louis77.tuner.yml.debug rename to io.github.louis77.tuner.yml.debug index 46f17eb..37441bc 100644 --- a/com.github.louis77.tuner.yml.debug +++ b/io.github.louis77.tuner.yml.debug @@ -1,9 +1,9 @@ --- -app-id: com.github.louis77.tuner +app-id: io.github.louis77.tuner runtime: io.elementary.Platform runtime-version: '8' sdk: io.elementary.Sdk -command: com.github.louis77.tuner +command: io.github.louis77.tuner finish-args: - "--share=ipc" - "--socket=fallback-x11" diff --git a/com.github.louis77.tuner.yml b/io.github.louis77.tuner.yml.release similarity index 94% rename from com.github.louis77.tuner.yml rename to io.github.louis77.tuner.yml.release index 1ef172b..a46eef0 100644 --- a/com.github.louis77.tuner.yml +++ b/io.github.louis77.tuner.yml.release @@ -1,9 +1,9 @@ --- -app-id: com.github.louis77.tuner +app-id: io.github.louis77.tuner runtime: io.elementary.Platform runtime-version: '8' sdk: io.elementary.Sdk -command: com.github.louis77.tuner +command: io.github.louis77.tuner finish-args: - "--share=ipc" - "--socket=fallback-x11" diff --git a/meson.build b/meson.build index c771a7e..fdd1f13 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,8 @@ # Project name, programming language and version project ( - 'com.github.louis77.tuner', + 'io.github.louis77.tuner', 'vala', 'c', - version: '1.5.6', + version: '2.0.0-BETA1', meson_version: '>= 0.60.0', ) diff --git a/po/de.po b/po/de.po index 9054024..7f1784e 100644 --- a/po/de.po +++ b/po/de.po @@ -1,6 +1,6 @@ -# German translations for com.github.louis77.tuner package. -# Copyright (C) 2020 THE com.github.louis77.tuner'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# German translations for io.github.louis77.tuner package. +# Copyright (C) 2020 THE io.github.louis77.tuner'S COPYRIGHT HOLDER +# This file is distributed under the same license as the io.github.louis77.tuner package. # Automatically generated, 2020. # # Louis Brauer , 2022. @@ -9,7 +9,7 @@ # uwe-ss , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-10-03 20:19+0000\n" diff --git a/po/es.po b/po/es.po index 8e7c63f..a234303 100644 --- a/po/es.po +++ b/po/es.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# This file is distributed under the same license as the io.github.louis77.tuner package. # Paxton , 2022. # Gaston Martres , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-07-15 11:28+0000\n" diff --git a/po/extra/POTFILES b/po/extra/POTFILES index 2b07968..979ba49 100644 --- a/po/extra/POTFILES +++ b/po/extra/POTFILES @@ -1,2 +1,2 @@ -data/com.github.louis77.tuner.desktop.in -data/com.github.louis77.tuner.appdata.xml.in +data/io.github.louis77.tuner.desktop.in +data/io.github.louis77.tuner.appdata.xml.in diff --git a/po/extra/de.po b/po/extra/de.po index b3698ae..2357e96 100644 --- a/po/extra/de.po +++ b/po/extra/de.po @@ -17,41 +17,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "Tuner" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 #, fuzzy msgid "Internet Radio Player" msgstr "Internet Radio Empfänger" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "Empfange Radio-Stationen aus aller Welt" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" -msgstr "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" +msgstr "io.github.louis77.tuner" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "Radio;Audio;Listen;Stations;Receiver;" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "Louis Brauer" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "Entdecke und empfange Internet Radio-Stationen" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "Endlich wieder Spass beim Empfangen von Internet Radio-Stationen!" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " @@ -61,81 +61,81 @@ msgstr "" "zeigt Dir Tuner bei jedem Start eine neue Auswahl von Stationen aus aller " "Welt. Drücke einfach den Mischen-Button um eine neue Auswahl zu erhalten." -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" "Tuner benutzt das offene Community-Radio-Verzeichnis von radio-browser.info" -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "Entdecke jeden Tag neue Radio-Stationen" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 #, fuzzy msgid "Star stations you like and visit their website" msgstr "Markiere deine Favoriten" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "Steuere Tuner von deinem System-Lautstärkeregler" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "Neue Funktionen:" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "Sonstige Verbesserungen:" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 #, fuzzy msgid "Added Dutch translation" msgstr "Französisch-Übersetzung hinzugefügt" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "Fehlerbehebungen:" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" "Klicken Sie mit der rechten Maustaste auf eine Station, um sie direkt zu " "Favoriten hinzuzufügen oder daraus zu entfernen" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" @@ -143,11 +143,11 @@ msgstr "" "Einstellungsmenü mit der Option \"Nicht verfolgen\" hinzugefügt um das " "Senden von Ereignissen an radio-browser.org zu verhindern" -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "Über Dialog hinzugefügt " -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" @@ -155,7 +155,7 @@ msgstr "" "Wenn sich ein Sender bereits in Ihren Favoriten befindet, wird im Titel ein " "kleiner Stern angezeigt" -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" @@ -163,7 +163,7 @@ msgstr "" "Wählt zufällig einen der verfügbaren radio-browser.org-Server aus (war " "vorher immer de1)" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" @@ -171,64 +171,64 @@ msgstr "" "Favoriten werden jetzt in einer lokalen Datei favourites.json gespeichert, " "um die Startzeit der App zu verbessern" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 #, fuzzy msgid "Added Italian translation" msgstr "Italienisch-Übersetzung hinzugefügt" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" "Behebung der Unterstützung für defekte dunkle Themen (elementary und Adwaita " "Dark sehen jetzt gut aus)" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "Suche nach Radio-Stationen" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "Neuer \"Genre\"-Abschnitt mit ausgewählten Genre" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "Französisch-Übersetzung hinzugefügt" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "Deutsch-Übersetzung hinzugefügt" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "Jeder Abschnitt zeigt" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "Stations-Bilder werden nun zwischengespeichert" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "Das App-Icon ist nun vertikal ausgerichtet" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "Stations-Bilder haben nun eine konsistente Grösse" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" "Einen Fehler behoben, bei dem markierte Stationen als nicht markiert " "angezeigt wurden" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "Zeige Hinweis falls Datenabruf fehlschlägt" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "Neue Seitenleiste mit verschiedenen Vorauswahlen und deinen Favoriten" -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "Erste Ausgabe" diff --git a/po/extra/extra.pot b/po/extra/extra.pot index d8e7423..fc6e77e 100644 --- a/po/extra/extra.pot +++ b/po/extra/extra.pot @@ -17,196 +17,196 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 msgid "Internet Radio Player" msgstr "" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" msgstr "" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " "you hit the Shuffle button." msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 msgid "Star stations you like and visit their website" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 msgid "Added Dutch translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 msgid "Added Italian translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "" diff --git a/po/extra/fr.po b/po/extra/fr.po index cea4187..44abe19 100644 --- a/po/extra/fr.po +++ b/po/extra/fr.po @@ -17,40 +17,40 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "Tuner" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 msgid "Internet Radio Player" msgstr "Lecteur de Web Radios" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "Écoutez les stations de radio du monde entier" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" -msgstr "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" +msgstr "io.github.louis77.tuner" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "Radio;Audio;Listen;Stations;Receiver;" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "Louis Brauer" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "Découvrez et écoutez des stations de radio en ligne" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "Rendons de nouveau fun l'écoute des stations de radio en ligne !" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " @@ -60,79 +60,79 @@ msgstr "" "Tuner vous présente une nouvelle sélection de stations du monde entier à " "chaque fois que vous appuyez sur le bouton « Aléatoire »." -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" "Tuner utilise le catalogue communautaire de stations radio-browser.info." -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "Découvrez de nouvelles stations chaque jour" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 msgid "Star stations you like and visit their website" msgstr "Marquez les stations que vous aimez comme favorites et visitez leur site Web" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "Contrôlez la lecture de Tuner depuis votre indicateur de volume" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "Nouvelles fonctionnalités" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "Affichage de la piste en cours de lecture lorsque cela est pris en charge par la station" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "Affichage du top 100 des stations du pays de l'utilisateur si détectable" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "Ajout de l'option « Visiter le site Web » dans le menu contextuel des stations" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "Autres améliorations :" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 msgid "Added Dutch translation" msgstr "Ajout de la traduction en néerlandais" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "Corrections de bug :" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "Correction des icônes manquantes pour les systèmes différents d'elementary OS" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "Ajout de l'option pour basculer au thème sombre dans les paramètres" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "Barre latérale compacte et fenêtre de contenu" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" "Cliquez avec le bouton droit sur une station pour ajouter ou supprimer " "directement les favoris" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" @@ -140,11 +140,11 @@ msgstr "" "Ajout de l'option « Ne pas suivre », désactive " "l'envoi des événements d'écoute de la station à radio-browser.org" -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "Ajout de la boîte de dialogue « À propos »" -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" @@ -152,7 +152,7 @@ msgstr "" "Si une station est déjà dans vos favoris, vous verrez une petite étoile dans " "le titre" -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" @@ -160,7 +160,7 @@ msgstr "" "Sélectionne au hasard l'un des serveurs radio-browser.org disponibles (était " "toujours de1 avant)" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" @@ -168,65 +168,65 @@ msgstr "" "Les favoris sont désormais stockés dans un fichier local favourites.json " "pour améliorer le temps de démarrage de l'application" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 msgid "Added Italian translation" msgstr "Ajout de la traduction en italien" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" "Correction de la prise en charge du thème sombre cassé (elementary et " "Adwaita theme semblent bien maintenant)" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "Recherchez des stations de radio" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "Nouvelle section « Genres » avec une sélection de genres populaires" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "Ajout de la traduction en français" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "Ajout de le traduction en allemand" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "Chaque section affiche désormais les 40 stations les plus votées" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "Les images de station sont désormais mises en cache" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "L'icône de l'application est désormais mieux centrée verticalement" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "Les images de station sont désormais toujours de la même taille" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" "Correction d'un bug où les stations marquées comme favorites " "n'apparaissaient pas comme telles" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "Affichage d'un un bel écran d'erreur en cas d'erreur de l'API" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "" "Nouveau menu dans la barre latérale avec différentes sélections et vos " "stations favorites." -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "Version initiale" diff --git a/po/extra/it.po b/po/extra/it.po index 07f8437..5a81806 100644 --- a/po/extra/it.po +++ b/po/extra/it.po @@ -17,40 +17,40 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "Tuner" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 msgid "Internet Radio Player" msgstr "Lettore diInternet Radio" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "Ascolta le stazioni radio di tutto il mondo" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" -msgstr "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" +msgstr "io.github.louis77.tuner" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "Radio;Audio;Listen;Stations;Receiver;" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "Louis Brauer" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "Scopri e ascolta le stazioni radio Internet" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "Rendi di nuovo divertente l'ascolto delle stazioni radio Internet!" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " @@ -60,81 +60,81 @@ msgstr "" "nuova selezione di stazioni da tutto il mondo ogni volta che premi il " "pulsante Shuffle." -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" "Il sintonizzatore utilizza il catalogo delle stazioni gestito dalla comunità " "radio-browser.info." -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "Scopri nuove stazioni ogni giorno" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 #, fuzzy msgid "Star stations you like and visit their website" msgstr "Aggiungi ai preferiti le stazioni che ti piacciono e visita il loro sito web" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "Gestisci Tuner dall'indicatore del volume" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "Nuove caratteristiche:" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "Mostra la traccia corrente in riproduzione se supportata dalla stazione" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "Mostra le prime 100 stazioni del paese dell'utente, se rilevabili" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "\"Visita sito web\" link nel menu contestuale della stazione" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "Altri miglioramenti:" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 msgid "Added Dutch translation" msgstr "Aggiunta traduzione in olandese" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "bugfix:" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "Corretto le icone mancanti per i sistemi operativi non elemtary OS" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "Aggiunto l'interruttore manuale della modalità oscura nelle impostazioni" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "Barra laterale compatta e finestra dei contenuti" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" "Fare clic con il pulsante destro del mouse su una stazione per aggiungere o " "rimuovere direttamente dai preferiti" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" @@ -142,11 +142,11 @@ msgstr "" "Aggiunto il menu delle impostazioni con l'opzione \"Do-Not-Track\", " "disabilita l'invio di eventi di ascolto della stazione a radio-browser.org" -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "Aggiunta finestra di dialogo informazioni" -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" @@ -154,7 +154,7 @@ msgstr "" "Se una stazione è già nei tuoi preferiti, vedrai una piccola stella nel " "titolo" -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" @@ -162,7 +162,7 @@ msgstr "" "Seleziona casualmente uno dei server radio-browser.org disponibili (prima " "era sempre de1)" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" @@ -170,65 +170,65 @@ msgstr "" "I preferiti sono ora archiviati in un file favourites.json locale per " "migliorare il tempo di avvio dell'app" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 msgid "Added Italian translation" msgstr "Aggiunta traduzione in italiano" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" "Risolto il problema con il supporto del tema scuro rotto (elementary e " "Adwaita Dark sembrano a posto ora)" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "Cerca stazioni radio" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "Nuova sezione \"Generi\" con generi popolari selezionati" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "Aggiunta traduzione in francese" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "Aggiunta traduzione in tedesco" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "Ogni sezione ora mostra le 40 stazioni più votate" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "Le immagini delle stazioni radio ora vengono salvate nella cache" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "L'icona dell'app ora appare più centrata verticalmente" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "Le immagini delle stazioni radio ora hanno sempre le stesse dimensioni" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" "Risolto un bug in cui le stazioni contrassegnate apparivano come non " "contrassegnate" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "Mostra una bella schermata di errore API" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "" "Nuovo menu della barra laterale con diverse selezioni e le tue stazioni " "preferite." -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "Versione iniziale" diff --git a/po/extra/nl.po b/po/extra/nl.po index 7db5c40..a702a19 100644 --- a/po/extra/nl.po +++ b/po/extra/nl.po @@ -18,40 +18,40 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.0\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "Tuner" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 msgid "Internet Radio Player" msgstr "Internetradiospeler" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "Luister naar radiostations van over heel de wereld" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" -msgstr "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" +msgstr "io.github.louis77.tuner" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "Radio;Audio;Luisteren;Stations;Ontvanger;" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "Louis Brauer" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "Ontdek en luister naar online-radiostations" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "Maak luisteren naar online-radiostreams weer leuk!" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " @@ -61,159 +61,159 @@ msgstr "" "selectie van radiostations van over heel de wereld, telkens als je op de " "knop 'Willekeurig' klikt." -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" "Tuner maakt gebruik van de door de gemeenschap bijgehouden catalogus op " "radio-browser.info" -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "Ontdek elke dag nieuwe radiostations" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 msgid "Star stations you like and visit their website" msgstr "Voeg radiostations toe aan je favorieten en bezoek hun websites" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "Bedien Tuner via de volume-indicator" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "Nieuwe mogelijkheden:" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "" "Toon het momenteel spelende nummer (indien ondersteund door radiostation)" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "Bekijk de top 100 van radiostations in eigen land (indien mogelijk)" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "‘Website openen’ toegevoegd aan het rechtermuisknopmenu" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "Andere verbeteringen:" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 msgid "Added Dutch translation" msgstr "Nederlandse vertaling" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "Opgeloste fouten:" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 msgid "Added Italian translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "Initiële uitgave" diff --git a/po/extra/pt_BR.po b/po/extra/pt_BR.po index 562cccc..7e8efda 100644 --- a/po/extra/pt_BR.po +++ b/po/extra/pt_BR.po @@ -18,41 +18,41 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Language: pt_BR\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "Tuner" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 msgid "Internet Radio Player" msgstr "Reprodutor de rádio na Internet" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "Ouça estações de rádio de todo o mundo" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" -msgstr "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" +msgstr "io.github.louis77.tuner" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "" "Radio;Audio;Listen;Stations;Receiver;Rádio;Áudio;Ouvir;Estações;Receptor" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "Louis Brauer" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "Descubra e ouça estações de rádio na Internet" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "Torne a escuta de estações de rádio na Internet divertida novamente!" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " @@ -62,157 +62,157 @@ msgstr "" "uma nova seleção de estações de todo o mundo a cada vez que você pressionar " "o botão Aleatório." -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" "O Tuner usa o catálogo de estações baseado na comunidade radio-browser.info." -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "Descubra novas estações a cada dia" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 msgid "Star stations you like and visit their website" msgstr "Dê estrela a estações que você gosta e visite o site deles" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 msgid "Added Dutch translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 msgid "Added Italian translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "" diff --git a/po/extra/tr.po b/po/extra/tr.po index 6cb59d9..edba088 100644 --- a/po/extra/tr.po +++ b/po/extra/tr.po @@ -17,41 +17,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: data/com.github.louis77.tuner.desktop.in:4 -#: data/com.github.louis77.tuner.appdata.xml.in:8 +#: data/io.github.louis77.tuner.desktop.in:4 +#: data/io.github.louis77.tuner.appdata.xml.in:8 msgid "Tuner" msgstr "" -#: data/com.github.louis77.tuner.desktop.in:5 +#: data/io.github.louis77.tuner.desktop.in:5 msgid "Internet Radio Player" msgstr "İnternet Radyo İstasyonu Alıcısı" -#: data/com.github.louis77.tuner.desktop.in:6 +#: data/io.github.louis77.tuner.desktop.in:6 msgid "Listen to Radio Stations all over the world" msgstr "Tüm dünyadaki Radyo İstasyonlarını dinleyin" -#: data/com.github.louis77.tuner.desktop.in:9 -msgid "com.github.louis77.tuner" -msgstr "com.github.louis77.tuner" +#: data/io.github.louis77.tuner.desktop.in:9 +msgid "io.github.louis77.tuner" +msgstr "io.github.louis77.tuner" -#: data/com.github.louis77.tuner.desktop.in:11 +#: data/io.github.louis77.tuner.desktop.in:11 msgid "Radio;Audio;Listen;Stations;Receiver;" msgstr "Radyo; Ses; Dinle; İstasyonlar; Alıcı;" -#: data/com.github.louis77.tuner.appdata.xml.in:9 +#: data/io.github.louis77.tuner.appdata.xml.in:9 msgid "Louis Brauer" msgstr "Louis Brauer" -#: data/com.github.louis77.tuner.appdata.xml.in:11 +#: data/io.github.louis77.tuner.appdata.xml.in:11 msgid "Discover and listen to internet radio stations" msgstr "İnternet radyo istasyonlarını keşfedin ve dinleyin" -#: data/com.github.louis77.tuner.appdata.xml.in:14 +#: data/io.github.louis77.tuner.appdata.xml.in:14 msgid "Make listening to internet radio stations fun again!" msgstr "" "İnternet radyo istasyonlarını dinlemeyi yeniden eğlenceli hale getirin!" -#: data/com.github.louis77.tuner.appdata.xml.in:15 +#: data/io.github.louis77.tuner.appdata.xml.in:15 msgid "" "Instead of providing you with all the stations you already know, Tuner " "presents you a new selection of stations from all over the world every time " @@ -60,78 +60,78 @@ msgstr "" "Sizin bildiğiniz tüm istasyonları sağlamak yerine, Tunersize dünyanın her " "yerinden yeni bir istasyon seçimi sunarKarıştır düğmesine basmanız yeterli." -#: data/com.github.louis77.tuner.appdata.xml.in:18 +#: data/io.github.louis77.tuner.appdata.xml.in:18 msgid "Tuner uses the community-driven station catalog radio-browser.info." msgstr "" "Tuner, topluluk odaklı istasyon kataloğu radio-browser.info'yu kullanır." -#: data/com.github.louis77.tuner.appdata.xml.in:20 +#: data/io.github.louis77.tuner.appdata.xml.in:20 msgid "Discover new stations every day" msgstr "Her gün yeni istasyonları keşfedin" -#: data/com.github.louis77.tuner.appdata.xml.in:21 +#: data/io.github.louis77.tuner.appdata.xml.in:21 msgid "Star stations you like and visit their website" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:22 +#: data/io.github.louis77.tuner.appdata.xml.in:22 msgid "Control Tuner from your volume indicator" msgstr "Tuner'ı ses göstergenizden kontrol edin" -#: data/com.github.louis77.tuner.appdata.xml.in:64 -#: data/com.github.louis77.tuner.appdata.xml.in:95 -#: data/com.github.louis77.tuner.appdata.xml.in:116 +#: data/io.github.louis77.tuner.appdata.xml.in:64 +#: data/io.github.louis77.tuner.appdata.xml.in:95 +#: data/io.github.louis77.tuner.appdata.xml.in:116 msgid "New features:" msgstr "Yeni özellikler:" -#: data/com.github.louis77.tuner.appdata.xml.in:66 +#: data/io.github.louis77.tuner.appdata.xml.in:66 msgid "Show the current track playing if supported by station" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:67 +#: data/io.github.louis77.tuner.appdata.xml.in:67 msgid "Show Top 100 stations of user country if detectable" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:68 +#: data/io.github.louis77.tuner.appdata.xml.in:68 msgid "\"Visit Website\" link in station context menu" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:70 -#: data/com.github.louis77.tuner.appdata.xml.in:82 -#: data/com.github.louis77.tuner.appdata.xml.in:101 -#: data/com.github.louis77.tuner.appdata.xml.in:123 +#: data/io.github.louis77.tuner.appdata.xml.in:70 +#: data/io.github.louis77.tuner.appdata.xml.in:82 +#: data/io.github.louis77.tuner.appdata.xml.in:101 +#: data/io.github.louis77.tuner.appdata.xml.in:123 msgid "Other improvements:" msgstr "Diğer iyileştirmeler:" -#: data/com.github.louis77.tuner.appdata.xml.in:72 +#: data/io.github.louis77.tuner.appdata.xml.in:72 msgid "Added Dutch translation" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:74 -#: data/com.github.louis77.tuner.appdata.xml.in:87 -#: data/com.github.louis77.tuner.appdata.xml.in:108 -#: data/com.github.louis77.tuner.appdata.xml.in:130 +#: data/io.github.louis77.tuner.appdata.xml.in:74 +#: data/io.github.louis77.tuner.appdata.xml.in:87 +#: data/io.github.louis77.tuner.appdata.xml.in:108 +#: data/io.github.louis77.tuner.appdata.xml.in:130 msgid "Bugfixes:" msgstr "Hata düzeltmeleri:" -#: data/com.github.louis77.tuner.appdata.xml.in:76 -#: data/com.github.louis77.tuner.appdata.xml.in:89 +#: data/io.github.louis77.tuner.appdata.xml.in:76 +#: data/io.github.louis77.tuner.appdata.xml.in:89 msgid "Fix missing icons for non elementary OS systems" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:84 +#: data/io.github.louis77.tuner.appdata.xml.in:84 msgid "Add manual dark mode switch in settings" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:85 +#: data/io.github.louis77.tuner.appdata.xml.in:85 msgid "Compact sidebar and content window" msgstr "" -#: data/com.github.louis77.tuner.appdata.xml.in:97 +#: data/io.github.louis77.tuner.appdata.xml.in:97 msgid "Right-click a station to add or remove to favourites directly" msgstr "" "Doğrudan favorilere eklemek veya çıkarmak için bir istasyona sağ tıklayın" -#: data/com.github.louis77.tuner.appdata.xml.in:98 +#: data/io.github.louis77.tuner.appdata.xml.in:98 msgid "" "Add settings menu with \"Do-Not-Track\" option, disables sending station " "listening events to radio-browser.org" @@ -139,23 +139,23 @@ msgstr "" "\"Takip Etme\" seçeneğiyle dinlediğiniz radyoların diğer sitelere 'radio-" "browser.org' veri gönderimini durdurur " -#: data/com.github.louis77.tuner.appdata.xml.in:99 +#: data/io.github.louis77.tuner.appdata.xml.in:99 msgid "Add About dialog" msgstr "Hakkında iletişim bölümüne eklendi" -#: data/com.github.louis77.tuner.appdata.xml.in:103 +#: data/io.github.louis77.tuner.appdata.xml.in:103 msgid "" "If a station is already in your favourites, you'll see a little star in the " "title" msgstr "Bir istasyon zaten favorilerinizdeyse, küçük bir yıldız göreceksiniz." -#: data/com.github.louis77.tuner.appdata.xml.in:104 +#: data/io.github.louis77.tuner.appdata.xml.in:104 msgid "" "Randomly selects one of the available radio-browser.org servers (was always " "de1 before)" msgstr "Mevcut radio-browser.org sunucularından birini rastgele seçer" -#: data/com.github.louis77.tuner.appdata.xml.in:105 +#: data/io.github.louis77.tuner.appdata.xml.in:105 msgid "" "Favourites are now stored in a local favourites.json file to improve app " "startup time" @@ -163,64 +163,64 @@ msgstr "" "Sık kullanılanlar artık uygulamayı iyileştirmek için yerel bir favourites." "json dosyasında saklanıyor" -#: data/com.github.louis77.tuner.appdata.xml.in:106 +#: data/io.github.louis77.tuner.appdata.xml.in:106 msgid "Added Italian translation" msgstr "İtalyanca Çeviri Eklendi" -#: data/com.github.louis77.tuner.appdata.xml.in:110 +#: data/io.github.louis77.tuner.appdata.xml.in:110 msgid "" "Fix broken dark theme support (elementary and Adwaita dark look fine now)" msgstr "" "Hatalı karanlık tema desteği düzeltildi (elemantry os ve adwaita koyu tema " "şimdi iyi gözüküyor)" -#: data/com.github.louis77.tuner.appdata.xml.in:118 +#: data/io.github.louis77.tuner.appdata.xml.in:118 msgid "Search for radio stations" msgstr "Radyo istasyonlarını arayın" -#: data/com.github.louis77.tuner.appdata.xml.in:119 +#: data/io.github.louis77.tuner.appdata.xml.in:119 msgid "New \"Genres\" section with select popular genres" msgstr "Seçkin popüler türleri içeren yeni \"Türler\" bölümü" -#: data/com.github.louis77.tuner.appdata.xml.in:120 +#: data/io.github.louis77.tuner.appdata.xml.in:120 msgid "Added French translation" msgstr "Fransızca Çeviri Eklendi" -#: data/com.github.louis77.tuner.appdata.xml.in:121 +#: data/io.github.louis77.tuner.appdata.xml.in:121 msgid "Added German translation" msgstr "Almanca Çeviri Eklendi" -#: data/com.github.louis77.tuner.appdata.xml.in:125 +#: data/io.github.louis77.tuner.appdata.xml.in:125 msgid "Each section now displays the most-voted-for 40 stations" msgstr "Her bölüm artık en çok oy alan 40 istasyonu gösteriyor" -#: data/com.github.louis77.tuner.appdata.xml.in:126 +#: data/io.github.louis77.tuner.appdata.xml.in:126 msgid "Station images are now being cached" msgstr "İstasyon görüntüleri artık önbelleğe alınıyor" -#: data/com.github.louis77.tuner.appdata.xml.in:127 +#: data/io.github.louis77.tuner.appdata.xml.in:127 msgid "The app icon now appears more vertically centered" msgstr "Uygulama simgesi artık daha dikey olarak ortalanmış görünüyor" -#: data/com.github.louis77.tuner.appdata.xml.in:128 +#: data/io.github.louis77.tuner.appdata.xml.in:128 msgid "Station images are now always the same size" msgstr "İstasyon görüntüleri artık her zaman aynı boyuttadır" -#: data/com.github.louis77.tuner.appdata.xml.in:132 +#: data/io.github.louis77.tuner.appdata.xml.in:132 msgid "Fixed a bug where starred stations appeared as unstarred" msgstr "" "Yıldızlı istasyonların yıldızsız olarak görünmesine neden olan bir hata " "düzeltildi" -#: data/com.github.louis77.tuner.appdata.xml.in:133 +#: data/io.github.louis77.tuner.appdata.xml.in:133 msgid "Display a nice API error screen" msgstr "Güzel bir API hatası ekranı görüntüleyin" -#: data/com.github.louis77.tuner.appdata.xml.in:140 +#: data/io.github.louis77.tuner.appdata.xml.in:140 msgid "New sidebar menu with different selections and your favourite stations." msgstr "" "Farklı seçimler ve favori istasyonlarınız ile yeni kenar çubuğu menüsü." -#: data/com.github.louis77.tuner.appdata.xml.in:145 +#: data/io.github.louis77.tuner.appdata.xml.in:145 msgid "Initial release" msgstr "İlk sürüm" diff --git a/po/fr.po b/po/fr.po index b50fb67..0b69325 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,11 +1,11 @@ -# French translations for com.github.louis77.tuner package. -# Copyright (C) 2020 THE com.github.louis77.tuner'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# French translations for io.github.louis77.tuner package. +# Copyright (C) 2020 THE io.github.louis77.tuner'S COPYRIGHT HOLDER +# This file is distributed under the same license as the io.github.louis77.tuner package. # Nathan Bonnemains (@NathanBnm), 2020. # msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2020-09-28 11:48+0200\n" diff --git a/po/com.github.louis77.tuner.pot b/po/io.github.louis77.tuner.pot similarity index 99% rename from po/com.github.louis77.tuner.pot rename to po/io.github.louis77.tuner.pot index cf46f92..3ccec99 100644 --- a/po/com.github.louis77.tuner.pot +++ b/po/io.github.louis77.tuner.pot @@ -1,12 +1,12 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# This file is distributed under the same license as the io.github.louis77.tuner package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" diff --git a/po/it.po b/po/it.po index f913db2..407413c 100644 --- a/po/it.po +++ b/po/it.po @@ -1,12 +1,12 @@ -# Italian translations for com.github.louis77.tuner package. -# Copyright (C) 2020 THE com.github.louis77.tuner'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# Italian translations for io.github.louis77.tuner package. +# Copyright (C) 2020 THE io.github.louis77.tuner'S COPYRIGHT HOLDER +# This file is distributed under the same license as the io.github.louis77.tuner package. # Automatically generated, 2020. # # FinixFighter , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-11-26 22:48+0000\n" diff --git a/po/ja.po b/po/ja.po index f46955d..8ac8afb 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,10 +1,10 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# This file is distributed under the same license as the io.github.louis77.tuner package. # TANIGUCHI Takaki , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-07-15 11:29+0000\n" diff --git a/po/nb_NO.po b/po/nb_NO.po index ddf7587..ab65b90 100644 --- a/po/nb_NO.po +++ b/po/nb_NO.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# This file is distributed under the same license as the io.github.louis77.tuner package. # Allan Nordhøy , 2022. # Jon Helgeland , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-11-20 15:47+0000\n" diff --git a/po/nl.po b/po/nl.po index f7bb5ab..e6f1f34 100644 --- a/po/nl.po +++ b/po/nl.po @@ -1,11 +1,11 @@ -# Dutch translations for com.github.louis77.tuner package. -# Copyright (C) 2020 THE com.github.louis77.tuner'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# Dutch translations for io.github.louis77.tuner package. +# Copyright (C) 2020 THE io.github.louis77.tuner'S COPYRIGHT HOLDER +# This file is distributed under the same license as the io.github.louis77.tuner package. # Automatically generated, 2020. # msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2021-06-10 14:17+0200\n" diff --git a/po/pt_BR.po b/po/pt_BR.po index a702ef2..20ce46f 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# This file is distributed under the same license as the io.github.louis77.tuner package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-02-16 23:23-0300\n" diff --git a/po/ru.po b/po/ru.po index 68f5ec2..b5734d5 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,11 +1,11 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# This file is distributed under the same license as the io.github.louis77.tuner package. # AHOHNMYC , 2022. # Мистер Перевод , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-09-22 22:21+0000\n" diff --git a/po/tr.po b/po/tr.po index c0de667..3d159ba 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,11 +1,11 @@ -# Turkish translations for com.github.louis77.tuner package. -# Copyright (C) 2020 THE com.github.louis77.tuner'S COPYRIGHT HOLDER -# This file is distributed under the same license as the com.github.louis77.tuner package. +# Turkish translations for io.github.louis77.tuner package. +# Copyright (C) 2020 THE io.github.louis77.tuner'S COPYRIGHT HOLDER +# This file is distributed under the same license as the io.github.louis77.tuner package. # Safak , 2020. # Oğuz Ersen , 2022. msgid "" msgstr "" -"Project-Id-Version: com.github.louis77.tuner\n" +"Project-Id-Version: io.github.louis77.tuner\n" "Report-Msgid-Bugs-To: louis@brauer.family\n" "POT-Creation-Date: 2020-11-08 19:49+0100\n" "PO-Revision-Date: 2022-05-18 21:15+0000\n" diff --git a/src/Application.vala b/src/Application.vala index 2995328..d08d7fc 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -1,192 +1,470 @@ /** - * @file Application.vala - * @brief Contains the main Application class for the Tuner application + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file Application.vala + * + * @brief Main application class and namespace assets for the Tuner radio application */ + using GLib; + /** - * @class Tuner.Application - * @brief Main application class for Tuner - * @extends Gtk.Application - * - * This class serves as the entry point for the Tuner application. - * It handles application initialization, window management, and theme settings. + * @namespace Tuner + * @brief Main namespace for the Tuner application */ -public class Tuner.Application : Gtk.Application { +namespace Tuner { - /** @brief Application version */ - public const string APP_VERSION = VERSION; - - /** @brief Application ID */ - public const string APP_ID = "com.github.louis77.tuner"; - - /** @brief Unicode character for starred items */ - public const string STAR_CHAR = "★ "; - - /** @brief Unicode character for unstarred items */ - public const string UNSTAR_CHAR = "☆ "; - - // /** - // * @enum Theme - // * @brief Enumeration of available themes - // */ - // public enum Theme { - // SYSTEM, - // LIGHT, - // DARK - // } - - /** @brief Application settings */ - public GLib.Settings settings { get; construct; } - - /** @brief Player controller */ - public PlayerController player { get; construct; } - - /** @brief Cache directory path */ - public string? cache_dir { get; construct; } - - /** @brief Data directory path */ - public string? data_dir { get; construct; } - /** @brief Main application window */ - public Window window; + /* + Namespace Assets and Methods + */ + private static Application _instance; - /** @brief Action entries for the application */ - private const ActionEntry[] ACTION_ENTRIES = { - { "resume-window", on_resume_window } - }; /** - * @brief Constructor for the Application - */ - public Application () { - Object ( - application_id: APP_ID, - flags: ApplicationFlags.FLAGS_NONE - ); - } + * @brief Available themes + * + */ + public enum THEME + { + SYSTEM, + LIGHT, + DARK; - /** - * @brief Construct block for initializing the application - */ - construct { - GLib.Intl.setlocale (LocaleCategory.ALL, ""); - GLib.Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); - GLib.Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); - GLib.Intl.textdomain (GETTEXT_PACKAGE); - - settings = new GLib.Settings (this.application_id); - - player = new PlayerController (); + public unowned string get_name () + { + switch (this) { + case SYSTEM: + return "system"; - cache_dir = Path.build_filename (Environment.get_user_cache_dir (), application_id); - ensure_dir (cache_dir); + case LIGHT: + return "light"; - data_dir = Path.build_filename (Environment.get_user_data_dir (), application_id); - ensure_dir (data_dir); + case DARK: + return "dark"; - add_action_entries(ACTION_ENTRIES, this); + default: + assert_not_reached(); + } + } + } // THEME + + + /** + * @brief Applys the given theme to the app + * + * @return The Application instance + */ + public static void apply_theme(THEME requested_theme) + { + apply_theme_name( requested_theme.get_name() ); } - /** @brief Singleton instance of the Application */ - public static Application _instance = null; + + public static void apply_theme_name(string requested_theme) + { + if ( requested_theme == THEME.LIGHT.get_name() ) + { + debug(@"Applying theme: light"); + Gtk.Settings.get_default().set_property("gtk-theme-name", "Adwaita"); + return; + } + + if ( requested_theme == THEME.DARK.get_name() ) + { + debug(@"Applying theme: dark"); + Gtk.Settings.get_default().set_property("gtk-theme-name", "Adwaita-dark"); + return; + } + + if ( requested_theme == THEME.SYSTEM.get_name() ) + { + debug(@"System theme X: $(Application.SYSTEM_THEME())"); + Gtk.Settings.get_default().set_property("gtk-theme-name", Application.SYSTEM_THEME()); + return; + } + assert_not_reached(); + } // apply_theme + /** - * @brief Getter for the singleton instance - * @return The Application instance - */ - public static Application instance { - get { - if (_instance == null) { - _instance = new Application (); - } + * @brief Getter for the singleton instance + * + * @return The Application instance + */ + public static Application app() { return _instance; - } - } + } // app - // /** - // * @brief Current theme of the application - // */ - // public bool prefer_dark_theme { - // get { - // return settings.get_boolean("prefer-dark-theme"); - // } - // set { - // var gtk_settings = Gtk.Settings.get_default(); - // if (gtk_settings == null) { - // warning("Failed to get GTK settings"); - // } - // else { - // gtk_settings.gtk_application_prefer_dark_theme = value; - // } - // settings.set_boolean("prefer-dark-theme", value); - // } - // } + /** + * @brief Send the calling method for a nap + * + * @param interval the time to nap + * @param priority priority of chacking nap is over + */ + public static async void nap (uint interval) { + Timeout.add (interval, () => { + nap.callback (); + return Source.REMOVE; + }, Priority.LOW); + yield; + } // nap /** - * @brief Activates the application - * - * This method is called when the application is activated. It creates - * or presents the main window and initializes the DBus connection. - */ - protected override void activate() { - if (window == null) { - window = new Window (this, player); - add_window (window); - DBus.initialize (); - } else { - window.present (); + * @brief Asynchronously transitions the image with a fade effect. + * + * @param {Gtk.Image} image - The image to transition. + * @param {uint} duration_ms - Duration of the fade effect in milliseconds. + * @param {Closure} callback - Optional callback function to execute after fading. + */ + public static async void fade(Gtk.Image image, uint duration_ms, bool fading_in) + { + double step = 0.05; // Adjust opacity in 5% increments + uint interval = (uint) (duration_ms / (1.0 / step)); // Interval based on duration + + while ( ( !fading_in && image.opacity != 0 ) || (fading_in && image.opacity != 1) ) + { + double op = image.opacity + (fading_in ? step : -step); + image.opacity = op.clamp(0, 1); + yield nap (interval); } - } + } // fade - /** - * @brief Resumes the window - * - * This method is called to bring the main window to the foreground. - */ - private void on_resume_window() { - window.present(); - } + + public static unowned string safestrip( string? text ) + { + if ( text == null ) return ""; + if ( text.length == 0 ) return ""; + return text._strip(); + } // safestrip + + + /* + + Application + + */ /** - * @brief Ensures a directory exists - * @param path The directory path to ensure - * - * This method creates the specified directory if it doesn't exist. - */ - private void ensure_dir (string path) { - var dir = File.new_for_path (path); + * @class Application + * @brief Main application class implementing core functionality + * @ingroup Tuner + * + * The Application class serves as the primary entry point and controller for the Tuner + * application. It manages: + * - Window creation and presentation + * - Settings management + * - Player control + * - Directory structure + * - DBus initialization + * + * @note This class follows the singleton pattern, accessible via Application.instance + */ + public class Application : Gtk.Application + { + + private static Gtk.Settings GTK_SETTINGS; + private static string GTK_SYSTEM_THEME = "unset"; + + /** @brief Application version */ + public const string APP_VERSION = VERSION; + + /** @brief Application ID */ + public const string APP_ID = "io.github.louis77.tuner"; - try { - debug (@"Ensuring dir exists: $path"); - dir.make_directory (); - - } catch (Error e) { - // TODO not enough error handling - // What should happen when there is another IOERROR? - if (!(e is IOError.EXISTS)) { - warning (@"dir couldn't be created: %s", e.message); + /** @brief Unicode character for starred items */ + public const string STAR_CHAR = "★ "; + + /** @brief Unicode character for unstarred items */ + public const string UNSTAR_CHAR = "☆ "; + + /** @brief Unicode character for out-of-date items */ + public const string EXCLAIM_CHAR = "⚠ "; + + /** @brief File name for starred station sore */ + public const string STARRED = "starred.json"; + + /** @brief Connectivity monitoring*/ + private static NetworkMonitor NETMON = NetworkMonitor.get_default (); + + private static Gtk.CssProvider CSSPROVIDER = new Gtk.CssProvider(); + + + + // ------------------------------------- + + + /** @brief Application settings */ + public Settings settings { get; construct; } + + /** @brief Player controller */ + public PlayerController player { get; construct; } + + /** @brief Player controller */ + public DirectoryController directory { get; construct; } + + /** @brief Player controller */ + public StarStore stars { get; construct; } + + /** @brief API DataProvider */ + public DataProvider.API provider { get; construct; } + + /** @brief Cache directory path */ + public string? cache_dir { get; construct; } + + /** @brief Data directory path */ + public string? data_dir { get; construct; } + + public Cancellable offline_cancel { get; construct; } + + public static string SYSTEM_THEME() { return GTK_SYSTEM_THEME; } + + + /** @brief Are we online */ + public bool is_offline { get; private set; } + private bool _is_online = false; + public bool is_online { + get { return _is_online; } + private set { + if ( value ) + { + _offline_cancel.reset (); + } + else + { + _offline_cancel.cancel (); + } + _is_online = value; + is_offline = !value; } + } + + + /** @brief Main application window */ + public Window window { get; private set; } + + + /** @brief Action entries for the application */ + private const ActionEntry[] ACTION_ENTRIES = { + { "resume-window", on_resume_window } + }; + + private uint _monitor_changed_id = 0; + private bool _has_started = false; + + + /** + * @brief Constructor for the Application + */ + private Application () { + Object ( + application_id: APP_ID, + flags: ApplicationFlags.FLAGS_NONE + ); } - } - // /** - // * @brief Converts a string to a Theme enum value - // * @param theme The theme string to convert - // * @return The corresponding Theme enum value - // */ - // public static Theme theme_from_string(string theme_string) { - - // warning(@"Theme requested: $(theme_string)"); - // switch (theme_string) { - // case "TUNER_APPLICATION_THEME_LIGHT": return Theme.LIGHT; - // case "TUNER_APPLICATION_THEME_DARK": return Theme.DARK; - // default: return Theme.SYSTEM; - // } - // } - -} + + /** + * @brief Construct block for initializing the application + */ + construct + { + Intl.setlocale (LocaleCategory.ALL, ""); + Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + Intl.textdomain (GETTEXT_PACKAGE); + + // Create required directories and files + + cache_dir = stat_dir(Environment.get_user_cache_dir ()); + data_dir = stat_dir(Environment.get_user_data_dir ()); + + + /* + Starred file and migration of favorites + */ + var _favorites_file = File.new_build_filename (data_dir, "favorites.json"); // v1 file + var _starred_file = File.new_build_filename (data_dir, Application.STARRED); // v2 file + + /* Migration not possible with renamed app */ + // try { + // _favorites_file.open_readwrite().close (); // Try to open, if succeeds it exists, if not err - no migration + // _starred_file.create(NONE); // Try to create, if fails starred already exists, if not ok to migrate + // _favorites_file.copy (_starred_file, FileCopyFlags.NONE); // Copy + // warning(@"Migrated v1 Favorites to v2 Starred"); + // } + // catch (Error e) { + // // Peconditions not met + // } + + /* + Create the cancellable. + Wrap network monitoring into a bool property + */ + offline_cancel = new Cancellable(); + NETMON.network_changed.connect((monitor) => { + check_online_status(); + }); + is_online = NETMON.get_network_available (); + is_offline = !is_online; + + + /* + Init Tuner assets + */ + settings = new Settings (); + provider = new DataProvider.RadioBrowser(null); + player = new PlayerController (); + stars = new StarStore(_starred_file); + directory = new DirectoryController(provider, stars); + + add_action_entries(ACTION_ENTRIES, this); + + /* + Hook up voting and counting + */ + player.state_changed_sig.connect ((station, state) => + // Do a provider click when starting to play a sation + { + if ( !settings.do_not_vote && state == PlayerController.Is.PLAYING ) + { + provider.click(station.stationuuid); + station.clickcount++; + station.clicktrend++; + } + }); + + player.tape_counter_sig.connect((station) => + // Every ten minutes of continuous playing tape counter sigs are emitted + // Vote and click the station each time as appropriate + { + if ( settings.do_not_vote ) return; + if ( station.starred ) + { + provider.vote(station.stationuuid); + station.votes++; + } + provider.click(station.stationuuid); + station.clickcount++; + station.clicktrend++; + }); + + } // construct + + + /** + * @brief Getter for the singleton instance + * + * @return The Application instance + */ + public static Application instance { + get { + if (Tuner._instance == null) { + Tuner._instance = new Application (); + } + return Tuner._instance; + } + } // instance + + + /** + * @brief Activates the application + * + * This method is called when the application is activated. It creates + * or presents the main window and initializes the DBus connection. + */ + protected override void activate() + { + if (window == null) { + window = new Window (this, player, settings, directory); + DBus.initialize (); + settings.configure(); + + GTK_SETTINGS = Gtk.Settings.get_default(); + GTK_SYSTEM_THEME = GTK_SETTINGS.gtk_theme_name; + apply_theme_name( settings.theme_mode); + + CSSPROVIDER.load_from_resource ("io/github/louis77/tuner/css/Tuner-system.css"); + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + CSSPROVIDER, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + add_window (window); + } else { + window.present (); + } + } // activate + + + /** + * @brief Resumes the window + * + * This method is called to bring the main window to the foreground. + */ + private void on_resume_window() { + window.present(); + } + + /** + * @brief Create directory structure quietly + * + */ + private string? stat_dir (string dir) + { + var _dir = File.new_build_filename (dir, application_id); + try { + _dir.make_directory_with_parents (); + } catch (IOError.EXISTS e) { + } catch (Error e) { + warning(@"Stat Directory failed $(e.message)"); + return null; + } + return _dir.get_path (); + + } // stat_dir + + /** + * @brief Set the network availability + * + * If going offline, set immediately. + * Going online - wait a second to allow network to stabilize + * This method removes any existing timeout and sets a new one + * reduces network state bouncyness + */ + private void check_online_status() + { + if(_monitor_changed_id > 0) + { + Source.remove(_monitor_changed_id); + _monitor_changed_id = 0; + } + + /* + If change to online from offline state + wait 1 seconds before setting to online status + to whatever the state is at that time + */ + if ( is_offline && NETMON.get_network_available () ) + { + _monitor_changed_id = Timeout.add_seconds( (uint)_has_started, () => + { + _monitor_changed_id = 0; // Reset timeout ID after scheduling + is_online = NETMON.get_network_available (); + _has_started = true; + return Source.REMOVE; + }); + + return; + } + // network is unavailable + is_online = false; + } // check_online_status + } // Application +} // namespace Tuner \ No newline at end of file diff --git a/src/Config.vala.in b/src/Config.vala.in index 30582b0..c320465 100644 --- a/src/Config.vala.in +++ b/src/Config.vala.in @@ -1,3 +1,15 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file Config.vala.in + * + * @brief App configuration template + * + */ + public const string GETTEXT_PACKAGE = @GETTEXT_PACKAGE@; public const string LOCALEDIR = @LOCALEDIR@; public const string VERSION = @VERSION@; diff --git a/src/Controllers/DirectoryController.vala b/src/Controllers/DirectoryController.vala index 4089560..9d40475 100644 --- a/src/Controllers/DirectoryController.vala +++ b/src/Controllers/DirectoryController.vala @@ -1,6 +1,13 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file DirectoryController.vala + * + * @brief Station Directories and their population + * */ using Gee; @@ -12,265 +19,496 @@ public errordomain SourceError { UNAVAILABLE } -/** - * @brief Delegate type for fetching radio stations. - * @param offset The starting index for fetching stations. - * @param limit The maximum number of stations to fetch. - * @return An ArrayList of RadioBrowser.Station objects. - * @throws SourceError If the source is unavailable. - */ -public delegate ArrayList Tuner.FetchType(uint offset, uint limit) throws SourceError; + /** - * @brief Controller class for managing radio station directories. + * Controller class responsible for managing directory-related operations in the Tuner application. + * + * This class handles various directory management tasks, + * such as file system operations, directory navigation, and file monitoring within the + * context of the Tuner namespace. */ -public class Tuner.DirectoryController : Object { - public RadioBrowser.Client? provider { get; set; } - public Model.StationStore store { get; set; } +public class Tuner.DirectoryController : Object +{ + + private const uint DIRECTORY_LIMIT = 41; - public signal void tags_updated (ArrayList tags); + private DataProvider.API _provider; + private StarStore _star_store; + private bool _loaded = false; + + public signal void tags_updated (Set tags); + + public string provider_name() + { + return _provider.name(); + } /** * @brief Constructor for DirectoryController. + * * @param store The StationStore to use for managing stations. */ - public DirectoryController (Model.StationStore store) { - try { - var client = new RadioBrowser.Client (); - this.provider = client; - } catch (RadioBrowser.DataError e) { - critical (@"RadioBrowser unavailable"); - } - - this.store = store; + public DirectoryController (DataProvider.API provider, StarStore star_store) { + Object(); + _provider = provider; + _star_store = star_store; + } // DirectoryController + + + public string provider() + { + return _provider.name(); + } // provider + + /** + * @brief Initializes the provider and starts the load of Saved Stations + * + */ + public void load() + { + if (_loaded || app().is_offline) + return; + + _provider.initialize(); + _star_store.load.begin(); + _loaded = true; + } // load + + + /** + * @brief Get a collection of Station station by its UUID. + * + * @param uuid The UUID of the station to load. + * @return A StationSet object for the requested station. + * @todo radio-browser should handle multiple UUID on a query, but is broken + */ + public Set get_stations_by_uuid (Collection uuids) + { + try + { + return _provider.by_uuids(uuids); + } catch (Tuner.DataProvider.DataError e) + { + critical (@"$(_provider.name()) unavailable"); + } + return new HashSet(); + } // get_stations_by_uuid - // migrate from <= 1.2.3 settings to json based store - //this.migrate_favourites (); - } /** - * @brief Load a station by its UUID. - * @param uuid The UUID of the station to load. - * @return A StationSource object for the requested station. + * @brief Get a collection of Station station by its stream URL. // TODO Not working at Provider + * + * @param uuid The URL of the station to load. + * @return A StationSet object for the requested station. + * @todo radio-browser should handle URL querys, but is broken */ - public StationSource load_station_uuid (string uuid) { - string[] lps_arr = { uuid }; - var params = RadioBrowser.SearchParams() { - uuids = new ArrayList.wrap (lps_arr) - }; - var source = new StationSource(1, params, provider, store); - return source; - } + // public Set get_station_by_url (string url) { + // try { + // return _provider.by_url(url); + // } catch (Tuner.DataProvider.DataError e) { + // critical (@"$(_provider.name) unavailable"); + // } + // return new HashSet(); + // } // get_station_by_url + + + /** + * @brief Load a station by its UUID. + * + * @param uuid The UUID of the station to load. + * @return A StationSet object for the requested station. + */ + public StationSet load_station_uuid (string uuid) + { + debug(@"LBU UUID: $uuid "); + var params = DataProvider.SearchParams() + { + uuids = new HashSet() + }; + params.uuids.add (uuid); + var source = new StationSet(1, params, _provider, _star_store); + return source; + } // load_station_uuid + + + /** + * @brief Load a set of random stations. + * + * @param limit The maximum number of stations to load. + * @return A StationSet object with random stations. + */ + public StationSet load_random_stations (uint limit) + { + var params = DataProvider.SearchParams() { + text = "", + countrycode = "", + tags = new HashSet(), + order = DataProvider.SortOrder.RANDOM + }; + var source = new StationSet(limit, params, _provider, _star_store); + return source; + } // load_random_stations + + + /** + * @brief Load trending stations. + * + * @param limit The maximum number of stations to load. + * @return A StationSet object with trending stations. + */ + public StationSet load_trending_stations (uint limit) + { + var params = DataProvider.SearchParams() { + text = "", + countrycode = "", + tags = new HashSet(), + order = DataProvider.SortOrder.CLICKTREND, + reverse = true + }; + var source = new StationSet(limit, params, _provider, _star_store); + return source; + } // load_trending_stations + + + /** + * @brief Load popular stations. + * + * @param limit The maximum number of stations to load. + * @return A StationSet object with popular stations. + */ + public StationSet load_popular_stations (uint limit) + { + var params = DataProvider.SearchParams() { + text = "", + countrycode = "", + tags = new HashSet(), + order = DataProvider.SortOrder.CLICKCOUNT, + reverse = true + }; + var source = new StationSet(limit, params, _provider, _star_store); + return source; + } // load_popular_stations + + + /** + * @brief Load stations by country code. + * + * @param limit The maximum number of stations to load. + * @param countrycode The country code to filter stations. + * @return A StationSet object with stations from the specified country. + */ + public StationSet load_by_country (uint limit, string countrycode) + { + var params = DataProvider.SearchParams () { + text = "", + countrycode = countrycode, + tags = new HashSet(), + order = DataProvider.SortOrder.CLICKCOUNT, + reverse = true + }; + var source = new StationSet(limit, params, _provider, _star_store); + return source; + } // load_by_country + + + /** + * @brief Load stations based on search text. + * + * @param utext The search text to filter stations. + * @param limit The maximum number of stations to load. + * @return A StationSet object with stations matching the search text. + */ + public StationSet load_search_stations (string utext, uint limit) + { + var params = DataProvider.SearchParams() { + text = utext, + countrycode = "", + tags = new HashSet(), + order = DataProvider.SortOrder.CLICKCOUNT, + reverse = true + }; + var source = new StationSet(limit, params, _provider, _star_store); + return source; + } // load_search_stations + /** - * @brief Load a set of random stations. - * @param limit The maximum number of stations to load. - * @return A StationSource object with random stations. + * @brief Get all starred stations. + * + * @return An Collection of starred Model.Station objects. */ - public StationSource load_random_stations (uint limit) { - var params = RadioBrowser.SearchParams() { - text = "", - countrycode = "", - tags = new ArrayList(), - order = RadioBrowser.SortOrder.RANDOM - }; - var source = new StationSource(limit, params, provider, store); - return source; - } - - public StationSource load_trending_stations (uint limit) { - var params = RadioBrowser.SearchParams() { - text = "", - countrycode = "", - tags = new ArrayList(), - order = RadioBrowser.SortOrder.CLICKTREND, - reverse = true - }; - var source = new StationSource(limit, params, provider, store); - return source; - } - - public StationSource load_popular_stations (uint limit) { - var params = RadioBrowser.SearchParams() { - text = "", - countrycode = "", - tags = new ArrayList(), - order = RadioBrowser.SortOrder.CLICKCOUNT, - reverse = true - }; - var source = new StationSource(limit, params, provider, store); - return source; - } - - public StationSource load_by_country (uint limit, string countrycode) { - var params = RadioBrowser.SearchParams () { - text = "", - countrycode = countrycode, - tags = new ArrayList(), - order = RadioBrowser.SortOrder.CLICKCOUNT, - reverse = true - }; - var source = new StationSource(limit, params, provider, store); - return source; - } - - public StationSource load_search_stations (owned string utext, uint limit) { - var params = RadioBrowser.SearchParams() { - text = utext, - countrycode = "", - tags = new ArrayList(), - order = RadioBrowser.SortOrder.CLICKCOUNT, - reverse = true - }; - var source = new StationSource(limit, params, provider, store); - return source; - } - - public ArrayList get_stored () { - return _store.get_all (); - } + public Collection get_starred () { + return _star_store.get_all_stations(); + } // get_starred + +// -------------------------------------------------- /** - * @brief Load stations by tags. - * @param utags An ArrayList of tags to filter stations. - * @return A StationSource object with stations matching the given tags. + * @brief Adds a saved search term and returns the StatioSet to retrieve the stations found + * + * @return A StationSet object with stations */ - public StationSource load_by_tags (owned ArrayList utags) { - var params = RadioBrowser.SearchParams() { - text = "", - countrycode = "", - tags = utags, - order = RadioBrowser.SortOrder.VOTES, - reverse = true - }; - var source = new StationSource(40, params, provider, store); - return source; - } + public StationSet add_saved_search( string search_term) + { + _star_store.add_saved_search ( search_term); + return load_search_stations(search_term,DIRECTORY_LIMIT); + } // add_saved_search + /** - * @brief Count a click for a station. - * @param station The station that was clicked. + * @brief Removed the given saved search term + * */ - public void count_station_click (Model.Station station) { - if (!Application.instance.settings.get_boolean ("do-not-track")) { - debug (@"Send listening event for station $(station.id)"); - provider.track (station.id); - } else { - debug ("do-not-track enabled, will not send listening event"); - } - } + public void remove_saved_search( string search_text) + { + _star_store.remove_saved_search ( search_text); + } // remove_saved_search + /** - * @brief Load tags from the provider. + * @brief Loads stations found from each saved search term + * + * @return A map of search terms and related StationSet object with stations */ - public void load_tags () { - try { - var tags = provider.get_tags (); - tags_updated (tags); - } catch (RadioBrowser.DataError e) { - warning (@"Load tags failed with error: $(e.message)"); + public Map load_saved_searches() + { + Map searches = new HashMap(); + foreach( var search in _star_store.get_all_searches ()) + { + searches.set (search, load_search_stations(search,DIRECTORY_LIMIT)); } - } + return searches; + } // load_saved_searches + + + // ------------------------------------------------- + + /** + * @brief Load stations by tags. + * + * @param utags An ArrayList of tags to filter stations. + * @return A StationSet object with stations matching the given tags. + */ + public StationSet load_by_tag (string utag) + { + var t =new HashSet(); + t.add (utag.down()); + var params = DataProvider.SearchParams() { + text = "", + countrycode = "", + tags = t, + order = DataProvider.SortOrder.VOTES, + reverse = true + }; + var source = new StationSet(40, params, _provider, _star_store); + return source; + } // load_by_tag + + + /** + * @brief Count a click for a station. + * + * @param station The station that was clicked. + */ + public StationSet load_by_tag_set (Set utags) + { + var params = DataProvider.SearchParams() { + text = "", + countrycode = "", + tags = utags, + order = DataProvider.SortOrder.VOTES, + reverse = true + }; + var source = new StationSet(40, params, _provider, _star_store); + return source; + } // load_by_tag_set + + + /** + * @brief Count a click for a station. + * + * @param station The station that was clicked. + */ + public void count_station_click (Model.Station station) + { + if (!app().settings.do_not_vote) + { + debug (@"Send listening event for station $(station.stationuuid)"); + _provider.click (station.stationuuid); + } + else + { + debug ("do-not-vote enabled, will not send listening event"); + } + } // count_station_click -} /** - * @brief Source class for managing sets of radio stations. + * @brief Load tags from the _provider. */ -public class Tuner.StationSource : Object { - private uint _offset = 0; - private uint _page_size = 20; - private bool _more = true; - private RadioBrowser.SearchParams _params; - private RadioBrowser.Client _client; - private Model.StationStore _store; + public void load_tags () + { + try + { + var tags = _provider.get_tags (); + tags_updated (tags); + } catch (DataProvider.DataError e) + { + warning (@"Load tags failed with error: $(e.message)"); + } + } // load_tags + + + public Set load_random_genres(int genres) + { + Set result = new HashSet(); + + while (app().is_online && result.size < genres) + { + try + { + var offset = Random.int_range(0, _provider.available_tags()); + var tag = _provider.get_tags (offset,1);// Get a random tag + result.add_all (tag); + } catch (Error e) + { + } + } + return result; + } // load_random_genres +} // DirectoryController + + +/** + * @brief A pagable set of Stations + */ +public class Tuner.StationSet : Object +{ + private uint _offset = 0; + private uint _page_size = 20; + private bool _more = true; + private DataProvider.SearchParams _params; + private DataProvider.API _provider; + private StarStore _star_store; /** - * @brief Constructor for StationSource. + * @brief Constructor for StationSet. + * * @param limit The maximum number of stations to fetch. * @param params The search parameters for fetching stations. * @param client The RadioBrowser client to use for fetching stations. - * @param store The StationStore to use for managing stations. + * @param star_store The StationStore to use for managing stations. */ - public StationSource (uint limit, - RadioBrowser.SearchParams params, - RadioBrowser.Client client, - Model.StationStore store) { + public StationSet (uint limit, + DataProvider.SearchParams params, + DataProvider.API client, + StarStore star_store) { Object (); // This disables paging for now _page_size = limit; _params = params; - _client = client; - _store = store; - } + _provider = client; + _star_store = star_store; + } // StationSet - /** - * @brief Fetch the next set of stations. - * @return An ArrayList of Model.Station objects. - * @throws SourceError If the source is unavailable. - */ - public ArrayList? next () throws SourceError { - // Fetch one more to determine if source has more items than page size - try { - var raw_stations = _client.search (_params, _page_size + 1, _offset); - // TODO Place filter here? - //var filtered_stations = raw_stations.filter (filterByCountry); - var filtered_stations = raw_stations.iterator (); - - var stations = convert_stations (filtered_stations); - _offset += _page_size; - _more = stations.size > _page_size; - if (_more) stations.remove_at( (int)_page_size); - return stations; - } catch (RadioBrowser.DataError e) { - throw new SourceError.UNAVAILABLE("Directory Error"); - } - } - /** - * @brief Check if there are more stations to fetch. - * @return true if there are more stations, false otherwise. - */ - public bool has_more () { - return _more; - } + /** + * @brief Fetch the next set of stations. + * + * @return An ArrayList of Model.Station objects. + * @throws SourceError If the source is unavailable. + */ + public Set? next_page () throws SourceError + { - /** - * @brief Convert RadioBrowser.Station objects to Model.Station objects. - * @param raw_stations An iterator of RadioBrowser.Station objects. - * @return An ArrayList of converted Model.Station objects. - */ - private ArrayList convert_stations (Iterator raw_stations) { - var stations = new ArrayList (); - - while (raw_stations.next()) { - // foreach (var station in raw_stations) { - var station = raw_stations.get (); - var s = new Model.Station ( - station.stationuuid, - station.name, - Model.Countries.get_by_code(station.countrycode, station.country), - station.url_resolved); - if (_store.contains (s)) { - s.starred = true; - } - s.favicon_url = station.favicon; - s.clickcount = station.clickcount; - s.homepage = station.homepage; - s.codec = station.codec; - s.bitrate = station.bitrate; - - s.notify["starred"].connect ( (sender, property) => { - if (s.starred) { - _store.add (s); - } else { - _store.remove (s); - } - }); - stations.add (s); - } - return stations; -} + if (app().is_offline) + return null; -} + // Fetch one more to determine if source has more items than page size + try + { + var raw_stations = _provider.search (_params, _page_size + 1, _offset); + // TODO Place filter here? + // var filtered_stations = raw_stations.filter (filterByCountry); + var filtered_stations = raw_stations.iterator (); + + var stations = convert_stations (filtered_stations); + _offset += _page_size; + _more = stations.size > _page_size; + if (_more) + stations.remove(stations.to_array ()[(int)_page_size]); + return stations; + } catch (DataProvider.DataError e) + { + throw new SourceError.UNAVAILABLE("Directory Error"); + } + } // next_page + + /** + * @brief Fetch the next set of stations. + * + * @return An ArrayList of Model.Station objects. + * @throws SourceError If the source is unavailable. + */ + public async Set? next_page_async () throws SourceError + { + + if (app().is_offline) + return null; + + // Fetch one more to determine if source has more items than page size + try + { + var raw_stations = yield _provider.search_async(_params, _page_size + 1, _offset); + // TODO Place filter here? + // var filtered_stations = raw_stations.filter (filterByCountry); + var filtered_stations = raw_stations.iterator (); + + var stations = convert_stations (filtered_stations); + _offset += _page_size; + _more = stations.size > _page_size; + if (_more) + stations.remove(stations.to_array ()[(int)_page_size]); + return stations; + } catch (DataProvider.DataError e) + { + throw new SourceError.UNAVAILABLE("Directory Error"); + } + } // next_page + + + /** + * @brief Check if there are more stations to fetch. + * + * @return true if there are more stations, false otherwise. + */ + public bool has_more () + { + return _more; + } // has_more + + + /** + * @brief Convert RadioBrowser.Station objects to Model.Station objects. + * + * @param raw_stations An iterator of RadioBrowser.Station objects. + * @return An ArrayList of converted Model.Station objects. + */ + private Set convert_stations (Iterator raw_stations) + { + var stations = new HashSet (); + + while (raw_stations.next()) + { + var station = raw_stations.get (); + station.station_star_changed_sig.connect (() => + { + _star_store.update_from_station(station); + }); + stations.add (station); + } + return stations; + } // convert_stations +} // StationSet diff --git a/src/Controllers/PlayerController.vala b/src/Controllers/PlayerController.vala index afbadef..0b92645 100644 --- a/src/Controllers/PlayerController.vala +++ b/src/Controllers/PlayerController.vala @@ -1,137 +1,456 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file PlayerController.vala */ +using Gst; + /** * @class Tuner.PlayerController * @brief Manages the playback of radio stations. * - * This class handles the player state, volume control, and title extraction + * This class handles the player state, volume control, and metadata extraction * from the media stream. It emits signals when the station, state, title, * or volume changes. */ -public class Tuner.PlayerController : Object { - private Model.Station _station; - private Gst.PlayerState? _current_state = Gst.PlayerState.STOPPED; - public Gst.Player player; - public string currentTitle = " "; +public class Tuner.PlayerController : GLib.Object +{ + /** + * @brief the Tuner play state + * + * Using our own play state keeps gstreamer deps out of the rest of the code + */ + public enum Is { + BUFFERING, + PAUSED, + PLAYING, + STOPPED, + STOPPED_ERROR + } // Is + /** Signal emitted when the station changes. */ - public signal void station_changed (Model.Station station); + public signal void station_changed_sig (Model.Station station); + /** Signal emitted when the player state changes. */ - public signal void state_changed (Gst.PlayerState state); - /** Signal emitted when the title changes. */ - public signal void title_changed (string title); + public signal void state_changed_sig (Model.Station station, Is state); + + // /** Signal emitted when the title changes. */ + public signal void metadata_changed_sig (Model.Station station, Metadata metadata); + /** Signal emitted when the volume changes. */ - public signal void volume_changed (double volume); + public signal void volume_changed_sig (double volume); - construct { - player = new Gst.Player (null, null); - player.state_changed.connect ((state) => { - // Don't forward flickering between playing and buffering - if (!(current_state == Gst.PlayerState.PLAYING && state == Gst.PlayerState.BUFFERING) && !(state == current_state)) { - state_changed (state); - current_state = state; - } + /** Signal emitted every ten minutes that a station has been playing continuously. */ + public signal void tape_counter_sig (Model.Station station); + + /** @brief Signal emitted when the shuffle mode changes */ + public signal void shuffle_mode_sig(bool shuffle); + + /** @brief Signal emitted when the shuffle is requested */ + public signal void shuffle_requested_sig(); + + /** The error received when playing, if any */ + public bool play_error{ get; private set; } + + private const uint TEN_MINUTES_IN_SECONDS = 606; // tape counter timer - 10 mins plus 1% + + private Player _player; + private Model.Station _station; + private Metadata _metadata; + private Is _player_state; + private string _player_state_name; + private uint _tape_counter_id = 0; + + + construct + { + _player = new Player (null, null); + + _player.error.connect ((error) => + // There was an error playing the stream + { + play_error = true; + info (@"player error on url $(_player.uri): $(error.message)"); }); - player.media_info_updated.connect ((obj) => { - string? title = extract_title_from_stream (obj); - if (title != null) { - debug(@"Got new title from station: $title"); - currentTitle = title; - title_changed(title); - } + + _player.media_info_updated.connect ((obj) => + // Stream metadata received + { + if (_metadata.process_media_info_update (obj)) + metadata_changed_sig (_station, _metadata); + }); + + _player.volume_changed.connect ((obj) => + // Volume changed + { + volume_changed_sig(obj.volume); + app().settings.volume = obj.volume; }); - player.volume_changed.connect ((obj) => { - volume_changed(obj.volume); + + _player.state_changed.connect ((state) => + // Play state changed + { + // Don't forward flickering between playing and buffering + if ( !(state == PlayerState.PLAYING && state == PlayerState.BUFFERING) + && (_player_state_name != state.get_name ())) + { + _player_state_name = state.get_name (); + set_play_state (state.get_name ()); + } }); - } + + } // construct + /** - * @return The current state of the player. + * @brief Process the Player play state changes emited from gstreamer. + * + * Actions are set in a seperate thread as attempting UI interaction + * on the gstreamer signal results in a seg fault */ - public Gst.PlayerState? current_state { - get { - return _current_state; - } + private void set_play_state (string state) + { + switch (state) { + case "playing": + Gdk.threads_add_idle (() => { + player_state = Is.PLAYING; + return false; + }); + break; - set { - _current_state = value; + case "buffering": + Gdk.threads_add_idle (() => { + player_state = Is.BUFFERING; + return false; + }); + break; + + default : // STOPPED: + if ( play_error ) + { + Gdk.threads_add_idle (() => { + player_state = Is.STOPPED_ERROR; + return false; + }); + } + else + { + Gdk.threads_add_idle (() => { + player_state = Is.STOPPED; + return false; + }); + } + break; } - } + } // set_reverse_symbol + /** + * @brief Player State getter/setter + * + * Set by player signal. Does the tape counter emit + */ + public Is player_state { + get { + return _player_state; + } // get + + private set { + _player_state = value; + state_changed_sig( _station, value ); + + if (value == Is.STOPPED || value == Is.STOPPED_ERROR) + { + if (_tape_counter_id > 0) + { + Source.remove(_tape_counter_id); + _tape_counter_id = 0; + } + } + else if (value == Is.PLAYING) + { + _tape_counter_id = Timeout.add_seconds_full(Priority.LOW, TEN_MINUTES_IN_SECONDS, () => + { + tape_counter_sig(_station); + return Source.CONTINUE; + }); + } + } // set + } // player_state + + + /** + * @brief Station * @return The current station being played. */ public Model.Station station { get { return _station; } - set { - _station = value; - play_station (_station); + if ( ( _station == null ) || ( _station != value ) ) + { + _metadata = new Metadata(); + _station = value; + play_station (_station); + } } - } + } // station + /** + * @brief Volume * @return The current volume of the player. */ public double volume { - get { return player.volume; } - set { player.volume = value; } + get { return _player.volume; } + set { _player.volume = value; } } - /** - * Plays the specified station. - * @param station The station to play. - */ - public void play_station (Model.Station station) { - player.uri = station.url; - player.play (); - station_changed (station); - } + +/** + * @brief Plays the specified station. + * + * @param station The station to play. + */ + public void play_station (Model.Station station) + { + _player.stop (); + _station = station; + station_changed_sig (_station); + _player.uri = (_station.urlResolved != null && _station.urlResolved != "") ? _station.urlResolved : _station.url; + play_error = false; + Timeout.add (500, () => + // Wait a half of a second to play the station to help flush metadata + { + _player.play (); + return Source.REMOVE; + }); + } // play_station + /** - * Checks if the player can play the current station. - * @return True if a station is set, false otherwise. + * @brief Checks if the player has a station to play. + * + * @return True if a station is ready to be played */ public bool can_play () { return _station != null; - } + } // can_play + /** - * Toggles play/pause state of the player. + * @brief Toggles play/pause state of the player. */ - public void play_pause () { - switch (_current_state) { - case Gst.PlayerState.PLAYING: - case Gst.PlayerState.BUFFERING: - player.stop (); + public void play_pause () { + switch (_player_state) { + case Is.PLAYING: + case Is.BUFFERING: + _player.stop (); break; default: - player.play (); + _player.play (); break; } - } + } // play_pause + /** - * Extracts the title from the media stream. - * @param media_info The media information from which to extract the title. - * @return The extracted title, or null if not found. + * @brief Stops the player + * */ - private string? extract_title_from_stream (Gst.PlayerMediaInfo media_info) { - string? title = null; - var streamlist = media_info.get_stream_list ().copy (); - foreach (var stream in streamlist) { - var tags = stream.get_tags (); - tags.foreach ((list, tag) => { - if (tag == "title") { - list.get_string(tag, out title); - } - }); - } - return title; - } -} + public void stop () { + _player.stop (); + } // stop + + + public void shuffle () + { + shuffle_requested_sig(); + } // shuffle + +/** + * @class Metadata + * + * @brief Stream Metadata transform + * + */ + public class Metadata : GLib.Object + { + private static string[,] METADATA_TITLES = + // Ordered array of tags and descriptions + { + {"title", _("Title") }, + {"artist", _("Artist") }, + {"album", _("Album") }, + {"image", _("Image") }, + {"genre", _("Genre") }, + {"homepage", _("Homepage") }, + {"organization", _("Organization") }, + {"location", _("Location") }, + {"extended-comment", _("Extended Comment") }, + {"bitrate", _("Bitrate") }, + {"audio-codec", _("Audio Codec") }, + {"channel-mode", _("Channel Mode") }, + {"track-number", _("Track Number") }, + {"track-count", _("Track Count") }, + {"nominal-bitrate", _("Nominal Bitrate") }, + {"minimum-bitrate", _("Minimum Bitrate") }, + {"maximum-bitrate", _("Maximim Bitrate") }, + {"container-format", ("Container Format") }, + {"application-name", _("Application Name") }, + {"encoder", _("Encoder") }, + {"encoder-version", _("Encoder Version") }, + {"datetime", _("Date Time") }, + {"private-data", _("Private Data") }, + {"has-crc", _("Has CRC") } + }; + + private static Gee.List METADATA_TAGS = new Gee.ArrayList (); + + static construct { + + uint8 tag_index = 0; + foreach ( var tag in METADATA_TITLES ) + // Replicating the order in METADATA_TITLES + { + if ((tag_index++)%2 == 0) + METADATA_TAGS.insert (tag_index/2, tag ); + } + } + + public string all_tags { get; private set; default = ""; } + public string title { get; private set; default = ""; } + public string artist { get; private set; default = ""; } + public string image { get; private set; default = ""; } + public string genre { get; private set; default = ""; } + public string homepage { get; private set; default = ""; } + public string audio_info { get; private set; default = ""; } + public string org_loc { get; private set; default = ""; } + public string pretty_print { get; private set; default = ""; } + + private Gee.Map _metadata_values = new Gee.HashMap(); // Hope it come out in order + + + /** + * Extracts the metadata from the media stream. + * + * @param media_info The media information stream + * @return true if the metadata has changed + */ + internal bool process_media_info_update (PlayerMediaInfo media_info) + { + var streamlist = media_info.get_stream_list ().copy (); + + title = ""; + artist = ""; + image = ""; + genre = ""; + homepage = ""; + audio_info = ""; + org_loc = ""; + pretty_print = ""; + + foreach (var stream in streamlist) // Hopefully just one metadata stream + { + var? tags = stream.get_tags (); // Get the raw tags + + if (tags == null) + break; // No tags, break on this metadata stream + + if (all_tags == tags.to_string ()) + return false; // Compare to all tags and if no change return false + + all_tags = tags.to_string (); + debug(@"All Tags: $all_tags"); + + string? s = null; + bool b = false; + uint u = 0; + + tags.foreach ((list, tag) => + { + var index = METADATA_TAGS.index_of (tag); + + if (index == -1) + { + warning(@"New meta tag: $tag"); + return; + } + + var type = (list.get_value_index(tag, 0)).type(); + + switch (type) + { + case GLib.Type.STRING: + list.get_string(tag, out s); + _metadata_values.set ( tag, s); + break; + case GLib.Type.UINT: + list.get_uint(tag, out u); + if ( u > 1000) + _metadata_values.set ( tag, @"$(u/1000)K"); + else + _metadata_values.set ( tag, u.to_string ()); + break; + case GLib.Type.BOOLEAN: + list.get_boolean (tag, out b); + _metadata_values.set ( tag, b.to_string ()); + break; + default: + warning(@"New Tag type: $(type.name())"); + break; + } + }); // tags.foreach + + if (_metadata_values.has_key ("title" )) + _title = _metadata_values.get ("title"); + if (_metadata_values.has_key ("artist" )) + _artist = _metadata_values.get ("artist"); + if (_metadata_values.has_key ("image" )) + _image = _metadata_values.get ("image"); + if (_metadata_values.has_key ("genre" )) + _genre = _metadata_values.get ("genre"); + if (_metadata_values.has_key ("homepage" )) + _homepage = _metadata_values.get ("homepage"); + + if (_metadata_values.has_key ("audio_codec" )) + _audio_info = _metadata_values.get ("audio_codec "); + if (_metadata_values.has_key ("bitrate" )) + _audio_info += _metadata_values.get ("bitrate "); + if (_metadata_values.has_key ("channel_mode" )) + _audio_info += _metadata_values.get ("channel_mode"); + if (_audio_info != null && _audio_info.length > 0) + _audio_info = safestrip(_audio_info); + + if (_metadata_values.has_key ("organization" )) + _org_loc = _metadata_values.get ("organization "); + if (_metadata_values.has_key ("location" )) + _org_loc += _metadata_values.get ("location"); + if (_org_loc != null && _org_loc.length > 0) + org_loc = safestrip(_org_loc); + + StringBuilder sb = new StringBuilder (); + foreach ( var tag in METADATA_TAGS ) + // Pretty print + { + if (_metadata_values.has_key(tag)) + { + sb.append ( METADATA_TITLES[METADATA_TAGS.index_of (tag),1]) + .append(" : ") + .append( _metadata_values.get (tag)) + .append("\n"); + } + } + pretty_print = sb.truncate (sb.len-1).str; + } // foreach + + return true; + } // process_media_info_update + } // Metadata +} // PlayerController diff --git a/src/Controllers/SearchController.vala b/src/Controllers/SearchController.vala new file mode 100644 index 0000000..5a2eafc --- /dev/null +++ b/src/Controllers/SearchController.vala @@ -0,0 +1,123 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file SearchController.vala + * + * @since 2.0.0 + * + * @brief Defines the below-headerbar display of stations in the Tuner application. + * + * This file contains the Display class, which implements various + * features such as a source list, content stack that display and manage Station + * settings and handles user actions like station selection. + * + * @see Tuner.DirectoryController + * @see Tuner.StationListHookup + * @see Tuner.StationListBox + */ + +using Gee; + +/** + * A controller class that handles searching functionality within the Tuner application. + * + * This class manages search operations and provides an interface for performing + * searches within the application. + */ +public class Tuner.SearchController : Object +{ + private const uint SEARCH_DELAY = 250; + public signal void search_for_sig(string text); + + private DirectoryController _directory; + private StationListBox _station_list_box; + private StationListHookup _station_list_hookup; + private uint _max_search_results; + private uint _search_handler_id = 0; + private string _current_search_term; + + /** + * Controller class for handling station search functionality. + * + * @param dc The directory controller instance to manage station directories + * @param slh The station list hookup instance for station data binding + * @param slb The station list box widget instance to display search results + * @param max_search_results Maximum number of search results to display (defaults to 100) + */ + public SearchController(DirectoryController dc, StationListHookup slh, StationListBox slb, uint max_search_results = 100) + { + Object(); + _directory = dc; + _station_list_hookup = slh; + _station_list_box = slb; + _max_search_results = max_search_results; + } // SearchController + + /** + * @brief Handles a search request. + * + * This method is called when the user types in the search entry. + * It cancels any ongoing search and starts a new search after a brief delay. + * + * @param search_term The search term to search for. + */ + public void handle_search_for(string search_term) + { + var search = search_term.strip(); + if (search.length == 0 || _current_search_term == search) + return; // No new search + + _current_search_term = search; + + if (_search_handler_id > 0) + // Cancel any ongoing search + { + Source.remove(_search_handler_id); + _search_handler_id = 0; + } + + _search_handler_id = Timeout.add(SEARCH_DELAY, () => + // After a brief delay, start the search + { + _search_handler_id = 0; + load_station_search_results.begin(search, _station_list_box); + return Source.REMOVE; + }); // _search_handler_id + } // handle_search_for + + /** + * @brief Loads search stations based on the provided text and updates the content box. + * + * Async since 1.5.5 so that UI is responsive during long searches + * @param search_term The text to search for stations. + * @param results_box The ContentBox to update with the search results. + */ + private async void load_station_search_results(string search_term, StationListBox results_box) + throws SourceError + { + var station_set = _directory.load_search_stations(search_term, 100); + + try + { + var stations = yield station_set.next_page_async(); // Loads results using async call + + if (stations == null || stations.size == 0) + { + results_box.show_nothing_found(); + } + else + { + var _slist = StationList.with_stations(stations); + _station_list_hookup.station_list_hookup(_slist); + results_box.parameter = search_term; // set parameter first as content sets off a signal + results_box.content = _slist; + } + } catch (SourceError e) + { + results_box.show_alert(); + } + results_box.show_all(); + } // load_search_stations +} // SearchController diff --git a/src/Main.vala b/src/Main.vala index 09a20a5..a45187b 100644 --- a/src/Main.vala +++ b/src/Main.vala @@ -1,6 +1,13 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file Main.vala + * + * @brief Tuner application entry point + * */ public static int main (string[] args) { diff --git a/src/Models/Genre.vala b/src/Models/Genre.vala index 45f1d29..1e65344 100644 --- a/src/Models/Genre.vala +++ b/src/Models/Genre.vala @@ -3,30 +3,87 @@ * SPDX-FileCopyrightText: 2020-2022 Louis Brauer */ -namespace Tuner.Model { - public class Genre { - public string name; - public string[] tags; - - public Genre (string name, string[] tags) { - this.name = name; - this.tags = tags; +using Gee; + +namespace Tuner.Model.Genre { + + private static Set PREDEFINED; + + public static bool in_genre(string genre) + { + if ( PREDEFINED == null) + { + PREDEFINED = new HashSet(); + foreach( var a in GENRES) {PREDEFINED.add(a); } + foreach( var a in SUBGENRES) {PREDEFINED.add(a); } + foreach( var a in ERAS) {PREDEFINED.add(a); } + foreach( var a in TALK) {PREDEFINED.add(a); } } + return PREDEFINED.contains(genre); } - public Genre[] genres() { - return { - new Genre (_("70s"), {"70s"}), - new Genre (_("80s"), {"80s"}), - new Genre (_("90s"), {"90s"}), - new Genre (_("Classical"), {"classical"}), - new Genre (_("Country"), {"country"}), - new Genre (_("Dance"), {"dance"}), - new Genre (_("Electronic"), {"electronic"}), - new Genre (_("House"), {"house"}), - new Genre (_("Jazz"), {"jazz"}), - new Genre (_("Pop"), {"pop"}), - new Genre (_("Rock"), {"rock"}) - }; - } + public const string[] GENRES = { + "Blues", + "Classical", + "Country", + "Dance", + "Disco", + "Easy", + "Folk", + "Hits", + "Jazz", + "Oldies", + "Pop", + "Rap", + "Rock", + "Soul" + }; + + public const string[] SUBGENRES = { + "Alternative", + "Ambient", + "Club", + "Electronic", + "Funk", + "HipHop", + "House", + "Indie", + "Metal", + "Latino", + "Punk", + "Reggae", + "Salsa", + "World Music" + }; + + public const string[] ERAS = { + "40s", + "50s", + "60s", + "70s", + "80s", + "90s", + "2000s", + "2010s", + "Contemporary" + }; + + public const string[] TALK = + { "AM" + ,"Comedy" + ,"College Radio" + ,"Community Radio" + ,"Culture" + ,"Educational" + ,"Kids" + ,"Public Radio" + ,"News" + ,"Religion" + ,"Sport" + ,"Talk" + }; } + + + + diff --git a/src/Models/Station.vala b/src/Models/Station.vala index 7077b40..aadafa3 100644 --- a/src/Models/Station.vala +++ b/src/Models/Station.vala @@ -1,39 +1,516 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file Station.vala + * + * @brief Station metadata and related cachable objects + * */ -public class Tuner.Model.Station : Object { - public string id { get; set; } - public string title { get; set; } - public string location { get; set; } - public string url { get; set; } - public bool starred { get; set; } - public string homepage { get; set; } - public string codec { get; set; } - public int bitrate { get; set; } - - public string? favicon_url { get; set; } - public bool favicon_load_error { get; set; } - public uint clickcount = 0; - - public Station (string id, string title, string location, string url) { - Object (); - - this.id = id; - this.title = title; - this.location = location; - this.url = url; - this.starred = starred; - this.favicon_load_error = false; // Is favicon load erring? Limit retry reloads during this session - } +using Gee; + +/** + * @class Station + * @brief Represents a radio station with various properties. + */ +public class Tuner.Model.Station : Object +{ + + // ---------------------------------------------------------- + // statics + // ---------------------------------------------------------- + + // Stations with Favicons that failed to load + private static Set STATION_FAILING_FAVICON = new HashSet(); + + // Core set of all station so far retrieved + private static Map STATIONS = new HashMap(); + + private const int FADE_MS = 400; + + // Signals + public signal void station_star_changed_sig( bool starred ); // Station starred state has changed - public void toggle_starred () { - this.starred = !this.starred; + public signal void station_favicon_sig(); // Station favicon loaded + + // ---------------------------------------------------------- + // Properties + // ---------------------------------------------------------- + + /** @property {string} changeuuid - Unique identifier for the change. */ + public string changeuuid { get; private set; } + /** @property {string} stationuuid - Unique identifier for the station. */ + public string stationuuid { get; private set; } + /** @property {string} name - Name of the station. */ + public string name { get; private set; } + /** @property {string} url - URL of the station stream. */ + public string url { get; private set; } + /** @property {string} url_resolved - Resolved URL of the station stream. Camel case so goobject serialize is OK*/ + public string urlResolved { get; private set; } + /** @property {string} homepage - Homepage of the station. */ + public string homepage { get; private set; } + /** @property {string} favicon - Favicon URL of the station. */ + public string favicon { get; private set ; } + /** @property {string} tags - Tags associated with the station. */ + public string tags { get; private set ; } + /** @property {string} country - Country where the station is located. */ + public string country { get; private set ; } + /** @property {string} countrycode - Country code of the station. */ + public string countrycode { get; private set ; } + /** @property {string} iso_3166_2 - ISO 3166-2 code for the station's location. */ + public string iso_3166_2 { get; private set ; } + /** @property {string} state - State where the station is located. */ + public string state { get; private set ; } + /** @property {string} language - Language of the station. */ + public string language { get; private set ; } + /** @property {string} languagecodes - Language codes associated with the station. */ + public string languagecodes { get; private set ; } + /** @property {string} codec - Audio codec used by the station. */ + public string codec { get; private set ; } + /** @property {int} bitrate - Bitrate of the station stream. */ + public int bitrate { get; private set ; } + /** @property {int} hls - HLS status of the station. */ + public int hls { get; private set ; } + + + // ---------------------------------------------------------- + // Non-Properties + // ---------------------------------------------------------- + + /** @property {int} votes - Number of votes for the station. */ + public int votes; + /** @property {string} lastchangetime - Last change time of the station. */ + public string lastchangetime; + /** @property {string} lastchangetime_iso8601 - Last change time in ISO 8601 format. */ + public string lastchangetime_iso8601; + /** @property {int} lastcheckok - Status of the last check (0 or 1). */ + public int lastcheckok; + /** @property {string} lastchecktime - Last check time of the station. */ + public string lastchecktime; + /** @property {string} lastchecktime_iso8601 - Last check time in ISO 8601 format. */ + public string lastchecktime_iso8601; + /** @property {string} lastcheckoktime - Last successful check time. */ + public string lastcheckoktime ; + /** @property {string} lastcheckoktime_iso8601 - Last successful check time in ISO 8601 format. */ + public string lastcheckoktime_iso8601; + /** @property {string} lastlocalchecktime - Last local check time. */ + public string lastlocalchecktime; + /** @property {string} lastlocalchecktime_iso8601 - Last local check time in ISO 8601 format. */ + public string lastlocalchecktime_iso8601; + /** @property {string} clicktimestamp - Timestamp of the last click. */ + public string clicktimestamp; + /** @property {string} clicktimestamp_iso8601 - Last click timestamp in ISO 8601 format. */ + public string clicktimestamp_iso8601; + /** @property {int} clickcount - Number of clicks on the station. */ + public int clickcount; + /** @property {int} clicktrend - Trend of clicks on the station. */ + public int clicktrend; + /** @property {int} ssl_error - SSL error status. */ + public int ssl_error; + /** @property {string} geo_lat - Latitude of the station's location. */ + public string geo_lat; + /** @property {string} geo_long - Longitude of the station's location. */ + public string geo_long; + /** @property {bool} has_extended_info - Indicates if extended info is available. */ + public bool has_extended_info; + + private bool _starred; + /** @property {bool} starred - Indicates if the station is starred. Only set by Favorites*/ + public bool starred { + get { return _starred; } + set { + if ( _starred == value ) return; + _starred = value; + station_star_changed_sig(_starred ); + } } + + public int favicon_loaded; // Indicates the number of times the favicon has been loaded from cache or internet + public bool is_in_index; // Indicates if the station is in the provider index + public bool is_up_to_date; // Indicates if the station is up-to-date with the provider index + public string up_to_date_difference = _("Station no longer in the index"); + + + // ---------------------------------------------------------- + // Privates + // ---------------------------------------------------------- + + private Uri _favicon_uri; + private Gdk.Pixbuf _favicon_pixbuf; // Favicon for this station + private string _favicon_cache_file; + + + // ---------------------------------------------------------- + // Functions + // ---------------------------------------------------------- + + /** + * @brief Returns a unique, initiated Station instance for a given JSON node. + * + * If station has already been initiated based on stationuuid, returns the existing Station + * Checks with StarStore and sets stations starred status + * + * @param {Json.Node} json_node - The JSON node containing station data. + * @return {Station} The created Station instance. + */ + public static Station make(Json.Node json_node) + { + Station station = new Station.basic(json_node); + station.is_in_index = true; + station.is_up_to_date = true; // Assume loaded from the provider as we're adding this to the list + station.up_to_date_difference = ""; + station.starred = app().stars.contains(station); + + if ( !STATIONS.has_key(station.stationuuid)) + /* + Add station to the index and kickoff async load of the favicon + */ + { + STATIONS.set(station.stationuuid,station); + station.load_favicon_async.begin(); + } + + return STATIONS.get(station.stationuuid); + } // make + + + /** + * @brief Constructor a basic Station instance from a JSON node. + * + * @param {Json.Node} json_node - The JSON node containing station data. + */ + public Station.basic(Json.Node json_node) + { + Object(); + + if ( json_node == null ) + { + warning(@"Station - no JSON"); + return; + } + + Json.Object json_object = json_node.get_object(); + + // + // Json is noisy as fields may/may not be returned. Turn off the logging while parsing. + // + var log_handler_1 = GLib.Log.set_handler( + "Json", + GLib.LogLevelFlags.LEVEL_CRITICAL, + (log_domain, log_level, message) => { + // Ignore the warnings + } + ); + + var log_handler_2 = GLib.Log.set_handler( + null, + GLib.LogLevelFlags.LEVEL_CRITICAL, + (log_domain, log_level, message) => { + // Ignore the warnings + } + ); + + try + { + // Deserialize properties manually + // Put in a try/finally as much for visuals as anything + changeuuid = json_object.get_string_member("changeuuid").strip(); + stationuuid = json_object.get_string_member("stationuuid").strip(); + name = json_object.get_string_member("name").strip(); + url = json_object.get_string_member("url").strip(); + urlResolved = json_object.get_string_member("url_resolved").strip(); + homepage = json_object.get_string_member("homepage").strip(); + favicon = json_object.get_string_member("favicon").strip(); + tags = json_object.get_string_member("tags").strip(); + country = json_object.get_string_member("country").strip(); + countrycode = json_object.get_string_member("countrycode").strip(); + iso_3166_2 = json_object.get_string_member("iso_3166_2").strip(); + state = json_object.get_string_member("state").strip(); + language = json_object.get_string_member("language").strip(); + languagecodes = json_object.get_string_member("languagecodes").strip(); + votes = (int)json_object.get_int_member("votes"); + lastchangetime = json_object.get_string_member("lastchangetime").strip(); + lastchangetime_iso8601 = json_object.get_string_member("lastchangetime_iso8601").strip(); + codec = json_object.get_string_member("codec").strip(); + bitrate = (int)json_object.get_int_member("bitrate"); + hls = (int)json_object.get_int_member("hls"); + lastcheckok = (int)json_object.get_int_member("lastcheckok"); + lastchecktime = json_object.get_string_member("lastchecktime").strip(); + lastchecktime_iso8601 = json_object.get_string_member("lastchecktime_iso8601").strip(); + lastcheckoktime = json_object.get_string_member("lastcheckoktime").strip(); + lastcheckoktime_iso8601 = json_object.get_string_member("lastcheckoktime_iso8601").strip(); + lastlocalchecktime = json_object.get_string_member("lastlocalchecktime").strip(); + lastlocalchecktime_iso8601 = json_object.get_string_member("lastlocalchecktime_iso8601").strip(); + lastlocalchecktime_iso8601 = json_object.get_string_member("has_extended_info").strip(); + clicktimestamp = json_object.get_string_member("clicktimestamp").strip(); + clicktimestamp_iso8601 = json_object.get_string_member("clicktimestamp_iso8601").strip(); + clickcount = (int)json_object.get_int_member("clickcount"); + clicktrend = (int)json_object.get_int_member("clicktrend"); + ssl_error = (int)json_object.get_int_member("ssl_error"); + geo_lat = json_object.get_string_member("geo_lat").strip(); + geo_long = json_object.get_string_member("geo_long").strip(); + has_extended_info = json_object.get_boolean_member("has_extended_info"); + + var go_serial_fudge = json_object.get_string_member("urlResolved").strip(); // Serialization camelcase fudge + if (go_serial_fudge != null && go_serial_fudge != "") + { + urlResolved = go_serial_fudge; + } + + // Process favorites + if (json_object.has_member("starred")) + { + _starred = json_object.get_boolean_member("starred"); + } + + /* ----------------------------------------------------------------------- + Process v1 Attribute, if any, from old Favorites format + ----------------------------------------------------------------------- */ + + if (json_object.has_member("id") ) + { + stationuuid = json_object.get_string_member("id").strip(); + } + + if (json_object.has_member("favicon-url") ) + { + favicon = json_object.get_string_member("favicon-url").strip(); + } + + if (json_object.has_member("location") ) + { + country = json_object.get_string_member("location").strip(); + } + + if (json_object.has_member("title") ) + { + name = json_object.get_string_member("title").strip(); + } + } finally { + GLib.Log.remove_handler(null, log_handler_2); + GLib.Log.remove_handler("Json", log_handler_1); + } + + is_in_index = false; // Basic station creation - assume not in provider index + is_up_to_date = false; // Basic station creation - assume not up-to-date with provider + + /* + Favicon setup + */ + favicon_loaded = 0;// Used to notify that favicon loaded + _favicon_cache_file = Path.build_filename(Application.instance.cache_dir, stationuuid); + + if (favicon == null || favicon.length == 0) + { + STATION_FAILING_FAVICON.add(stationuuid); + debug(@"$(stationuuid) - Favicon missing"); + return; + } + + try + { + debug(@"$(stationuuid) - constructed - Start parse favicon URL: $(favicon)"); + _favicon_uri = Uri.parse(favicon, NONE); + } catch (GLib.UriError e) + { + info(@"$(stationuuid) - Failed to parse favicon URL: $(e.message)"); + STATION_FAILING_FAVICON.add(stationuuid); + } + + load_favicon_async.begin(); + } // Station.basic + + + /** + * @brief Toggles the Starred status of the station + * @return {bool} True if Station is Starred. + */ + public bool toggle_starred() + { + starred = !starred; + return _starred; + } // toggle_starred + + + /** + * @brief Returns a string representation of the station. + * @return {string} A string in the format "[id] title". + */ + public string popularity() { + return _(@"Votes: $(votes)\t Clicks: $(clickcount)\t Trend: $(clicktrend)"); + } // to_string + + + /** + * @brief Returns a string representation of the station. + * @return {string} A string in the format "[id] title". + */ public string to_string() { - return @"[$(this.id)] $(this.title)"; - } + return @"[$(stationuuid)] $(name)"; + } // to_string + + + /** + * @brief Asynchronously loads the favicon for the station. + * + * Loads from cache is not requesting reload and cache exists + * Otherwise async calls to the website to download the favicon, + * then stores it in the cache and replaces the Station favicon + * + * @param {bool} reload - Whether to force reload the favicon. + */ + private async void load_favicon_async( bool reload = false ) + { + debug(@"$(stationuuid) - Start - load_favicon_async for favicon: $(favicon)"); + + /* + Get favicon from cache file if file is in cache AND + Not requesting reload Or favicon is currently failing + */ + if ( ( !reload || STATION_FAILING_FAVICON.contains(stationuuid) ) + && FileUtils.test(_favicon_cache_file, FileTest.EXISTS)) + { + try { + var pixbuf = new Gdk.Pixbuf.from_file_at_scale(_favicon_cache_file, 48, 48, true); + _favicon_pixbuf = pixbuf; + debug(@"$(stationuuid) - Complete - load_favicon_async from cache stored in file://$(_favicon_cache_file)"); + favicon_loaded++; + station_favicon_sig(); + return; + } catch (Error e) { + info(@"$(stationuuid) - Failed to load cached favicon: $(e.message)"); + } + } + + if (_favicon_uri == null) + return; // First load or reload requested and favicon is not failing + + uint status_code; + + InputStream? stream = yield HttpClient.GETasync(_favicon_uri, Priority.LOW, out status_code); // Will automatically try several times + + if ( stream != null && status_code == 200 ) + /* + Input stream OK + */ + { + try { + var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(stream, 48, 48, true,null); + pixbuf.save(_favicon_cache_file, "png"); + _favicon_pixbuf = pixbuf; + debug(@"$(stationuuid) - Complete - load_favicon_async from internet for $(_favicon_uri.to_string())\nStored in file://$(_favicon_cache_file)"); + favicon_loaded++; + station_favicon_sig(); + return; + + } catch (Error e) { + debug(@"$(stationuuid) - Failed to process favicon $(_favicon_uri.to_string()) - $(e.message)"); + } + } + info(@"$(stationuuid) - Failed to load favicon $(_favicon_uri.to_string()) - Status code: $(status_code)"); + STATION_FAILING_FAVICON.add(stationuuid); + } // load_favicon_async + + + /** + * @brief Asynchronously sets the given image to the station favicon, if available. + * + * Load the known icons first, and if reload, go around after trying the favicon url + * + * @param {Image} favicon_image - The favicon image to be updated. + * @param {bool} reload - Whether to force reload the favicon from source. + * @return {bool} True if the favicon was available and the image updated. + */ + public async bool update_favicon_image( Gtk.Image favicon_image, bool reload = false, string defaulticon = "") + { + bool reloading = false; + do { + try{ + if ( _favicon_pixbuf == null || STATION_FAILING_FAVICON.contains(stationuuid)) + { + yield fade(favicon_image, FADE_MS, false); + favicon_image.set_from_icon_name(defaulticon,Gtk.IconSize.DIALOG); + yield fade(favicon_image, FADE_MS, true); + } + else{ + + yield fade(favicon_image, FADE_MS, false); + favicon_image.set_from_pixbuf(_favicon_pixbuf); + yield fade(favicon_image, FADE_MS, true); + } + } finally { + favicon_image.opacity = 1; + reloading = false; + } + + if ( reload && favicon_loaded < 2 ) + /* + Reload requested, and favicon has not had reload requested before + */ + { + STATION_FAILING_FAVICON.remove(stationuuid); // Give possible 2nd chance + yield load_favicon_async(true); // Wait for load_favicon_async to complete + reloading = true; + reload = false; + } + } while ( reloading ); + return true; + } // update_favicon_image + + + /** + * @brief Sets the station up-to-date status based on the given station. + * + * @param {Station} p - The station to compare with. + * @return {bool} True if the station is up-to-date with the given station. + */ + public bool set_up_to_date_with(Station? p) + { + if ( p == null || this.stationuuid != p.stationuuid) return false; + if ( + ( this.url == p.url) + && (this.bitrate == p.bitrate) + && ( this.codec == p.codec) + // && (this.changeuuid == p.changeuuid) // TODO Radio browser changeuuids broken + ) + { + is_up_to_date = true; + up_to_date_difference = ""; + } + else + { + StringBuilder sb = new StringBuilder(_("Changes:")); + if ( this.url != p.url) sb.append(_("\n\tStream Url")); + if ( this.urlResolved != p.urlResolved) sb.append(_("\n\tStream Resolved Url")); + if ( this.favicon != p.favicon) sb.append(_("\n\tFavicon address")); + if ( this.homepage != p.homepage) sb.append(_("\n\tHomepage address")); + if ( this.tags != p.tags) sb.append(_("\n\tStation tags")); + if ( this.bitrate != p.bitrate) sb.append(_("\n\tBitrate: $(this.bitrate) > $(p.bitrate)")); + if ( this.codec != p.codec) sb.append(_("\n\todec: $(this.codec) > $(p.codec)")); + // if (this.changeuuid != p.changeuuid) sb.append(_("\nOther minor items have changed")); + // sb.append(@"\n\n stationuuid: $(p.stationuuid) - $(this.stationuuid) "); + // sb.append(@"\n\n changeuuid: $(p.changeuuid) - $(this.changeuuid) "); + // sb.append(@"\n\n urlResolved: $(p.urlResolved) - $(this.urlResolved) "); + // warning(@"$name $(p.stationuuid) - $(this.stationuuid)\n$(p.changeuuid) - $(this.changeuuid)"); + // sb.append(@"\n\n changeuuid: $(p.changeuuid) - $(this.changeuuid) "); + up_to_date_difference = sb.str; + is_up_to_date = false; + } + return true; + } // set_up_to_date_with + -} + /** + * Returns a copy of the current station from the Provider. + * + * @return A new {@link Station} instance with the current properties + */ + public Station updated() + { + return STATIONS.get(stationuuid); + } // updated +} // Station diff --git a/src/Models/StationStore.vala b/src/Models/StationStore.vala deleted file mode 100644 index 792f921..0000000 --- a/src/Models/StationStore.vala +++ /dev/null @@ -1,160 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - - /** - StationStore - - Store and retrieve a collection of stations in a JSON file, i.e. favorites - - Uses libgee for data structures. - */ - -using Gee; - -namespace Tuner.Model { - - public class StationStore : Object - { - private const string M3U8 = "#EXTM3U\n#EXTENC:UTF-8\n#PLAYLIST:Tuner\n"; - private const string M3U8_UUID = "STATIONUUID"; - - private ArrayList _store; - private File _favorites_file; - - public StationStore (string favorites_path) { - Object (); - - _store = new ArrayList (); - _favorites_file = File.new_for_path (favorites_path); - ensure (); - load (); - debug (@"store initialized in path $favorites_path"); - } - - public void add (Station station) { - _add (station); - persist (); - } - - private void _add (Station station) { - _store.add (station); - // TODO Should we do a sorted insert? - } - - public void remove (Station station) { - _store.remove (station); - persist (); - } - - private void ensure () { - // Non-racy approach is to try to create the file first - // and ignore errors if it already exists - try { - var df = _favorites_file.create (FileCreateFlags.PRIVATE); - df.close (); - debug (@"store created"); - } catch (Error e) { - // Ignore, file already existed, which is good - } - } - - private void load () { - debug ("loading store"); - Json.Parser parser = new Json.Parser (); - - try { - var stream = _favorites_file.read (); - parser.load_from_stream (stream); - stream.close (); - } catch (Error e) { - warning (@"Load failed with error: $(e.message)"); - } - - Json.Node? node = parser.get_root (); - - if ( node == null ) return; // No favorites store - - Json.Array array = node.get_array (); // Json-CRITICAL **: 21:02:51.821: json_node_get_array: assertion 'JSON_NODE_IS_VALID (node)' failed - array.foreach_element ((a, i, elem) => { // json_array_foreach_element: assertion 'array != NULL' failed - Station station = Json.gobject_deserialize (typeof (Station), elem) as Station; - // TODO This should probably not be here but in - // DirectoryController - station.notify["starred"].connect ( (sender, property) => { - if (station.starred) { - this.add (station); - } else { - this.remove (station); - } - }); - - _add (station); - }); - - debug (@"loaded store size: $(_store.size)"); - } - - private void persist () { - debug ("persisting store"); - var data = serialize (); - - try { - _favorites_file.delete (); - var stream = _favorites_file.create ( - FileCreateFlags.REPLACE_DESTINATION | FileCreateFlags.PRIVATE - ); - var s = new DataOutputStream (stream); - s.put_string (data); - s.flush (); - s.close (); // closes base stream also - } catch (Error e) { - warning (@"Persist failed with error: $(e.message)"); - } - } - - public string serialize () { - Json.Builder builder = new Json.Builder (); - builder.begin_array (); - foreach (var station in _store) { - var node = Json.gobject_serialize (station); - builder.add_value (node); - } - builder.end_array (); - - Json.Generator generator = new Json.Generator (); - generator.set_root (builder.get_root ()); - string data = generator.to_data (null); - return data; - } - - public ArrayList get_all () { - return _store; - } - - public bool contains (Station station) { - foreach (var s in _store) { - if (s.id == station.id) { - return true; - } - } - return false; - } - - /** - * @brief Creates a string of Starred stations in m3u format - * @return A string representation of the favorites formated as M3U - */ - public string export_m3u8() - { - StringBuilder playlist = new StringBuilder(M3U8); - foreach ( var station in _store) - { - playlist.append (@"#EXTINF:-1,$(station.title) - logo=\"$(station.favicon_url)\",$M3U8_UUID=\"$(station.id)\"\n$(station.url)\n#EXTIMG:$(station.favicon_url)\n"); - } - - return playlist.str; - - } // export_m3u8 - } // StationStore -} // Tuner.Model \ No newline at end of file diff --git a/src/Providers/RadioBrowser.vala b/src/Providers/RadioBrowser.vala new file mode 100644 index 0000000..df7bd79 --- /dev/null +++ b/src/Providers/RadioBrowser.vala @@ -0,0 +1,710 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file RadioBrowser.vala + * + * @brief Interface to radio-browser.info API and servers + * + */ + +using Gee; + +/** + * @namespace Tuner.RadioBrowser + * + * @brief DataProvider implementation for radio-browser.info API and servers + * + * This namespace provides functionality to interact with the radio-browser.info API. + * It includes features for: + * - Retrieving radio station metadata JSON + * - Executing searches and retrieving radio station metadata JSON + * - Reporting back user interactions (voting, listen tracking) + * - Tag and other metadata retrieval + * - API Server discovery and connection handling from DNS and from round-robin API server + */ +namespace Tuner.DataProvider { + + private const string SRV_SERVICE = "api"; + private const string SRV_PROTOCOL = "tcp"; + private const string SRV_DOMAIN = "radio-browser.info"; + + private const string RBI_ALL_API = "all.api.radio-browser.info"; // Round-robin API address + private const string RBI_STATS = "/json/stats"; + private const string RBI_SERVERS = "/json/servers"; + + // RB Queries + private const string RBI_STATION = "/json/url/$stationuuid"; + private const string RBI_SEARCH = "/json/stations/search"; + private const string RBI_VOTE = "/json/vote/$stationuuid"; + private const string RBI_UUID = "/json/stations/byuuid"; + private const string RBI_URL = "/json/stations/byurl"; + private const string RBI_TAGS = "/json/tags"; + + + + /** + * A data provider implementation for the Radio Browser API. + * + * RadioBrowser class implements the DataProvider.API interface to fetch + * radio station data from the Radio Browser service. + * + * See https://www.radio-browser.info/ for API details. + */ + public class RadioBrowser : Object, DataProvider.API + { + private const int DEGRADE_CAPITAL = 100; + private const int DEGRADE_COST = 7; + + private string? _optionalservers; + private ArrayList _servers; + private string _current_server = RBI_ALL_API; + private int _degrade = DEGRADE_CAPITAL; + private int _available_tags = 1000; // default guess + + + public string name() + { + return @"RadioBrowser 2.0\n\nServer: $_current_server"; + } + + + public Status status { get; protected set; } + + public DataError? last_data_error { get; set; } + + public int available_tags() + { + return _available_tags; + } + + + /** + * @brief Constructor for RadioBrowser Client + * + * @throw DataError if unable to initialize the client + */ + public RadioBrowser(string? optionalservers ) + { + Object( ); + _optionalservers = optionalservers; + status = NOT_AVAILABLE; + } // RadioBrowser + + + /** + * @brief Builds a Glib Uri from path and query + * + * Uses Glib Uri Parse with encoding to correctly check and encode the query + * + * @return the Uri if successful + */ + private Uri? build_uri(string path, string query = "") + { + debug(@"http://$_current_server$path?$query"); + try { + if (query == "") + { + return Uri.parse(@"http://$_current_server$path",UriFlags.ENCODED); + } + else + { + return Uri.parse(@"http://$_current_server$path?$query",UriFlags.ENCODED); + } + } catch (UriError e) + { + warning(@"Server: $_current_server Path: $path Query: $query Error: $(e.message)"); + } + return null; + } // build_uri + + + /** + * @brief Initialize the DataProvider implementation + * + * @return true if initialization successful + */ + public bool initialize() + { + if ( app().is_offline ) return false; + + if (_optionalservers != null) + // Run time server parameter was passed in + { + _servers = new Gee.ArrayList.wrap(_optionalservers.split(":")); + } else + // Identify servers from DNS or API + { + try { + _servers = get_srv_api_servers(); + } catch (DataError e) { + last_data_error = new DataError.NO_CONNECTION(@"Failed to retrieve API servers: $(e.message)"); + status = NO_SERVER_LIST; + return false; + } + } + + if (_servers.size == 0) + { + last_data_error = new DataError.NO_CONNECTION("Unable to resolve API servers for radio-browser.info"); + status = NO_SERVERS_PRESENTED; + return false; + } + + choose_server(); + status = OK; + clear_last_error(); + stats(); + return true; + } // initialize + + + /** + * @brief Track a station listen event + * + * @param stationuuid UUID of the station being listened to + */ + public void click(string stationuuid) { + debug(@"sending listening event for station $(stationuuid)"); + uint status_code; + HttpClient.GET(build_uri(RBI_STATION, stationuuid), out status_code); + debug(@"response: $(status_code)"); + } // track + + + /** + * @brief Vote for a station + * @param stationuuid UUID of the station being voted for + */ + public void vote(string stationuuid) { + debug(@"sending vote event for station $(stationuuid)"); + uint status_code; + //var uri = Uri.build(NONE, "http", null, _current_server, -1, RBI_VOTE, stationuuid, null); + HttpClient.GET(build_uri(RBI_VOTE, stationuuid), out status_code); + //HttpClient.GET(@"$(_current_server)/$(RBI_VOTE)/$(stationuuid)", Priority.HIGH, out status_code); + // HttpClient.GETasync(@"$(_current_server)/$(RBI_VOTE)/$(stationuuid)", out status_code); + debug(@"response: $(status_code)"); + } // vote + + + /** + * @brief Get all available tags + * + * @return ArrayList of Tag objects + * @throw DataError if unable to retrieve or parse tag data + */ + public Set get_tags(int offset, int limit) throws DataError { + Json.Node rootnode; + try { + uint status_code; + var query = ""; + if (offset > 0) query = @"offset=$offset"; + if (limit > 0) query = @"$query&limit=$limit"; + + var uri = build_uri(RBI_TAGS, query); + var stream = HttpClient.GET(uri, out status_code); + + if ( status_code != 0 && stream != null) + { + try { + var parser = new Json.Parser(); + parser.load_from_stream(stream); + rootnode = parser.get_root(); + } catch (Error e) { + throw new DataError.PARSE_DATA(@"unable to parse JSON response: $(e.message)"); + } + var rootarray = rootnode.get_array(); + var tags = jarray_to_tags(rootarray); + return tags; + } + } catch (GLib.Error e) { + debug("cannot get_tags()"); + } + return new HashSet(); + } // get_tags + + + /** + * @brief Get a station or stations by UUID + * + * @param uuids comma seperated lists of the stations to retrieve + * @return Station object if found, null otherwise + * @throw DataError if unable to retrieve or parse station data + */ + public Set by_uuid(string uuid) throws DataError { + if ( app().is_offline || safestrip(uuid).length == 0 ) return new HashSet(); + var result = station_query(RBI_UUID,@"uuids=$uuid"); + return result; + } // by_uuid + + + /** + * @brief Get a station or stations by UUIDs + * + * @param a collection of uuids of the stations to retrieve + * @return Station object if found, null otherwise + * @throw DataError if unable to retrieve or parse station data + */ + public Set by_uuids(Collection uuids) throws DataError { + StringBuilder sb = new StringBuilder(); + foreach ( var uuid in uuids) { sb.append(uuid).append(","); } + return by_uuid(sb.str); + } // by_uuid + + + /** + Not implemented + */ + public Set by_url(string url) throws DataError { + if ( app().is_offline || safestrip(url).length == 0 ) return new HashSet(); + var result = station_query(RBI_URL,@"$url"); + return result; + } // by_url + + + /** + * @brief Search for stations based on given parameters + * + * @param params Search parameters + * @param rowcount Maximum number of results to return + * @param offset Offset for pagination + * @return ArrayList of Station objects matching the search criteria + * @throw DataError if unable to retrieve or parse station data + */ + public Set search(SearchParams params, uint rowcount, uint offset = 0) throws DataError + { + // by uuids + if (params.uuids != null) + { + return by_uuids(params.uuids); + } + + // OR + + // by text or tags + var query = get_search_query_params(params,rowcount, offset); + + debug(@"Search: $(query)"); + return station_query(RBI_SEARCH, query); + } // search + + + /** + * @brief Search for stations based on given parameters + * + * @param params Search parameters + * @param rowcount Maximum number of results to return + * @param offset Offset for pagination + * @return ArrayList of Station objects matching the search criteria + * @throw DataError if unable to retrieve or parse station data + */ + public async Set search_async(SearchParams params, uint rowcount, uint offset = 0) throws DataError + { + // by uuids + if (params.uuids != null) + { + return by_uuids(params.uuids); + } + + // OR + + // by text or tags + var query = get_search_query_params(params, rowcount, offset); + + debug(@"Search: $(query)"); + return yield station_query_async(RBI_SEARCH, query); + } // search + + + + /* --------------------------------------------------------------- + Private + ---------------------------------------------------------------*/ + + /** + * @brief Get all available tags + * + * @return ArrayList of Tag objects + * @throw DataError if unable to retrieve or parse tag data + */ + private void choose_server() + { + var random_server = Random.int_range(0, _servers.size); + + for (int a = 0; a < _servers.size; a++) + /* Randomly start checking servers, break on first good one */ + { + uint status = 0; + var server = (random_server + a) %_servers.size; + _current_server = _servers[server]; + try { + var uri = Uri.parse(@"http://$_current_server/json/stats",UriFlags.NONE); + //status = HttpClient.HEAD(uri); // RB doesn't support HEAD + HttpClient.GET(uri,out status); + } catch (UriError e) + { + debug(@"Server - bad Uri from $_current_server"); + } + if ( status == 200 ) break; // Check the server + } + debug(@"RadioBrowser Client - Chosen radio-browser.info server: $_current_server"); + } // choose_server + + + /** + * @brief Manages tracking server degradation and new server selection + * + * Tracks degraded server responses and at a certain level will choose + * a new server if possible + */ + private void degrade(bool degraded = true ) + { + if ( !degraded ) + // Track nominal result + { + _degrade += ((_degrade > DEGRADE_CAPITAL) ? 0 : 1); + } + else + // Degraded result + { + warning(@"RadioBrowser degrading server: $_current_server"); + _degrade =- DEGRADE_COST; + if ( _degrade < 0 ) + // This server degraded to zero + { + choose_server(); + _degrade = DEGRADE_CAPITAL; + } + } + } // degrade + + + /** + * @brief Retrieve server stats + * + */ + private void stats() + { + uint status_code; + Json.Node rootnode; + + var stream = HttpClient.GET(build_uri(RBI_STATS), out status_code); + + if ( status_code != 0 && stream != null) + { + try { + var parser = new Json.Parser(); + parser.load_from_stream(stream, null); + rootnode = parser.get_root(); + Json.Object json_object = rootnode.get_object(); + _available_tags = (int)json_object.get_int_member("tags"); + + } catch (Error e) { + warning(@"Could not get server stats: $(e.message)"); + } + } + debug(@"response: $(status_code) - Tags: $(_available_tags)"); + } // stats + + + /** + * @brief Get stations by querying the API + * + * @param query the API query + * @return ArrayList of Station objects + * @throw DataError if unable to retrieve or parse station data + */ + private Set station_query(string path, string query) throws DataError { + + + debug(@"station_query - $(path) $(query)"); + + uint status_code; + var uri = build_uri(path, query); + debug(@"station_query - $(uri.to_string())"); + + var stream = HttpClient.GET(uri, out status_code); + + if (status_code == 200 && stream != null) + { + degrade(false); + try + { + var stations = parse_json_response(stream); + return stations; + } catch (Error e) + { + debug(@"JSON error \"$(e.message)\" for uri $(uri)"); + } + } + else + { + warning(@"Response from 'radio-browser.info': $(status_code) for url: $(uri.to_string())"); + degrade(); + } + return new HashSet(); + } // station_query + + + /** + * @brief Get stations by querying the API + * + * @param query the API query + * @return ArrayList of Station objects + * @throw DataError if unable to retrieve or parse station data + */ + private async Set station_query_async(string path, string query) throws DataError + { + debug(@"station_query - $(path) $(query)"); + + uint status_code; + var uri = build_uri(path, query); + debug(@"station_query - $(uri.to_string())"); + + var stream = yield HttpClient.GETasync(uri, Priority.HIGH_IDLE, out status_code); + + if (status_code == 200 && stream != null) + { + degrade(false); + try + { + var stations = parse_json_response(stream); + return stations; + } catch (Error e) + { + debug(@"JSON error \"$(e.message)\" for uri $(uri)"); + } + } + else + { + warning(@"Response from 'radio-browser.info': $(status_code) for url: $(uri.to_string())"); + degrade(); + } + return new HashSet(); + } // station_query_async + + + + /** + * Generates query parameters string for radio-browser API search requests. + * + * @param params SearchParams object containing search criteria + * @param rowcount Maximum number of results to return + * @param offset Starting position in the result set + * + * @return String containing formatted query parameters + */ + private string get_search_query_params(SearchParams params, uint rowcount, uint offset) + { + // by text or tags + var query = @"limit=$rowcount&order=$(params.order)&offset=$offset"; + + if (params.text != "") { + query += @"&name=$(encode_text(params.text))"; // Encode text for ampersands etc + } + if (params.countrycode.length > 0) { + query += @"&countrycode=$(params.countrycode)"; + } + if (params.order != SortOrder.RANDOM) { + // random and reverse doesn't make sense + query += @"&reverse=$(params.reverse)"; + } + // Put tags last + if (params.tags.size > 0) { + string tag_list = params.tags.to_array()[0]; + if (params.tags.size > 1) { + tag_list = string.joinv(",", params.tags.to_array()); + } + query += @"&tagExact=false&tagList=$(encode_text(tag_list))"; // Encode text for ampersands etc + } + return query; + } // get_search_query_params + + + /** + * Parses JSON response from Radio Browser API into a set of stations. + * + * @param stream The input stream containing JSON data to be parsed + * + * @return Set A set of Station objects parsed from the JSON + * + * @throws Error If there is an error parsing the JSON or processing the data + */ + private Set parse_json_response(InputStream stream) throws Error + { + var parser = new Json.Parser.immutable_new (); + parser.load_from_stream(stream, null); + var rootnode = parser.get_root(); + var rootarray = rootnode.get_array(); + return jarray_to_stations(rootarray); + } // parse_json_response + + + /** + * @brief Marshals JSON array data into an array of Station + * + * Not knowing what the produce did, allow null data + * + * @param data JSON array containing station data + * @return ArrayList of Station objects + */ + private Set jarray_to_stations(Json.Array data) + { + var stations = new HashSet(); + + if ( data != null ) + { + data.foreach_element((array, index, element) => { + Model.Station s = Model.Station.make(element); + stations.add(s); + }); + } + return stations; + } // jarray_to_stations + + + /** + * @brief Converts a JSON node to a Tag object + * + * @param node JSON node representing a tag + * @return Tag object + */ + private Tag jnode_to_tag(Json.Node node) + { + return Json.gobject_deserialize(typeof(Tag), node) as Tag; + } // jnode_to_tag + + + /** + * @brief Marshals JSON tag data into an array of Tag + * + * Not knowing what the produce did, allow null data + * + * @param data JSON array containing tag data + * @return ArrayList of Tag objects + */ + private Set jarray_to_tags(Json.Array? data) + { + var tags = new HashSet(); + + if ( data != null ) + { + data.foreach_element((array, index, element) => { + Tag s = jnode_to_tag(element); + tags.add(s); + }); + } + + return tags; + } // jarray_to_tags + + + /** + * @brief Get all radio-browser.info API servers + * + * Gets server list from Radio Browser DNS SRV record, + * and failing that, from the API + * + * @since 1.5.4 + * @return ArrayList of strings containing the resolved hostnames + * @throw DataError if unable to resolve DNS records + */ + private ArrayList get_srv_api_servers() throws DataError + { + var results = new ArrayList(); + + if ( app().is_offline ) return results; + + try { + /* + DNS SRV record lookup + */ + var srv_targets = GLib.Resolver.get_default().lookup_service(SRV_SERVICE, SRV_PROTOCOL, SRV_DOMAIN, null); + foreach (var target in srv_targets) { + results.add(target.get_hostname()); + } + } catch (GLib.Error e) { + @warning(@"Unable to resolve Radio-Browser SRV records: $(e.message)"); + } + + if (results.is_empty) { + /* + JSON API server lookup as SRV record lookup failed + Get the servers from the API itself from a round-robin server + */ + try { + uint status_code; + + var stream = HttpClient.GET(build_uri(@"$(RBI_SERVERS)"), out status_code); + + debug(@"response from $(_current_server)$(RBI_SERVERS): $(status_code)"); + + if (status_code == 200) { + Json.Node root_node; + + try { + var parser = new Json.Parser(); + parser.load_from_stream(stream); + root_node = parser.get_root(); + } catch (Error e) { + throw new DataError.PARSE_DATA(@"RBI API get servers - unable to parse JSON response: $(e.message)"); + } + + if (root_node != null && root_node.get_node_type() == Json.NodeType.ARRAY) { + root_node.get_array().foreach_element((array, index_, element_node) => { + var object = element_node.get_object(); + if (object != null) { + var name = object.get_string_member("name"); + if (name != null && !results.contains(name)) { + results.add(name); + } + } + }); + } + } + } catch (Error e) { + warning("Failed to parse RBI APIs JSON response: $(e.message)"); + } + } + + debug(@"Results $(results.size)"); + return results; + } // get_srv_api_servers + + + /** + * Encodes a text string for use in RadioBrowser API requests. + * + * This method ensures that the text is properly encoded for use in URLs + * by converting special characters into their URL-safe equivalents. + * + * @param tag The text string to be encoded + * @return A URL-encoded string representation of the input text + */ + private static string encode_text(string tag) + { + + string output = tag; + string[,] x = new string[,] + { + {"%","%25"} + ,{":","%3B"} + ,{"/","%2F"} + ,{"#","%23"} + ,{"?","%3F"} + ,{"&","%26"} + ,{"@","%40"} + ,{"+","%2B"} + ,{" ","%20"} + }; + + for (int a = 0 ; a < 9; a++) + { + output = output.replace(x[a,0], x[a,1]); + } + return output; + } // encode_tag + } // RadioBrowser +} \ No newline at end of file diff --git a/src/Services/DBusMediaPlayer.vala b/src/Services/DBusMediaPlayer.vala index 03349a4..1ec281c 100644 --- a/src/Services/DBusMediaPlayer.vala +++ b/src/Services/DBusMediaPlayer.vala @@ -1,336 +1,384 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file DBusMediaPlayer.vala */ namespace Tuner.DBus { - const string ServerName = "org.mpris.MediaPlayer2.Tuner"; - const string ServerPath = "/org/mpris/MediaPlayer2"; - private bool is_initialized = false; - - public void initialize () { - if (is_initialized) { - // App is already running, do nothing - return; - } - - var owner_id = Bus.own_name( - BusType.SESSION, - ServerName, - BusNameOwnerFlags.NONE, - onBusAcquired, - () => { - is_initialized = true; - }, - () => warning (@"Could not acquire name $ServerName, the DBus interface will not be available") - ); - - if (owner_id == 0) { - warning ("Could not initialize MPRIS session.\n"); - } - } - - void onBusAcquired (DBusConnection conn) { - try { - conn.register_object (ServerPath, new MediaPlayer ()); - conn.register_object (ServerPath, new MediaPlayerPlayer (conn)); - } catch (IOError e) { - error (@"Could not acquire path $ServerPath: $(e.message)"); - } - info (@"DBus Server is now listening on $ServerName $ServerPath…\n"); - } - - public class MediaPlayer : Object, DBus.IMediaPlayer2 { - public void raise() throws DBusError, IOError { - debug ("DBus Raise() requested"); - var now = new DateTime.now_local (); - var timestamp = (uint32) now.to_unix (); - Application.instance.window.present_with_time (timestamp); - } - - public void quit() throws DBusError, IOError { - debug ("DBus Quit() requested"); - } - - public bool can_quit { - get { - return true; - } - } - - public bool can_raise { - get { - return true; - } - } - - public bool has_track_list { - get { - return false; - } - } - - public string desktop_entry { - owned get { - return ((Gtk.Application) GLib.Application.get_default ()).application_id; - } - } - - public string identity { - owned get { - return "Tuner"; - } - } - - public string[] supported_uri_schemes { - owned get { - return {"http", "https"}; - } - } - - // TODO - public string[] supported_mime_types { - owned get { - return {"audio/mp3"}; - } - } - - public bool fullscreen { get; set; default = false; } - public bool can_set_fullscreen { - get { - debug ("CanSetFullscreen() requested"); - return true; - } - } - } - - - public class MediaPlayerPlayer : Object, DBus.IMediaPlayer2Player { - [DBus (visible = false)] - private string _playback_status = "Stopped"; - private string _current_title = ""; - private string _current_artist = "Tuner"; - private string? _current_art_url = null; - private uint update_metadata_source = 0; - private uint send_property_source = 0; - private HashTable changed_properties = null; - - [DBus (visible = false)] - public unowned DBusConnection conn { get; construct set; } - - private const string INTERFACE_NAME = "org.mpris.MediaPlayer2.Player"; - - public MediaPlayerPlayer (DBusConnection conn) { - Object (conn: conn); - Application.instance.player.state_changed.connect ((state) => { - switch (state) { - case Gst.PlayerState.PLAYING: - case Gst.PlayerState.BUFFERING: - playback_status = "Playing"; - break; - case Gst.PlayerState.STOPPED: - playback_status = "Stopped"; - break; - case Gst.PlayerState.PAUSED: - playback_status = "Paused"; - break; - } - }); - - Application.instance.player.title_changed.connect ((title) => { - _current_title = title; - trigger_metadata_update (); + const string ServerName = "org.mpris.MediaPlayer2.Tuner"; + const string ServerPath = "/org/mpris/MediaPlayer2"; + private bool is_initialized = false; + + public void initialize () + { + if (is_initialized) + { + // App is already running, do nothing + return; + } + + var owner_id = Bus.own_name( + BusType.SESSION, + ServerName, + BusNameOwnerFlags.NONE, + onBusAcquired, + () => { + is_initialized = true; + }, + () => warning (@"Could not acquire name $ServerName, the DBus interface will not be available") + ); + + if (owner_id == 0) + { + warning ("Could not initialize MPRIS session.\n"); + } + } // initialize + + + void onBusAcquired (DBusConnection conn) + { + try + { + conn.register_object (ServerPath, new MediaPlayer ()); + conn.register_object (ServerPath, new MediaPlayerPlayer (conn)); + } catch (IOError e) + { + error (@"Could not acquire path $ServerPath: $(e.message)"); + } + info (@"DBus Server is now listening on $ServerName $ServerPath…\n"); + } // onBusAcquired + + + public class MediaPlayer : Object, DBus.IMediaPlayer2 + { + public void raise() throws DBusError, IOError + { + debug ("DBus Raise() requested"); + var now = new DateTime.now_local (); + var timestamp = (uint32)now.to_unix (); + app().window.present_with_time (timestamp); + } + + public void quit() throws DBusError, IOError + { + debug ("DBus Quit() requested"); + } + + public bool can_quit { + get { + return true; + } + } + + public bool can_raise { + get { + return true; + } + } + + public bool has_track_list { + get { + return false; + } + } + + public string desktop_entry { + owned get { + return ((Gtk.Application)GLib.Application.get_default ()).application_id; + } + } + + public string identity { + owned get { + return "Tuner"; + } + } + + public string[] supported_uri_schemes { + owned get { + return {"http", "https"}; + } + } + + public string[] supported_mime_types { + owned get { + return {"audio/mp3","audio/aac","audio/x-vorbis+ogg","audio/x-flac","audio/x-wav","audio/x-m4a","audio/mpeg"}; + } + } + + public bool fullscreen { get; set; default = false; } + public bool can_set_fullscreen { + get { + debug ("CanSetFullscreen() requested"); + return true; + } + } + } // MediaPlayer + + + public class MediaPlayerPlayer : Object, DBus.IMediaPlayer2Player + { + [DBus (visible = false)] + private Model.Station _station; + private string _playback_status = "Stopped"; + private string _current_title = ""; + private string _current_artist = "Tuner"; + private string? _current_art_url = null; + private uint _update_metadata_source = 0; + private uint _send_property_source = 0; + private HashTable _metadata = new HashTable (str_hash, str_equal); + private HashTable _changed_properties = null; + + [DBus (visible = false)] + public unowned DBusConnection conn { get; construct set; } + + private const string INTERFACE_NAME = "org.mpris.MediaPlayer2.Player"; + + public MediaPlayerPlayer (DBusConnection conn) + { + Object (conn: conn); + app().player.state_changed_sig.connect ((station, state) => + { + switch (state) + { + case PlayerController.Is.PLAYING: + case PlayerController.Is.BUFFERING: + playback_status = "Playing"; + break; + case PlayerController.Is.PAUSED: + playback_status = "Paused"; + break; + default: + playback_status = "Stopped"; + break; + } }); - Application.instance.player.station_changed.connect ((station) => { - _current_title = station.title; - _current_artist = station.title; - _current_art_url = station.favicon_url; - trigger_metadata_update (); - }); - - } - - public void next() throws DBusError, IOError { - // debug ("DBus Next() requested"); - } - - public void previous() throws DBusError, IOError { - // debug ("DBus Previous() requested"); - } - - public void pause() throws DBusError, IOError { - // debug ("DBus Pause() requested"); - } - - public void play_pause() throws DBusError, IOError { - // debug ("DBus PlayPause() requested"); - Application.instance.player.play_pause(); - } - - public void stop() throws DBusError, IOError { - // debug ("DBus stop() requested"); - Application.instance.player.player.stop(); - } - - public void play() throws DBusError, IOError { - // debug ("DBus Play() requested"); - Application.instance.player.play_pause (); - } - - public void seek(int64 Offset) throws DBusError, IOError { - // debug ("DBus Seek() requested"); - } - - public void set_position(ObjectPath TrackId, int64 Position) throws DBusError, IOError { - // debug ("DBus SetPosition() requested"); - } - - public void open_uri(string uri) throws DBusError, IOError { - // debug ("DBus OpenUri() requested"); - } - - // Already defined in the interface - // public signal void seeked(int64 Position); - - public string playback_status { - owned get { - // debug ("DBus PlaybackStatus() requested"); - return _playback_status; - } - set { - _playback_status = value; - trigger_metadata_update (); - } - } - - public string loop_status { - owned get { - return "None"; - } - } - - public double rate { get; set; } - public bool shuffle { get; set; } - - public HashTable? metadata { - owned get { - // debug ("DBus metadata requested"); - var table = new HashTable (str_hash, str_equal); - table.insert ("xesam:title", _current_title); - table.insert ("xesam:artist", get_simple_string_array (_current_artist)); - - // this is necessary to remove previous images if the current - // station has none - var art = _current_art_url == null || _current_art_url == "" ? "file:///" : _current_art_url; - - table.insert ("mpris:artUrl", art); - - return table; - } - } - public double volume { owned get; set; } - public int64 position { get; } - public double minimum_rate { get; set; } - public double maximum_rate { get; set; } - - public bool can_go_next { - get { - // debug ("CanGoNext() requested"); - return false; - } - } - - public bool can_go_previous { - get { - // debug ("CanGoPrevious() requested"); - return false; - } - } - - public bool can_play { - get { - // debug ("CanPlay() requested"); - return Application.instance.player.can_play (); - } - } - public bool can_pause { get; } - public bool can_seek { get; } - - public bool can_control { - get { - // debug ("CanControl() requested"); - return true; - } - } - - private void trigger_metadata_update () { - if (update_metadata_source != 0) { - Source.remove (update_metadata_source); - } - - update_metadata_source = Timeout.add (300, () => { - Variant variant = playback_status; - - queue_property_for_notification ("PlaybackStatus", variant); - queue_property_for_notification ("Metadata", metadata); - update_metadata_source = 0; - return false; - }); - } - - private void queue_property_for_notification (string property, Variant val) { - if (changed_properties == null) { - changed_properties = new HashTable (str_hash, str_equal); - } - - changed_properties.insert (property, val); - - if (send_property_source == 0) { - send_property_source = Idle.add (send_property_change); - } - } - - private bool send_property_change () { - if (changed_properties == null) { - return false; - } - - var builder = new VariantBuilder (VariantType.ARRAY); - var invalidated_builder = new VariantBuilder (new VariantType ("as")); - - foreach (string name in changed_properties.get_keys ()) { - Variant variant = changed_properties.lookup (name); - builder.add ("{sv}", name, variant); - } - - changed_properties = null; - - try { - conn.emit_signal (null, - "/org/mpris/MediaPlayer2", - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - new Variant ("(sa{sv}as)", - INTERFACE_NAME, - builder, - invalidated_builder) - ); - } catch (Error e) { - debug (@"Could not send MPRIS property change: $(e.message)"); - } - send_property_source = 0; - return false; - } - - private static string[] get_simple_string_array (string? text) { - if (text == null) { - return new string[0]; - } - string[] array = new string[0]; - array += text; - return array; - } - } + + app().player.metadata_changed_sig.connect (( station, metadata) => + { + _station = station; + _current_title = station.name; + _current_artist = station.name; + _current_art_url = station.favicon; + _current_title = (metadata.title != null && metadata.title != "") ? metadata.title : _station.name; + _current_artist = (metadata.artist != null && metadata.artist != "") ? metadata.artist : _station.name; + update_metadata (); + trigger_metadata_update (); + }); + + app().player.shuffle_mode_sig.connect ((shuffle) => + { + _shuffle = shuffle; + }); + } // MediaPlayerPlayer + + + private void update_metadata () + { + // debug ("DBus metadata requested"); + _metadata.set ("xesam:title", _current_title); + _metadata.set ("xesam:artist", get_simple_string_array (_current_artist)); + + // this is necessary to remove previous images if the current station has none + var art = _current_art_url == null || _current_art_url == "" ? "file:///" : _current_art_url; + _metadata.set ("mpris:artUrl", art); + } // update_metadata + + + public void next() throws DBusError, IOError + { + app().player.shuffle(); + } + + public void previous() throws DBusError, IOError + { + // debug ("DBus Previous() requested"); + } + + public void pause() throws DBusError, IOError + { + // debug ("DBus Pause() requested"); + } + + public void play_pause() throws DBusError, IOError + { + // debug ("DBus PlayPause() requested"); + app().player.play_pause(); + } + + public void stop() throws DBusError, IOError + { + // debug ("DBus stop() requested"); + app().player.stop(); + } + + public void play() throws DBusError, IOError + { + // debug ("DBus Play() requested"); + app().player.play_pause (); + } + + public void seek(int64 Offset) throws DBusError, IOError + { + // debug ("DBus Seek() requested"); + } + + public void set_position(ObjectPath TrackId, int64 Position) throws DBusError, IOError + { + // debug ("DBus SetPosition() requested"); + } + + public void open_uri(string uri) throws DBusError, IOError + { + // debug ("DBus OpenUri() requested"); + } + +// Already defined in the interface +// public signal void seeked(int64 Position); + + public string playback_status { + owned get { + // debug ("DBus PlaybackStatus() requested"); + return _playback_status; + } + set { + _playback_status = value; + trigger_metadata_update (); + } + } + + public string loop_status { + owned get { + return "None"; + } + } + + public double rate { get; set; } + public bool shuffle { get; private set; } + + public HashTable? metadata { + owned get { + return _metadata; + } + } + public double volume { owned get; set; } + public int64 position { get; } + public double minimum_rate { get; set; } + public double maximum_rate { get; set; } + + public bool can_go_next { + get { + // debug ("CanGoNext() requested"); + return false; + } + } + + public bool can_go_previous { + get { + // debug ("CanGoPrevious() requested"); + return false; + } + } + + public bool can_play { + get { + // debug ("CanPlay() requested"); + return app().player.can_play (); + } + } + public bool can_pause { get; } + public bool can_seek { get; } + + public bool can_control { + get { + // debug ("CanControl() requested"); + return true; + } + } + + private void trigger_metadata_update () + { + if (_update_metadata_source != 0) + { + Source.remove (_update_metadata_source); + } + + _update_metadata_source = Timeout.add (300, () => { + Variant variant = playback_status; + + queue_property_for_notification ("PlaybackStatus", variant); + queue_property_for_notification ("Metadata", metadata); + _update_metadata_source = 0; + return false; + }); + } + + private void queue_property_for_notification (string property, Variant val) + { + if (_changed_properties == null) + { + _changed_properties = new HashTable (str_hash, str_equal); + } + + _changed_properties.insert (property, val); + + if (_send_property_source == 0) + { + _send_property_source = Idle.add (send_property_change); + } + } + + private bool send_property_change () + { + if (_changed_properties == null) + { + return false; + } + + var builder = new VariantBuilder (VariantType.ARRAY); + var invalidated_builder = new VariantBuilder (new VariantType ("as")); + + foreach (string name in _changed_properties.get_keys ()) + { + Variant variant = _changed_properties.lookup (name); + builder.add ("{sv}", name, variant); + } + + _changed_properties = null; + + try + { + conn.emit_signal (null, + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + new Variant ("(sa{sv}as)", + INTERFACE_NAME, + builder, + invalidated_builder) + ); + } catch (Error e) + { + debug (@"Could not send MPRIS property change: $(e.message)"); + } + _send_property_source = 0; + return false; + } + + private static string[] get_simple_string_array (string? text) + { + if (text == null) + { + return new string[0]; + } + string[] array = new string[0]; + array += text; + return array; + } + } // MediaPlayerPlayer } diff --git a/src/Services/DataProvider.vala b/src/Services/DataProvider.vala new file mode 100644 index 0000000..7448a6e --- /dev/null +++ b/src/Services/DataProvider.vala @@ -0,0 +1,285 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file DataProvider.vala + * + * @author technosf + * @date 2024-12-01 + * @since 2.0.0 + * @brief Interface to radio-browser.info API and servers + * + */ + +using Gee; + +/** + * @namespace Tuner.DataProvider + * + * @brief API for radio station information provider inplementations + * + */ +namespace Tuner.DataProvider { + + /** + * @struct SearchParams + * @brief API search parameters + * + * Defines the search criteria used when querying the radio-browser.info API + * for stations. + */ + public struct SearchParams { + /** @brief Search text to match against station names */ + string text; + + /** @brief List of tags to filter stations by */ + Set tags; + + /** @brief List of specific station UUIDs to retrieve */ + Set uuids; + + /** @brief ISO country code to filter stations by */ + string countrycode; + + /** @brief Sorting criteria for the results */ + SortOrder order; + + /** @brief Whether to reverse the sort order */ + bool reverse; + } + + + /** + * @brief Error domain for DataProvider-related errors + * + */ + public errordomain DataError { + /** @brief Error parsing API response data */ + PARSE_DATA, + + /** @brief Unable to establish connection to API servers */ + NO_CONNECTION + } // DataError + + + /** + * @enum SortOrder + * @brief Enumeration of sorting options for station search results + * + */ + public enum SortOrder { + NAME, + URL, + HOMEPAGE, + FAVICON, + TAGS, + COUNTRY, + STATE, + LANGUAGE, + VOTES, + CODEC, + BITRATE, + LASTCHECKOK, + LASTCHECKTIME, + CLICKTIMESTAMP, + CLICKCOUNT, + CLICKTREND, + RANDOM; + + /** + * @brief Convert SortOrder enum to string representation + * + * @return String representation of the SortOrder + */ + public string to_string() { + switch (this) { + case NAME: + return "name"; + case URL: + return "url"; + case HOMEPAGE: + return "homepage"; + case FAVICON: + return "favicon"; + case TAGS: + return "tags"; + case COUNTRY: + return "country"; + case STATE: + return "state"; + case LANGUAGE: + return "language"; + case VOTES: + return "votes"; + case CODEC: + return "codec"; + case BITRATE: + return "bitrate"; + case LASTCHECKOK: + return "lastcheckok"; + case LASTCHECKTIME: + return "lastchecktime"; + case CLICKTIMESTAMP: + return "clicktimestamp"; + case CLICKCOUNT: + return "clickcount"; + case CLICKTREND: + return "clicktrend"; + case RANDOM: + return "random"; + default: + assert_not_reached(); + } + } // to_string + } // SortOrder + + + /** + * @class Tag + * + * @brief Represents a radio station tag with usage statistics + * + * Encapsulates metadata about a tag used to categorize radio stations, + * including its name and the number of stations using it. + */ + public class Tag : Object { + /** @brief The tag name */ + public string name { get; set; } + + /** @brief Number of stations using this tag */ + public uint stationcount { get; set; } + } // Tag + + + /** + * @class API + * + * @brief Main DataProvider API + * + * Defines methods to interact with the DataProvider API, including: + * - Station search and retrieval + * - User interaction tracking (votes, listens) + * - Tag management + * - Server discovery and connection handling + * + */ + public interface API : Object + { + /** + * @brief DataProvider status + * + */ + public enum Status + { + OK, + NO_SERVER_LIST, + NO_SERVERS_PRESENTED, + NOT_AVAILABLE, + UNKNOW_ERROR + } // Status + + + /** + * @brief Clears the last data error + * + */ + public virtual void clear_last_error() { last_data_error = null; } + + + /** + * @brief The last DataError from the DataProvider + * + */ + public abstract DataError? last_data_error { get; protected set; } + + + /** + * @brief DataProvider status property + * + */ + public abstract Status status { get; protected set; } + + + /** + * @brief DataProvider name property + * + */ + public abstract string name(); + + + /** + * @brief Number of tags available + * + * @return the number of available tags + */ + public abstract int available_tags(); + + + /** + * @brief Initialize the DataProvider implementation + * + * @return true if initialization successful + */ + public abstract bool initialize(); + + + /** + * @brief Register a station listen event + * + * @param stationuuid UUID of the station being listened to + */ + public abstract void click(string stationuuid); + + + /** + * @brief Vote for a station + * + * @param stationuuid UUID of the station being voted for + */ + public abstract void vote(string stationuuid) ; + + + /** + * @brief Get all available tags + * + * @return ArrayList of Tag objects + * @throw DataError if unable to retrieve or parse tag data + */ + public abstract Set get_tags(int offset = 0, int limit = 0) throws DataError; + + + /** + * @brief Get a station or stations by UUID/S + * + * @param uuids comma seperated lists of the stations to retrieve + * @return Station object if found, null otherwise + * @throw DataError if unable to retrieve or parse station data + */ + public abstract Set by_uuid(string uuids) throws DataError; + public abstract Set by_uuids(Collection uuids) throws DataError; + + /** + * @brief Get a station or stations by Streaming URL + * + * @param url streaming url the stations to retrieve + * @return Station object if found, null otherwise + * @throw DataError if unable to retrieve or parse station data + */ + //public abstract Set by_url(string url) throws DataError; // TODO Not working at Provider + +/** + * @brief Search for stations based on given parameters + * + * @param params Search parameters + * @param rowcount Maximum number of results to return + * @param offset Offset for pagination + * @return ArrayList of Station objects matching the search criteria + * @throw DataError if unable to retrieve or parse station data + */ + public abstract Set search(SearchParams params, uint rowcount, uint offset = 0) throws DataError; + public async abstract Set search_async(SearchParams params, uint rowcount, uint offset = 0) throws DataError; + + } // API +} // DataProvider \ No newline at end of file diff --git a/src/Services/Favicon.vala b/src/Services/Favicon.vala deleted file mode 100644 index 16f1015..0000000 --- a/src/Services/Favicon.vala +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -/** - * @file Favicon.vala - * @author technosf - * @date 2024-10-01 - * @brief Get, cache and serve favicons - * @version 1.5.4 - * - * This file contains the Tuner.Favicon class, which handles the retrieval, - * caching, and serving of favicons for radio stations. - */ - -/** - * @brief Get, cache and serve favicons - * - * This class handles the retrieval, caching, and serving of favicons for radio stations. - * It provides methods to load favicons from cache or fetch them from the internet. - * - * @class Tuner.Favicon - * @extends Object - */ -public class Tuner.Favicon : GLib.Object { - - /** - * @brief Asynchronously load the favicon for a given station - * - * This method attempts to load the favicon from the cache first. If not found in the cache - * or if forceReload is true, it will fetch the favicon from the internet asynchronously - * scale it to 48x48 pixels, and save it to a cache file. - * - * @param station The station for which to load the favicon - * @param forceReload If true, bypass the cache and fetch the favicon from the internet - * @return The loaded favicon as a Gdk.Pixbuf, or null if loading fails - */ - public static async Gdk.Pixbuf? load_async(Model.Station station, bool forceReload = false) - { - if ( station.favicon_url == null || station.favicon_url == "" || station.favicon_load_error ) return null; // Favicon is erring out, so bypss loading it this session - - var favicon_cache_file = Path.build_filename(Application.instance.cache_dir, station.id); - - // Check if not forcing reload and then if favicon is cached - if ( !forceReload - && FileUtils.test(favicon_cache_file, FileTest.EXISTS)) - { - try { - return new Gdk.Pixbuf.from_file_at_scale(favicon_cache_file, 48, 48, true); - } catch (Error e) { - warning(@"Failed to load cached favicon: $(e.message)"); - } - } - - // If not in cache or force reload, fetch from internet - uint status_code; - - InputStream? stream = yield HttpClient.GETasync(station.favicon_url, out status_code); - - if ( stream != null && status_code == 200) { - try { - var pixbuf = yield new Gdk.Pixbuf.from_stream_async(stream, null); - var scaled_pixbuf = pixbuf.scale_simple(48, 48, Gdk.InterpType.BILINEAR); - - // Save to cache - scaled_pixbuf.save(favicon_cache_file, "png"); - - return scaled_pixbuf; - } catch (Error e) { - warning(@"Failed to process favicon $(station.favicon_url) - $(e.message)"); - } - } - - // Could not load the favicon, so flag not to try again in this session - station.favicon_load_error = true; - return null; - } - } \ No newline at end of file diff --git a/src/Services/HttpClient.vala b/src/Services/HttpClient.vala index 8881b09..b466fcc 100644 --- a/src/Services/HttpClient.vala +++ b/src/Services/HttpClient.vala @@ -1,9 +1,9 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - /** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * * @file HttpClient.vala * @author technosf * @date 2024-10-01 @@ -20,7 +20,8 @@ using Gee; * This class provides static methods for making HTTP requests using the Soup library. * It includes a singleton Soup.Session instance for efficient request handling. */ -public class Tuner.HttpClient : Object { +public class Tuner.HttpClient : Object +{ /** * @brief Singleton instance of Soup.Session @@ -31,6 +32,7 @@ public class Tuner.HttpClient : Object { */ private static Soup.Session _session; + /** * @brief Get the singleton Soup.Session instance * @@ -55,9 +57,44 @@ public class Tuner.HttpClient : Object { } debug(@"Conns Max: $(_session.get_max_conns()), Conns PH: $(_session.get_max_conns_per_host())"); return _session; - } + } // getSession + /** + * @brief Perform a HEAD request to the specified URL + * + * Does not sanity check the URL + * + * @param url_string The URL to send the GET request to + * @return status_code the HTTP status code of the response + * @throws Error if there's an error sending the request or receiving the response + */ + public static uint HEAD(Uri uri) + { + if ( app().is_offline) return 0; + + var msg = new Soup.Message.from_uri("HEAD", uri); + /* + Ignore all TLS certificate errors + */ + msg.accept_certificate.connect ((msg, cert, errors) => { + return true; + }); + + try { + getSession().send(msg); + return msg.status_code; + } catch (Error e) { + warning("HEAD - Error accessing URL: %s (%s)", + uri.to_string(), + e.message); + } + + return 0; + } // HEAD + + + /** * @brief Perform a GET request to the specified URL * * @param url_string The URL to send the GET request to @@ -65,19 +102,15 @@ public class Tuner.HttpClient : Object { * @return InputStream containing the response body, or null if the request failed * @throws Error if there's an error sending the request or receiving the response */ - public static InputStream? GET(string url_string, out uint status_code) + public static InputStream? GET(Uri uri, out uint status_code) { - status_code = 0; - - if (url_string == null || url_string.length < 4) // domains are at least 4 chars - { - warning("GET - Invalid URL: %s", url_string ?? "null"); - return null; - } + debug(@"Get: $(uri.to_string()) "); + status_code = 0; - string sanitized_url = ensure_https_prefix(url_string); + if (app().is_offline) + return null; - var msg = new Soup.Message("GET", sanitized_url); + var msg = new Soup.Message.from_uri("GET", uri); /* Ignore all TLS certificate errors @@ -87,23 +120,16 @@ public class Tuner.HttpClient : Object { }); try { - - if (Uri.is_valid(sanitized_url, UriFlags.NONE)) - { - var inputStream = getSession().send(msg); - status_code = msg.status_code; - return inputStream; - } else { - debug("GET - Invalid URL format: %s", sanitized_url); - } + var inputStream = getSession().send(msg); + status_code = msg.status_code; + return inputStream; } catch (Error e) { - warning("GET - Error accessing URL: %s (%s)", - sanitized_url, - e.message); + warning(@"GET - Error accessing URL: $(uri.to_string()) ($(e.message))"); } return null; - } + } // GET + /** * @brief Perform an asynchronous GET request to the specified URL @@ -112,29 +138,14 @@ public class Tuner.HttpClient : Object { * @param status_code Output parameter for the HTTP status code of the response * @return InputStream containing the response body, or null if the request failed */ - public static async InputStream? GETasync(string url_string, out uint status_code) + public static async InputStream? GETasync(Uri uri, int priority, out uint status_code) { status_code = 0; - if (url_string == null || url_string.length < 4 || url_string == "null") // domains are at least 4 chars - { - debug("GETasync - Invalid URL: %s", url_string ?? "null"); - return null; - } - - string sanitized_url = ensure_https_prefix(url_string); + if (app().is_offline) + return null; - try { - if (!Uri.is_valid(sanitized_url, UriFlags.NONE)) { - debug("GETasync - Invalid URL format: %s", sanitized_url); - return null; - } - } catch (GLib.UriError e) { - debug("GETasync - URI error: %s", e.message); - return null; - } - - var msg = new Soup.Message("GET", sanitized_url); + var msg = new Soup.Message.from_uri("GET", uri); /* Ignore all TLS certificate errors @@ -143,38 +154,26 @@ public class Tuner.HttpClient : Object { return true; }); - try { - - var inputStream = yield getSession().send_async(msg, Priority.DEFAULT, null); - status_code = msg.status_code; - return inputStream; - - } catch (Error e) { - warning("GETasync - Couldn't fetch resource: %s (%s)", - sanitized_url, - e.message); - } - + uint loop = 1; + do + /* + Try three times + */ + { + try + { + var inputStream = yield getSession().send_async(msg, priority, app().offline_cancel); + status_code = msg.status_code; + if (status_code >= 200 && status_code < 300) + return inputStream; + } catch (Error e) + { + info(@"GETasync - Try $(loop) failed to fetch: $(uri.to_string()) $(e.message)"); + } + yield nap(200 * loop); + } while (loop++ < 3); + + info(@"GETasync - GETasync failed for: $(uri.to_string())"); return null; - } - - /** - * @brief Ensures that the given URL has an HTTPS prefix - * - * This method checks if the provided URL starts with either "http://" or "https://". - * If it doesn't have either prefix, it adds "https://" to the beginning of the URL. - * - * @param url The input URL string to be checked and potentially modified - * @return A string representing the URL with an HTTPS prefix - * - * @note This method does not validate the URL structure beyond checking for the protocol prefix - */ - private static string ensure_https_prefix(string url) { - // Check if the string starts with "http://" or "https://" - if (!url.has_prefix("http://") && !url.has_prefix("https://")) { - // If it doesn't, prefix the string with "https://" - return "https://" + url; - } - return url; - } -} \ No newline at end of file + } // GETasync +} diff --git a/src/Services/RadioBrowser.vala b/src/Services/RadioBrowser.vala deleted file mode 100644 index 4ae9fd6..0000000 --- a/src/Services/RadioBrowser.vala +++ /dev/null @@ -1,473 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - - -using Gee; - -/** - * @namespace Tuner.RadioBrowser - * @brief Interface to radio-browser.info API and servers - * - * This namespace provides functionality to interact with the radio-browser.info API, - * including searching for stations, retrieving station information, and managing user actions - * such as voting and tracking listens. - */ -namespace Tuner.RadioBrowser { - - private const string SRV_SERVICE = "api"; - private const string SRV_PROTOCOL = "tcp"; - private const string SRV_DOMAIN = "radio-browser.info"; - private const string ALL_API = "https://all.api.radio-browser.info"; - - /** - * @class Station - * @brief Station data subset returned from radio-browser API - * - * This class represents a radio station with its properties as returned by the radio-browser API. - */ - public class Station : Object { - /** @brief Unique identifier for the station */ - public string stationuuid { get; set; } - /** @brief Name of the station */ - public string name { get; set; } - /** @brief Resolved URL of the station's stream */ - public string url_resolved { get; set; } - /** @brief Country where the station is located */ - public string country { get; set; } - /** @brief Country code of the station's location */ - public string countrycode { get; set; } - /** @brief URL of the station's favicon */ - public string favicon { get; set; } - /** @brief Number of clicks/listens for the station */ - public uint clickcount { get; set; } - /** @brief URL of the station's homepage */ - public string homepage { get; set; } - /** @brief Audio codec used by the station */ - public string codec { get; set; } - /** @brief Bitrate of the station's stream */ - public int bitrate { get; set; } - } - - /** - * @struct SearchParams - * @brief Parameters for searching radio stations - * - * This struct defines the parameters used for searching radio stations. - */ - public struct SearchParams { - /** @brief Text to search for in station names */ - string text; - /** @brief List of tags to filter stations */ - ArrayList tags; - /** @brief List of station UUIDs to retrieve */ - ArrayList uuids; - /** @brief Country code to filter stations */ - string countrycode; - /** @brief Sort order for the search results */ - SortOrder order; - /** @brief Whether to reverse the sort order */ - bool reverse; - } - - /** - * @brief Error domain for data-related errors - */ - public errordomain DataError { - PARSE_DATA, - NO_CONNECTION - } - - /** - * @enum SortOrder - * @brief Enumeration of sorting options for station search results - */ - public enum SortOrder { - NAME, - URL, - HOMEPAGE, - FAVICON, - TAGS, - COUNTRY, - STATE, - LANGUAGE, - VOTES, - CODEC, - BITRATE, - LASTCHECKOK, - LASTCHECKTIME, - CLICKTIMESTAMP, - CLICKCOUNT, - CLICKTREND, - RANDOM; - - /** - * @brief Convert SortOrder enum to string representation - * @return String representation of the SortOrder - */ - public string to_string () { - switch (this) { - case NAME: - return "name"; - case URL: - return "url"; - case HOMEPAGE: - return "homepage"; - case FAVICON: - return "favicon"; - case TAGS: - return "tags"; - case COUNTRY: - return "country"; - case STATE: - return "state"; - case LANGUAGE: - return "language"; - case VOTES: - return "votes"; - case CODEC: - return "codec"; - case BITRATE: - return "bitrate"; - case LASTCHECKOK: - return "lastcheckok"; - case LASTCHECKTIME: - return "lastchecktime"; - case CLICKTIMESTAMP: - return "clicktimestamp"; - case CLICKCOUNT: - return "clickcount"; - case CLICKTREND: - return "clicktrend"; - case RANDOM: - return "random"; - default: - assert_not_reached (); - } - } - } - - /** - * @class Tag - * @brief Represents a tag associated with radio stations - */ - public class Tag : Object { - /** @brief Name of the tag */ - public string name { get; set; } - /** @brief Number of stations associated with this tag */ - public uint stationcount { get; set; } - } - - /** - * @brief Compare two strings for equality - * @param a First string to compare - * @param b Second string to compare - * @return True if strings are equal, false otherwise - */ - public bool EqualCompareString (string a, string b) { - return a == b; - } - - /** - * @class Client - * @brief RadioBrowser API Client - * - * This class provides methods to interact with the RadioBrowser API, including - * searching for stations, retrieving station information, and managing user actions. - */ - public class Client : Object { - private string current_server; - - /** - * @brief Constructor for RadioBrowser Client - * @throw DataError if unable to initialize the client - */ - public Client() throws DataError { - Object(); - - ArrayList servers; - string _servers = GLib.Environment.get_variable ("TUNER_API"); - if ( _servers != null ){ - servers = new Gee.ArrayList.wrap(_servers.split(":")); - } else { - //servers = DEFAULT_STATION_SERVERS; - servers = get_api_servers(); - } - - if ( servers.size == 0 ) { - throw new DataError.NO_CONNECTION ("Unable to resolve API servers for radio-browser.info"); - } - - var chosen_server = Random.int_range(0, servers.size); - - current_server = @"https://$(servers[chosen_server])"; - debug (@"RadioBrowser Client - Chosen radio-browser.info server: $current_server"); - } - - /** - * @brief Track a station listen event - * @param stationuuid UUID of the station being listened to - */ - public void track (string stationuuid) { - debug (@"sending listening event for station $stationuuid"); - uint status_code; - HttpClient.GET (@"$current_server/json/url/$stationuuid", out status_code); - debug (@"response: $(status_code)"); - } - - /** - * @brief Vote for a station - * @param stationuuid UUID of the station being voted for - */ - public void vote (string stationuuid) { - debug (@"sending vote event for station $stationuuid"); - uint status_code; - HttpClient.GET(@"$current_server/json/vote/$stationuuid", out status_code); - debug (@"response: $(status_code)"); - } - - /** - * @brief Get stations from a specific API resource - * @param resource API resource path - * @return ArrayList of Station objects - * @throw DataError if unable to retrieve or parse station data - */ - public ArrayList get_stations (string resource) throws DataError { - debug (@"RB $resource"); - - Json.Node rootnode; - - try { - uint status_code; - - debug (@"Requesting from 'radio-browser.info'"); - var response = HttpClient.GET(@"$current_server/$resource", out status_code); - debug (@"Response from 'radio-browser.info': $(status_code)"); - - try { - var parser = new Json.Parser(); - parser.load_from_stream (response, null); - rootnode = parser.get_root(); - } catch (Error e) { - throw new DataError.PARSE_DATA (@"Unable to parse JSON response: $(e.message)"); - } - var rootarray = rootnode.get_array (); - - var stations = jarray_to_stations (rootarray); - return stations; - } catch (GLib.Error e) { - warning (@"Error retrieving stations: $(e.message)"); - } - - return new ArrayList(); - } - - /** - * @brief Search for stations based on given parameters - * @param params Search parameters - * @param rowcount Maximum number of results to return - * @param offset Offset for pagination - * @return ArrayList of Station objects matching the search criteria - * @throw DataError if unable to retrieve or parse station data - */ - public ArrayList search (SearchParams params, - uint rowcount, - uint offset = 0) throws DataError { - // by uuids - if (params.uuids != null) { - var stations = new ArrayList (); - foreach (var uuid in params.uuids) { - var station = this.by_uuid(uuid); - if (station != null) { - stations.add (station); - } - } - return stations; - } - - // by text or tags - var resource = @"json/stations/search?limit=$rowcount&order=$(params.order)&offset=$offset"; - - debug (@"Search: $(resource)"); - if ( params.text != "") { - resource += @"&name=$(params.text)"; - } - - if (params.tags.size > 0 ) { - string tag_list = params.tags[0]; - if (params.tags.size > 1) { - tag_list = string.joinv (",", params.tags.to_array()); - } - resource += @"&tagList=$tag_list&tagExact=true"; - } - if (params.countrycode.length > 0) { - resource += @"&countrycode=$(params.countrycode)"; - } - if (params.order != SortOrder.RANDOM) { - // random and reverse doesn't make sense - resource += @"&reverse=$(params.reverse)"; - } - return get_stations (resource); - } - - /** - * @brief Get a station by its UUID - * @param uuid UUID of the station to retrieve - * @return Station object if found, null otherwise - * @throw DataError if unable to retrieve or parse station data - */ - public Station? by_uuid (string uuid) throws DataError { - var resource = @"json/stations/byuuid/$uuid"; - var result = get_stations (resource); - if (result.size == 0) { - return null; - } - return result[0]; - } - - /** - * @brief Get all available tags - * @return ArrayList of Tag objects - * @throw DataError if unable to retrieve or parse tag data - */ - public ArrayList get_tags () throws DataError { - - Json.Node rootnode; - - try { - uint status_code; - var stream = HttpClient.GET(@"$current_server/json/tags", out status_code); - - debug (@"response from radio-browser.info: $(status_code)"); - - try { - var parser = new Json.Parser(); - parser.load_from_stream (stream); - rootnode = parser.get_root (); - } catch (Error e) { - throw new DataError.PARSE_DATA (@"unable to parse JSON response: $(e.message)"); - } - var rootarray = rootnode.get_array (); - - var tags = jarray_to_tags (rootarray); - return tags; - } catch(GLib.Error e) { - debug("cannot get_tags()"); - } - - return new ArrayList(); - } - - /** - */ - private Station jnode_to_station (Json.Node node) { - return Json.gobject_deserialize (typeof (Station), node) as Station; - } - - /** - */ - private ArrayList jarray_to_stations (Json.Array data) { - var stations = new ArrayList (); - - data.foreach_element ((array, index, element) => { - Station s = jnode_to_station (element); - stations.add (s); - }); - - return stations; - } - - /** - */ - private Tag jnode_to_tag (Json.Node node) { - return Json.gobject_deserialize (typeof (Tag), node) as Tag; - } - - /** - */ - private ArrayList jarray_to_tags (Json.Array data) { - var tags = new ArrayList (); - - data.foreach_element ((array, index, element) => { - Tag s = jnode_to_tag (element); - tags.add (s); - }); - - return tags; - } - - /** - * @brief Get all radio-browser.info API servers - * - * Gets server list from - * - * @since 1.5.4 - * @return ArrayList of strings containing the resolved hostnames - * @throw DataError if unable to resolve DNS records - */ - private ArrayList get_api_servers() throws DataError { - - var results = new ArrayList(); - - try - /* - DNS SRV record lookup - */ - { - var srv_targets = GLib.Resolver.get_default(). - lookup_service( SRV_SERVICE, SRV_PROTOCOL, SRV_DOMAIN, null ); - foreach (var target in srv_targets) { - results.add(target.get_hostname()); - } - } catch (GLib.Error e) { - @warning(@"Unable to resolve Radio-Browser SRV records: $(e.message)"); - } - - if (results.is_empty) - /* - JSON API server lookup as SRV record lookup failed - */ - { - - try { - uint status_code; - var stream = HttpClient.GET(@"$ALL_API/json/servers", out status_code); - - debug (@"response from $(ALL_API)/json/servers: $(status_code)"); - - if (status_code == 200) { - - Json.Node root_node; - - try { - var parser = new Json.Parser(); - parser.load_from_stream (stream); - root_node = parser.get_root (); - } catch (Error e) { - throw new DataError.PARSE_DATA (@"unable to parse JSON response: $(e.message)"); - } - - if (root_node != null && root_node.get_node_type() == Json.NodeType.ARRAY) { - - root_node.get_array().foreach_element((array, index_, element_node) => { - var object = element_node.get_object(); - if (object != null) { - var name = object.get_string_member("name"); - if (name != null && !results.contains (name)) { - results.add(name); - } - } - }); - - } - } - } catch (Error e) { - warning("Failed to parse APIs JSON response: $(e.message)"); - } - } - - return results; - } - } -} \ No newline at end of file diff --git a/src/Services/StarStore.vala b/src/Services/StarStore.vala new file mode 100644 index 0000000..bf59313 --- /dev/null +++ b/src/Services/StarStore.vala @@ -0,0 +1,439 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file StarStore.vala + * + * @brief Store and retrieve a collection of starred stations. + * + * Manages a collection of stations stored in a JSON file. + * Provides methods to add, remove, and persist stations. + * The JSON file store a subset of station data - the minimum + * to be able to play a station without retrieving its information] + * from radio-browser + */ + +using Gee; +using GLib; +using Tuner.Model; + + +/** + * @class StarStore + * + * @brief Manages a collection of starred stations and saved searches + * + * This class allows for storing, retrieving, and persisting a + * list of favorite stations in a JSON file. It uses libgee for data structures. + * + * DirectoryController uses StationStore to load the starred stations from RadioBrowser + */ +public class Tuner.StarStore : Object +{ + + public signal void starred_stations_changed_sig ( Model.Station station ); ///< Emitted when the starred stations change. + + private const string FAVORITES_PROPERTY_APP = "app"; + private const string FAVORITES_PROPERTY_FILE = "file"; + private const string FAVORITES_PROPERTY_SCHEMA = "schema"; + private const string FAVORITES_PROPERTY_STATIONS = "stations"; + private const string FAVORITES_PROPERTY_SEARCHES = "searches"; + + private const string FAVORITES_SCHEMA_VERSION = "2.0"; + + private const string M3U8 = "#EXTM3U\n#EXTENC:UTF-8\n#PLAYLIST:Tuner\n"; + private const string M3U8_UUID = "STATIONUUID"; + private const string UUID_REGEX = "([a-fA-Z0-9]{8}-[a-fA-Z0-9]{4}-[a-fA-Z0-9]{4}-[a-fA-Z0-9]{4}-[a-fA-Z0-9]{12})"; + private Regex uuid_regex; + + construct + { + try { + uuid_regex = new Regex (UUID_REGEX, 0, 0); + } catch ( RegexError e ) + { + critical(@"Could not compile regex: $(e.message)"); + } + } + + private File _starred_file; ///< File to persist favorite stations. + + + private Map _starred_station_map = new HashMap (); ///< Collection of starred station UUIDs. + private Gee.Set _saved_searches = new HashSet (); ///< Collection of saved searchess. + private bool _loaded = false; + + + // ---------------------------------------------------------- + // Publics + // ---------------------------------------------------------- + + /** + * @brief Constructor for StationStore. + * @param favorites_path The path to the JSON file where favorites are stored. + */ + public StarStore (File starred_file) + { + Object (); + _starred_file = starred_file; + + } // StarredStationController + + + /** + * @brief Adds a station to the favorites and persists the change. + * @param station The station to be added. + */ + public void add_station (Station station) { + if (_starred_station_map.has_key (station.stationuuid)) return; + _starred_station_map.set (station.stationuuid, station); + persist (); + starred_stations_changed_sig ( station ); + } // add_station + + + /** + * @brief Removes a station from the favorites and persists the change. + * + * @param station The station to be removed. + */ + public void remove_station (Station station) { + _starred_station_map.unset (station.stationuuid); + persist (); + starred_stations_changed_sig ( station ); + } // remove_station + + + /** + * @brief Add or removes a station from the favorites and persists the change. + * + * @param station The station. + */ + public void update_from_station (Station station) + { + if ( station.starred ) + { + add_station (station); + } + else + { + remove_station (station); + } + } // remove_station + + + /** + * @brief Adds a saved search + * + * @param search_text The search term to save + */ + public void add_saved_search(string search_text) + { + _saved_searches.add (search_text); + persist(); + } // add_saved_search + + + /** + * @brief Removes a saved search + * + * @param search_text The search term to remove + */ + public void remove_saved_search(string search_text) + { + _saved_searches.remove (search_text); + persist(); + } // remove_saved_search + + + /** + * @brief Serializes the current favorites to a JSON string. + * + * @return A JSON string representation of the favorites. + */ + public string serialize () + { + Json.Builder builder = new Json.Builder (); + builder.begin_object (); + builder.set_member_name (FAVORITES_PROPERTY_APP); + builder.add_string_value (Application.APP_ID); + builder.set_member_name (FAVORITES_PROPERTY_FILE); + builder.add_string_value (Application.STARRED); + builder.set_member_name (FAVORITES_PROPERTY_SCHEMA); + builder.add_string_value (FAVORITES_SCHEMA_VERSION); + + // Starred Stations + builder.set_member_name (FAVORITES_PROPERTY_STATIONS); + builder.begin_array (); + foreach (var starred in _starred_station_map.values) { + var node = Json.gobject_serialize (starred); + builder.add_value (node); + } + builder.end_array (); + + // Saved Searches + builder.set_member_name (FAVORITES_PROPERTY_SEARCHES); + builder.begin_array (); + foreach (var searched in _saved_searches) { + builder.add_string_value (searched); + } + builder.end_array (); + builder.end_object (); + + Json.Generator generator = new Json.Generator (); + generator.set_pretty (true); + generator.set_root (builder.get_root ()); + string data = generator.to_data (null); + return data; + } // serialize + + + /** + * @brief Retrieves all favorite stations. + * + * @return An ArrayList of favorite stations. + */ + public Collection get_all_stations () { + return _starred_station_map.values; + } // get_all_stations + + + /** + * @brief Retrieves all favorite stations. + * + * @return An ArrayList of favorite stations. + */ + public Gee.Set get_all_searches () { + return _saved_searches; + } // get_all_searches + + + /** + * @brief Checks if a station is in the favorites. + * + * @param station The station to check. + * @return True if the station is in favorites, false otherwise. + */ + public bool contains (Station station) { + return _starred_station_map.has_key (station.stationuuid); + } // contains + + + /** + * @brief Updates a station in the favorites. + * + * @param station The station to update. + */ + public void update(Station station) + { + if ( _starred_station_map.has_key (station.stationuuid) ) + { + _starred_station_map.set (station.stationuuid, station.updated ()); + persist (); + } + } // update + + + /** + * @brief Loads the favorites from the JSON file. + * + * Starred staions are defined by the file and do not load from the DataProvider + * and so can deviate. If the station disapears from the DataProvider, a copy + * of its info remain in the starred file. + * Load needs to happen after Application creation. + * + * This method reads the JSON file and populates the _store with + * the favorite stations. + */ + public async void load () { + + if ( _loaded) return; + _loaded = true; + debug ("store file initial creation"); + try + // Create file if it does not already exist + { + _starred_file.create (FileCreateFlags.PRIVATE).close (); + //df.close (); + debug (@"store created"); + } catch (Error e) { + // File already existes + } + + debug ("loading store"); + Json.Parser parser = new Json.Parser.immutable_new (); + + try { + var stream = _starred_file.read (); + parser.load_from_stream (stream); + stream.close (); + } catch (Error e) { + warning (@"Load failed with error: $(e.message)"); + } + + Json.Node? root = parser.get_root (); + + if ( root == null ) return; // No favorites stored + + Json.Array jstarred; + Json.Array jsearches; + + // Check for schema versions + if ( get_member( root, FAVORITES_PROPERTY_SCHEMA) == null ) + /* + v1 Schema + */ + { + jstarred = root.get_array (); + jsearches = null; + } + else + /* + v2+ Schema + */ + { + var stations = get_member( root, FAVORITES_PROPERTY_STATIONS); + jstarred = stations.get_array (); + var searches = get_member( root, FAVORITES_PROPERTY_SEARCHES); + jsearches = searches.get_array (); + } + + /* + Read in each starred item + */ + jstarred.foreach_element ((a, i, elem) => + { + var station = new Station.basic(elem); // Creates a basic station without adding it to the main station directory + _starred_station_map.set (station.stationuuid, station); + + station.starred = true; + + // Connect the star button + station.station_star_changed_sig.connect ( (starred) => + { + update_from_station ( station ); + }); + }); + + // Check starred station currency - doing this way means once call to DataProvider for all the stations + try { + var provider_station = app().provider.by_uuids( _starred_station_map.keys); + foreach ( var ps in provider_station) + { + var ss = _starred_station_map.get (ps.stationuuid); + if ( ss != null) ss.is_in_index = ss.set_up_to_date_with (ps); + } + } + catch (DataProvider.DataError e) { + warning(@"Comparing with DataProvider copy of station: $(e.message)"); + } + + // Read each stored search + jsearches.foreach_element ((a, i, elem) => { + _saved_searches.add (elem.get_string ()); + debug(@"Search:$(elem.get_string ())"); + }); // jsearches.foreach_element + + } // load + + + /** + * @brief Creates a string of Starred stations in m3u format + * @return A string representation of the favorites formated as M3U + */ + public string export_m3u8() + { + StringBuilder playlist = new StringBuilder(M3U8); + foreach ( var station in _starred_station_map.values) + { + var url = ( station.urlResolved == null || station.urlResolved == "") ? station.url : station.urlResolved; + playlist.append (@"#EXTINF:-1,$(station.name) - logo=\"$(station.favicon)\",$M3U8_UUID=\"$(station.stationuuid)\"\n$(url)\n#EXTIMG:$(station.favicon)\n"); + } + + return playlist.str; + + } // export_m3u8 + + + /** + * @brief Scans data for Station UUIDs which are checked and added as Starred. + * @param data_stream The data to be scanned + */ + public void import_stationuuids( DataInputStream data_stream ) throws GLib.IOError + { + Gee.List stationuuids = new ArrayList(); + string content; + MatchInfo match_info; + while ( (content = data_stream.read_line(null)) != null) + { + uuid_regex.match (content, 0, out match_info); + while (match_info.matches ()) + { + stationuuids.add (match_info.fetch_all()[0]); + debug (@"UUID: $(match_info.fetch_all()[0])"); + + try { + match_info.next (); + } catch ( RegexError e) + { + warning (@"Regex error processing line: $content\n Error: $(e.message)"); + } + } // while + } // while + + foreach ( var station in app().directory.get_stations_by_uuid(stationuuids)) + { + add_station(station); + } + } // import_stationuuids + + + + // ---------------------------------------------------------- + // Privates + // ---------------------------------------------------------- + + + + /** + * @brief Persists the current state of favorites to the JSON file. + * + * This method serializes the _store and writes it to the favorites file. + */ + private void persist () + { + debug ("persisting store"); + + try { + _starred_file.delete (); + var stream = _starred_file.create ( + FileCreateFlags.REPLACE_DESTINATION | FileCreateFlags.PRIVATE + ); + var s = new DataOutputStream (stream); + s.put_string ( serialize () ); + s.flush (); + s.close (); // closes base stream also + } catch (Error e) { + warning (@"Persist failed with error: $(e.message)"); + } + } // persist + + + /** + * @brief Returns the given property from the JSON node. + * + */ + private static Json.Node? get_member(Json.Node node, string property_name) { + // Check if the node is of type OBJECT + if (node.get_node_type() == Json.NodeType.OBJECT) { + Json.Object json_object = node.get_object(); + + // Check if the JSON object has the specified property + if ( json_object.has_member(property_name) ) + return json_object.get_member (property_name); + } + return null; // Not an object, so no properties exist + } // get_member +} // StarStore diff --git a/src/Settings.vala b/src/Settings.vala new file mode 100644 index 0000000..57dd62c --- /dev/null +++ b/src/Settings.vala @@ -0,0 +1,91 @@ +/** + * @file Application.vala + * @brief Contains the main Application class for the Tuner application + * + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + */ + + +public class Tuner.Settings : GLib.Settings +{ + private const string SETTINGS_AUTO_PLAY = "auto-play"; + private const string SETTINGS_DO_NOT_VOTE = "do-not-vote"; + private const string SETTINGS_LAST_PLAYED_STATION = "last-played-station"; + private const string SETTINGS_POS_X = "pos-x"; + private const string SETTINGS_POS_Y = "pos-y"; + private const string SETTINGS_START_ON_STARRED = "start-on-starred"; + private const string SETTINGS_STREAM_INFO = "stream-info"; + private const string SETTINGS_STREAM_INFO_FAST = "stream-info-fast"; + private const string SETTINGS_THEME_MODE = "theme-mode"; + private const string SETTINGS_VOLUME = "volume"; + private const string SETTINGS_WINDOW_HEIGHT = "window-height"; + private const string SETTINGS_WINDOW_WIDTH = "window-width"; + + public bool auto_play { get; set; } + public bool do_not_vote { get; set; } + public string last_played_station { get; set; } + public bool start_on_starred { get; set; } + public bool stream_info { get; set; } + public bool stream_info_fast { get; set; } + public string theme_mode { get; set; } + public double volume { get; set; } + + private int _pos_x; + private int _pos_y; + private int _window_height; + private int _window_width; + + + public Settings() { + Object( + schema_id : Application.APP_ID + ); + + _pos_x = get_int(SETTINGS_POS_X); + _pos_y = get_int(SETTINGS_POS_Y); + _window_height = get_int(SETTINGS_WINDOW_HEIGHT); + _window_width = get_int(SETTINGS_WINDOW_WIDTH); + + auto_play = get_boolean(SETTINGS_AUTO_PLAY); + do_not_vote = get_boolean(SETTINGS_DO_NOT_VOTE); + last_played_station = get_string(SETTINGS_LAST_PLAYED_STATION); + start_on_starred = get_boolean(SETTINGS_START_ON_STARRED); + stream_info = get_boolean(SETTINGS_STREAM_INFO); + stream_info_fast = get_boolean(SETTINGS_STREAM_INFO_FAST); + theme_mode = get_string(SETTINGS_THEME_MODE); + volume = get_double(SETTINGS_VOLUME); + } // Settings + + + public void configure() + { + app().window.resize(_window_width, _window_height); + app().window.move(_pos_x, _pos_y); + app().player.volume = _volume; + + } // configure + + + public void save() + { + app().window.get_position(out _pos_x, out _pos_y); + if ( _pos_x !=0 && _pos_y != 0 ) + { + set_int(SETTINGS_POS_X, _pos_x); + set_int(SETTINGS_POS_Y, _pos_y); + } + + set_int(SETTINGS_WINDOW_HEIGHT, app().window.height); + set_int(SETTINGS_WINDOW_WIDTH, app().window.width); + + set_boolean(SETTINGS_AUTO_PLAY, auto_play); + set_boolean(SETTINGS_DO_NOT_VOTE, do_not_vote); + set_string(SETTINGS_LAST_PLAYED_STATION, last_played_station); + set_boolean(SETTINGS_START_ON_STARRED, start_on_starred); + set_boolean(SETTINGS_STREAM_INFO, stream_info); + set_boolean(SETTINGS_STREAM_INFO_FAST, stream_info_fast); + set_string(SETTINGS_THEME_MODE, theme_mode); + set_double(SETTINGS_VOLUME, app().player.volume); + } // save +} // Tuner.Settings diff --git a/src/Widgets/AboutDialog.vala b/src/Widgets/AboutDialog.vala index 2181529..84f0bc3 100644 --- a/src/Widgets/AboutDialog.vala +++ b/src/Widgets/AboutDialog.vala @@ -1,6 +1,13 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file AboutDialog.vala + * + * @brief App About dialogue + * */ /** @@ -23,21 +30,22 @@ public class Tuner.AboutDialog : Gtk.AboutDialog { set_transient_for (window); set_modal (true); - artists = {"https://faleksandar.com/"}; - authors = {"Louis Brauer"}; - documenters = null; - translator_credits = """French translation by NathanBnm https://github.com/NathanBnm + artists = {"https://faleksandar.com/"}; + authors = {"Louis Brauer, technosf"}; + documenters = null; + translator_credits = """French translation by NathanBnm https://github.com/NathanBnm Italian translation by DevAlien https://github.com/DevAlien and albanobattistella https://github.com/albanobattistella Dutch translation by Vistaus https://github.com/Vistaus Turkish translation by safak45x https://github.com/safak45x"""; - logo_icon_name = Application._instance.get_application_id (); - program_name = "Tuner"; - comments = "Listen to internet radio stations"; - copyright = "Copyright © 2020-2024 Louis Brauer"; - version = @"v$(Application.APP_VERSION)"; + logo_icon_name = app().get_application_id (); + program_name = "Tuner"; + comments = "Find & listen to internet radio stations"; + copyright = "Copyright © 2020-2024 Louis Brauer\nCopyright © 2024 technosf https://github.com/technosf"; + version = @"v$(Application.APP_VERSION)"; - license = """* Copyright (c) 2020-2024 Louis Brauer + license = """* Copyright (c) 2020-2024 Louis Brauer , + Copyright © 2024 technosf Tuner is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -53,13 +61,14 @@ You should have received a copy of the GNU General Public License along with Tuner. If not, see ."""; wrap_license = true; - website = "https://github.com/louis77/tuner"; - website_label = "Tuner on Github"; + website = "https://github.com/louis77/tuner"; + website_label = "Tuner on Github"; - response.connect ((response_id) => { - if (response_id == Gtk.ResponseType.CANCEL || response_id == Gtk.ResponseType.DELETE_EVENT) { - hide_on_delete (); - } - }); - } + response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.CANCEL || response_id == Gtk.ResponseType.DELETE_EVENT) + { + hide_on_delete (); + } + }); + } } diff --git a/src/Widgets/ContentBox.vala b/src/Widgets/ContentBox.vala deleted file mode 100644 index 7ed39fd..0000000 --- a/src/Widgets/ContentBox.vala +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ -/** - * @file ContentBox.vala - * @brief Defines the ContentBox widget for displaying content with a header and action button. - * - * This file contains the implementation of the ContentBox class, which is a custom - * Gtk.Box widget used to display content with a header, optional icon, and an - * optional action button. It provides a flexible layout for presenting various - * types of content within the Tuner application. - * - * @namespace Tuner - * @class ContentBox - * @extends Gtk.Box - */ - -using Gee; - -/** - * @class ContentBox - * @brief A custom Gtk.Box widget for displaying content with a header and action button. - * - * The ContentBox class is a versatile widget used to present various types of content - * within the Tuner application. It features a header with an optional icon and action - * button, and a content area that can display different views based on the current state. - * - * @extends Gtk.Box - */ -public class Tuner.ContentBox : Gtk.Box { - - /** - * @property header_label - * @brief The label displayed in the header of the ContentBox. - */ - public HeaderLabel header_label; - - /** - * @signal action_activated_sig - * @brief Emitted when the action button is clicked. - */ - public signal void action_activated_sig (); - - /** - * @signal content_changed_sig - * @brief Emitted when the content of the ContentBox is changed. - */ - public signal void content_changed_sig (); - - - private Gtk.Box _header; - private Gtk.Box _content; - private AbstractContentList _content_list; - private Gtk.Stack _stack; - - - /** - * @brief Constructs a new ContentBox instance. - * - * @param icon The optional icon to display in the header. - * @param title The title text for the header. - * @param subtitle An optional subtitle to display below the header. - * @param action_icon_name The name of the icon for the action button. - * @param action_tooltip_text The tooltip text for the action button. - */ - public ContentBox (Gtk.Image? icon, - string title, - string? subtitle, - string? action_icon_name, - string? action_tooltip_text) - { - Object ( - orientation: Gtk.Orientation.VERTICAL, - spacing: 0 - ); - - _stack = new Gtk.Stack (); - - var alert = new Granite.Widgets.AlertView (_("Nothing here"), _("Something went wrong loading radio stations data from radio-browser.info. Please try again later."), "dialog-warning"); - /* - alert.show_action ("Try again"); - alert.action_activated.connect (() => { - realize (); - }); - */ - - _stack.add_named (alert, "alert"); - - var no_results = new Granite.Widgets.AlertView (_("No stations found"), _("Please try a different search term."), "dialog-warning"); - _stack.add_named (no_results, "nothing-found"); - - _header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); - _header.homogeneous = false; - - if (icon != null) { - _header.pack_start (icon, false, false, 20); - } - - // header_label = new HeaderLabel (title); - // header_label.xpad = 20; - // header_label.ypad = 20; - // _header.pack_start (header_label, false, false); - _header.pack_start (new HeaderLabel (title, 20, 20 ), false, false); - - if (action_icon_name != null && action_tooltip_text != null) { - var shuffle_button = new Gtk.Button.from_icon_name ( - action_icon_name, - Gtk.IconSize.LARGE_TOOLBAR - ); - shuffle_button.valign = Gtk.Align.CENTER; - shuffle_button.tooltip_text = action_tooltip_text; - shuffle_button.clicked.connect (() => { action_activated_sig (); }); - _header.pack_start (shuffle_button, false, false); - } - - pack_start (_header, false, false); - - if (subtitle != null) { - var subtitle_label = new Gtk.Label (subtitle); - pack_start (subtitle_label); - } - - pack_start (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), false, false); - - _content = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - _content.get_style_context ().add_class ("color-light"); - _content.valign = Gtk.Align.START; - _content.get_style_context().add_class("welcome"); - - var scroller = new Gtk.ScrolledWindow (null, null); - scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; - scroller.add (_content); - scroller.propagate_natural_height = true; - - _stack.add_named (scroller, "content"); - add (_stack); - - show.connect (() => { - _stack.set_visible_child_full ("content", Gtk.StackTransitionType.NONE); - }); - } // ContentBox - - - /** - * @brief Initializes the ContentBox instance. - * - * This method is called automatically by the Vala compiler and sets up - * the initial style context for the widget. - */ - construct { - get_style_context ().add_class ("color-dark"); - } - - - /** - * @brief Displays the alert view in the content area. - */ - public void show_alert () { - _stack.set_visible_child_full ("alert", Gtk.StackTransitionType.NONE); - } - - - /** - * @brief Displays the "nothing found" view in the content area. - */ - public void show_nothing_found () { - _stack.set_visible_child_full ("nothing-found", Gtk.StackTransitionType.NONE); - } - - - /** - * @property content - * @brief Gets or sets the content list displayed in the ContentBox. - * - * When setting this property, it replaces the current content with the new - * AbstractContentList and emits the content_changed_sig signal. - */ - public AbstractContentList content { - set { - - foreach (var child in _content.get_children ()) { child.destroy (); } - - _stack.set_visible_child_full ("content", Gtk.StackTransitionType.NONE); - _content_list = value; - _content.add (_content_list); - _content.show_all (); - content_changed_sig (); - } - - get { - return _content_list; - } - } - - - -} diff --git a/src/Widgets/CountryList.vala b/src/Widgets/CountryList.vala index 02bc2c8..a76ae31 100644 --- a/src/Widgets/CountryList.vala +++ b/src/Widgets/CountryList.vala @@ -10,44 +10,37 @@ * This class extends AbstractContentList to create a specialized list * for displaying countries, potentially for selecting radio stations by country. * - * @extends AbstractContentList + * @extends ListFlowBox */ -public class Tuner.CountryList : AbstractContentList { +public class Tuner.CountryList : ListFlowBox +{ - /** - * @brief Constructs a new CountryList. - * - * Initializes the CountryList with specific layout properties. - */ - public CountryList () { - Object ( - homogeneous: false, - min_children_per_line: 2, - max_children_per_line: 2, - column_spacing: 5, - row_spacing: 5, - border_width: 20, - valign: Gtk.Align.START, - selection_mode: Gtk.SelectionMode.NONE - ); - } - - /** - * @brief Initializes the CountryList with a sample button. - */ - construct { - var button = new Gtk.Button (); - button.label = "a country"; - - add (button); - } +/** + * @brief Constructs a new CountryList. + * + * Initializes the CountryList with specific layout properties. + */ + public CountryList () + { + Object ( + homogeneous: false, + min_children_per_line: 2, + max_children_per_line: 2, + column_spacing: 5, + row_spacing: 5, + border_width: 20, + valign: Gtk.Align.START, + selection_mode: Gtk.SelectionMode.NONE + ); + } - /** - * @property item_count - * @brief The number of items (countries) in the list. - * - * This property implements the abstract property from AbstractContentList. - */ - public override uint item_count { get; set; } + /** + * @brief Initializes the CountryList with a sample button. + */ + construct { + var button = new Gtk.Button (); + button.label = "a country"; + add (button); + } } diff --git a/src/Widgets/Display.vala b/src/Widgets/Display.vala new file mode 100644 index 0000000..a18a710 --- /dev/null +++ b/src/Widgets/Display.vala @@ -0,0 +1,758 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file Display.vala + * + * @brief Defines the below-headerbar display of stations in the Tuner application. + * + * This file contains the Display class, which implements visual elements for + * features such as a source list, content stack that display and manage Station + * settings and handles user actions like station selection. + * + * @since 2.0.0 + * + * @see Tuner.Application + * @see Tuner.DirectoryController + */ + + +using Gee; +using Granite.Widgets; + + +/** + * @brief Display class for managing organization and presentation of genres and thier stations + * + * Display should be initialized and re-initialized by its owning class + */ +public class Tuner.Display : Gtk.Paned, StationListHookup { + + private const string BACKGROUND_TUNER = "/io/github/louis77/tuner/icons/background-tuner"; + private const string BACKGROUND_JUKEBOX = "/io/github/louis77/tuner/icons/background-jukebox"; + private const int EXPLORE_CATEGORIES = 5; // How many explore categories to display + private const double BACKGROUND_OPACITY = 0.15; + private const int BACKGROUND_TRANSITION_TIME_MS = 1500; + private const Gtk.RevealerTransitionType BACKGROUND_TRANSITION_TYPE = Gtk.RevealerTransitionType.CROSSFADE; + + + /** + * @brief Signal emitted when a station is clicked. + * @param station The clicked station. + */ + public signal void station_clicked_sig (Model.Station station); + + + /** + * @brief Signal emitted when the favourites list changes. + */ + public signal void favourites_changed_sig (); + + + /** + * @brief Signal emitted to refresh starred stations. + */ + public signal void refresh_starred_stations_sig (); + + + /** + * @brief Signal emitted when a search is performed. + * @param text The search text. + */ + public signal void searched_for_sig(string text); + + + /** + * @brief Signal emitted when the search is focused. + */ + public signal void search_focused_sig(); + + + /** + * @property stack + * @brief The stack widget for managing different views. + */ + public Gtk.Stack stack { get; construct; } + + + /** + * @property source_list + * @brief The source list widget for displaying categories. + */ + public SourceList source_list { get; construct; } + + + /** + * @property directory + * @brief The directory controller for managing station data. + */ + public DirectoryController directory { get; construct; } + + + /* + Display Assets + */ + + private SourceList.ExpandableItem _selections_category = new SourceList.ExpandableItem (_("Selections")); + private SourceList.ExpandableItem _library_category = new SourceList.ExpandableItem (_("Library")); + private SourceList.ExpandableItem _saved_searches_category = new SourceList.ExpandableItem (_("Saved Searches")); + private SourceList.ExpandableItem _explore_category = new SourceList.ExpandableItem (_("Explore")); + private SourceList.ExpandableItem _genres_category = new SourceList.ExpandableItem (_("Genres")); + private SourceList.ExpandableItem _subgenres_category = new SourceList.ExpandableItem (_("Sub Genres")); + private SourceList.ExpandableItem _eras_category = new SourceList.ExpandableItem (_("Eras")); + private SourceList.ExpandableItem _talk_category = new SourceList.ExpandableItem (_("Talk, News, Sport")); + + + private bool _first_activation = true; // display has not been activated before + private bool _active = false; // display is active + private bool _shuffle = false; // Shuffle mode + private Gtk.Revealer _background_tuner = new Gtk.Revealer(); // Background image + private Gtk.Revealer _background_jukebox = new Gtk.Revealer(); // Background image + private Gtk.Overlay _overlay = new Gtk.Overlay (); + private StationSet jukebox_station_set; // Jukebox station set + private SearchController _search_controller; // Search controller + private StationListBox _search_results; // Search results list + + + + /* -------------------------------------------------------- + + Public Methods + + ---------------------------------------------------------- */ + + /** + * @brief Constructs a new Display instance. + * @param directory The directory controller to use. + */ + public Display(DirectoryController directory) + { + Object( + directory : directory, + source_list : new SourceList(), + stack : new Gtk.Stack () + ); + + jukebox_station_set = _directory.load_random_stations(1); + app().player.shuffle_requested_sig.connect(() => + { + if (_shuffle) + jukebox_shuffle.begin(); + }); + + var tuner = new Gtk.Image.from_resource (BACKGROUND_TUNER); + tuner.opacity = BACKGROUND_OPACITY; + _background_tuner.transition_duration = BACKGROUND_TRANSITION_TIME_MS; + _background_tuner.transition_type = BACKGROUND_TRANSITION_TYPE; + _background_tuner.reveal_child = true; + _background_tuner.child = tuner; + + var jukebox = new Gtk.Image.from_resource (BACKGROUND_JUKEBOX); + jukebox.opacity = BACKGROUND_OPACITY; + _background_jukebox.transition_duration = BACKGROUND_TRANSITION_TIME_MS; + _background_jukebox.transition_type = BACKGROUND_TRANSITION_TYPE; + _background_jukebox.reveal_child = false; + _background_jukebox.child = jukebox; + + var background = new Gtk.Fixed(); + background.add(_background_tuner); + background.add(_background_jukebox); + background.halign = Gtk.Align.CENTER; + background.valign = Gtk.Align.CENTER; + _overlay.add (background); + + + stack.transition_type = Gtk.StackTransitionType.CROSSFADE; + _overlay.add_overlay(stack); + + // --------------------------------------------------------------------------- + + // Set up the LHS directory structure + + _selections_category.collapsible = false; + _selections_category.expanded = true; + + _library_category.collapsible = false; + _library_category.expanded = false; + + _saved_searches_category.collapsible = true; + _saved_searches_category.expanded = false; + + _explore_category.collapsible = true; + _explore_category.expanded = false; + + _genres_category.collapsible = true; + _genres_category.expanded = false; + + _subgenres_category.collapsible = true; + _subgenres_category.expanded = false; + + _eras_category.collapsible = true; + _eras_category.expanded = false; + + _talk_category.collapsible = true; + _talk_category.expanded = false; + + + source_list.root.add (_selections_category); + source_list.root.add (_library_category); + source_list.root.add (_explore_category); + source_list.root.add (_genres_category); + source_list.root.add (_subgenres_category); + source_list.root.add (_eras_category); + source_list.root.add (_talk_category); + + source_list.ellipsize_mode = Pango.EllipsizeMode.NONE; + source_list.item_selected.connect ((item) => + // Syncs Item choice to Stack view + { + if (item is StationListItem) + ((StationListItem)item).populate( this ); + var selected_item = item.get_data ("stack_child"); + stack.visible_child_name = selected_item; + }); + + pack1 (source_list, false, false); + pack2 (_overlay, true, false); + set_position(200); + + } // Display + + + /* -------------------------------------------------------- + + Public + + ---------------------------------------------------------- + */ + + /** + * @brief Asynchronously shuffles to a new random station in jukebox mode + * + * If shuffle mode is active, selects and plays a new random station + * from the jukebox station set. + */ + public async void jukebox_shuffle(){ + if (!_shuffle) + return; + try + { + var station = jukebox_station_set.next_page().to_array()[0]; + station_clicked_sig(station); + } + catch (SourceError e) + { + warning(_(@"Could not get random station: $(e.message)")); + } + } // jukebox_shuffle + + + /** + * @brief Updates the display state based on activation status + * @param activate Whether to activate (true) or deactivate (false) the display + * + * Manages the display's active state and performs first-time initialization + * when needed. + */ + public void update_state( bool activate, bool start_on_starred ) + { + if ( _active && !activate ) + /* Present Offline look */ + { + _active = false; + return; + } + + if ( !_active && activate ) + // Move from not active to active + { + if (_first_activation) + // One time set up - do post initialization + { + // TBD + _first_activation = false; + initialize.begin(() => + { + if ( start_on_starred) choose_starred_stations(); // coresponding to same call in Window + }); + } + _active = true; + show_all(); + } + } // update_state + + + /** + * @brief Selects the starred stations view in the source list + * + * Changes the current view to show the user's starred stations by selecting + * the first child of the library category. + */ + public void choose_starred_stations() + { + source_list.selected = source_list.get_first_child (_library_category); + } // choose_star + + + + /* -------------------------------------------------------- + + Private Methods + + ---------------------------------------------------------- */ + + /** + * @brief Asynchronously initializes the display components + * + * Sets up all categories, loads initial station data, and configures + * signal handlers for various display components. + */ + private async void initialize(){ + _directory.load (); // Initialize the DirectoryController + + /* Initialize the directory contents */ + + /* --------------------------------------------------------------------------- + Discover + */ + + var discover = StationListBox.create ( stack + , source_list + , _selections_category + , "discover" + , "face-smile" + , "Discover" + , "Stations to Discover" + , false + ,_directory.load_random_stations(20) + , "Discover more stations" + , "media-playlist-shuffle-symbolic"); + + discover.action_button_activated_sig.connect (() => { + discover.item.populate( this, true ); + }); + + + /* --------------------------------------------------------------------------- + Trending + */ + create_category_specific + ( stack, + source_list, + _selections_category, + "trending", + "playlist-queue", + "Trending", + "Trending in the last 24 hours", + _directory.load_trending_stations(40) + ); + + /* --------------------------------------------------------------------------- + Popular + */ + + create_category_specific + ( stack + , source_list + , _selections_category + , "popular" + , "playlist-similar" + , "Popular" + , "Most-listened over 24 hours" + ,_directory.load_popular_stations(40) + ); + + + // --------------------------------------------------------------------------- + + jukebox(_selections_category); + + // --------------------------------------------------------------------------- + // Country-specific stations list + + // var item4 = new Granite.Widgets.SourceList.Item (_("Your Country")); + // item4.icon = new ThemedIcon ("emblem-web"); + // ContentBox c_country; + // c_country = create_content_box ("my-country", item4, + // _("Your Country"), null, null, + // stack, source_list, true); + // var c_slist = new StationList (); + // c_slist.selection_changed.connect (handle_station_click); + // c_slist.favourites_changed.connect (handle_favourites_changed); + + // --------------------------------------------------------------------------- + + /* --------------------------------------------------------------------------- + Starred + */ + + var starred = create_category_predefined + ( stack + , source_list + , _library_category + , "starred" + , "starred" + , _("Starred by You") + , _("Starred by You :") + ,_directory.get_starred() + ); + + starred.badge ( @"$(starred.item_count)\t"); + starred.parameter = @"$(starred.item_count)"; + + starred.item_count_changed_sig.connect (( item_count ) => + { + starred.badge ( @"$(starred.item_count)\t"); + starred.parameter = @"$(starred.item_count)"; + }); + + + // --------------------------------------------------------------------------- + // Search Results Box + + + _search_results = StationListBox.create + ( stack + , source_list + , _library_category + , "searched" + , "folder-saved-search" + , _("Recent Search") + , _("Search Results") + , false + , null + , _("Save this search") + , "starred-symbolic"); + + _search_results.tooltip_button.sensitive = false; + _search_controller = new SearchController(directory,this,_search_results ); + + _search_results.item_count_changed_sig.connect (( item_count, parameter ) => + { + if ( parameter.length > 0 && stack.get_child_by_name (parameter) == null ) // Search names are prefixed with > + { + _search_results.tooltip_button.sensitive = true; + return; + } + _search_results.tooltip_button.sensitive = false; + }); + + + // Add saved search from star press + _search_results.action_button_activated_sig.connect (() => + // FIXME - Causes double wrap of a widget + { + if (app().is_offline) + return; + _search_results.tooltip_button.sensitive = false; + var new_saved_search= + add_saved_search( _search_results.parameter, _directory.add_saved_search (_search_results.parameter)); + new_saved_search.list(_search_results.content); + source_list.selected = source_list.get_last_child (_saved_searches_category); + }); + + + // --------------------------------------------------------------------------- + // Saved Searches + + + // Add saved searches to category from Directory + var saved_searches = _directory.load_saved_searches(); + foreach( var search_term in saved_searches.keys) + { + add_saved_search( search_term, saved_searches.get (search_term)); + } + _saved_searches_category.icon = new ThemedIcon ("library-music"); + _library_category.add (_saved_searches_category); // Added as last item of library category + + // --------------------------------------------------------------------------- + + // Explore Categories category + // Get random categories and stations in them + if ( app().is_online) + { + uint explore = 0; + foreach (var tag in _directory.load_random_genres(EXPLORE_CATEGORIES)) + { + if ( Model.Genre.in_genre (tag.name)) break; // Predefined genre, ignore + create_category_specific( stack, source_list, _explore_category + , @"$(explore++)" // tag names can have charaters that are not suitable for name + , "playlist-symbolic" + , tag.name + , tag.name + , _directory.load_by_tag (tag.name)); + } + } + + // --------------------------------------------------------------------------- + + // Genre Boxes + create_category_genre( stack, source_list, _genres_category, _directory, Model.Genre.GENRES ); + + // Sub Genre Boxes + create_category_genre( stack, source_list, _subgenres_category, _directory, Model.Genre.SUBGENRES ); + + // Eras Boxes + create_category_genre( stack, source_list, _eras_category, _directory, Model.Genre.ERAS ); + + // Talk Boxes + create_category_genre( stack, source_list, _talk_category, _directory, Model.Genre.TALK ); + + // -------------------------------------------------------------------- + + + app().stars.starred_stations_changed_sig.connect ((station) => + /* + * Refresh the starred stations list when a station is starred or unstarred + */ + { + if (app().is_offline && _directory.get_starred ().size > 0) + return; + var _slist = StationList.with_stations (_directory.get_starred ()); + station_list_hookup(_slist); + starred.content = _slist; + starred.parameter = @"$(starred.item_count)"; + starred.show_all(); + }); + + + search_focused_sig.connect (() => + /* Show searched stack when cursor hits search text area */ + { + stack.visible_child_name = "searched"; + }); + + + searched_for_sig.connect ((text) => + /* process the searched text, stripping it, and sensitizing the save + search star depending on if the search is already saved */ + { + _search_results.tooltip_button.sensitive = false; + _search_controller.handle_search_for(text); + }); + + source_list.selected = source_list.get_first_child(_selections_category); + + show(); + } // initialize + + + /* ------------------------------------------------- + + Helpers + + Shortcuts to configure the source_list and stack + + ------------------------------------------------- + */ + + /** + * @brief Configures the jukebox mode for a category. + * @param category The category to configure. + */ + private void jukebox(SourceList.ExpandableItem category) + { + SourceList.Item item = new SourceList.Item(_("Jukebox")); + item.icon = new ThemedIcon("jukebox"); + item.activated.connect(() => + { + _shuffle = true; + jukebox_shuffle.begin(); + app().player.shuffle_mode_sig(true); + _background_tuner.reveal_child = false; + _background_jukebox.reveal_child = true; + }); + + app().player.tape_counter_sig.connect((oldstation) => + { + if (_shuffle) + jukebox_shuffle.begin(); + }); + category.add(item); + } // jukebox + + + /** + * @brief Hooks up signals for a StationList. + * @param station_list The StationList to hook up. + * + * Configures signal handlers for station clicks and favorites changes. + */ + internal void station_list_hookup(StationList station_list) + { + station_list.station_clicked_sig.connect((station) => + { + station_clicked_sig(station); + if ( _shuffle ) + { + _shuffle = false; + app().player.shuffle_mode_sig(false); + _background_jukebox.reveal_child = false; + _background_tuner.reveal_child = true; + } // if + }); + } // station_list_hookup + + + + /** + * Adds a saved search to the display with the specified search term and station set. + * + * @param search The search term to be saved + * @param station_set The set of stations to associate with this search + * @param content Optional station list to be used as content. If null, a new list will be created + * + * @return Returns a StationListBox widget containing the search results + */ + private StationListBox add_saved_search(string search, StationSet station_set) //, StationList? content = null)//StationSet station_set) + { + var saved_search = create_category_specific + ( stack + , source_list + , _saved_searches_category + , search + , "playlist-symbolic" + , search + , _(@"Saved Search : $search") + , station_set + , _("Remove this saved search") + , "non-starred-symbolic" + ); + + // if ( content != null ) { + // saved_search.content = content; + // } + + saved_search.action_button_activated_sig.connect (() => { + if ( app().is_offline ) return; + _directory.remove_saved_search (search); + if ( _search_results.parameter == search ) + _search_results.tooltip_button.sensitive = true; + saved_search.delist (); + }); + + return saved_search; + } // refresh_saved_searches + + + /** + * @brief Creates a predefined category in the source list. + * @param stack The stack widget. + * @param source_list The source list widget. + * @param category The category to add to. + * @param name The name of the category. + * @param icon The icon for the category. + * @param title The title of the category. + * @param subtitle The subtitle of the category. + * @param stations The collection of stations for the category. + * @return The created SourceListBox for the category. + */ + private StationListBox create_category_predefined + ( Gtk.Stack stack + , Granite.Widgets.SourceList source_list + , Granite.Widgets.SourceList.ExpandableItem category + , string name + , string icon + , string title + , string subtitle + , Collection? stations + ) + { + var genre = StationListBox.create + ( stack + , source_list + , category + , name + , icon + , title + , subtitle + , true + ); + + if (stations != null) + { + var slist = StationList.with_stations (stations); + station_list_hookup(slist); + genre.content = slist; + } + + return genre; + + } // create_category_predefined + + + /** + * @brief Creates a specific category in the source list. + * @param stack The stack widget. + * @param source_list The source list widget. + * @param category The category to add to. + * @param name The name of the category. + * @param icon The icon for the category. + * @param title The title of the category. + * @param subtitle The subtitle of the category. + * @param station_set The set of stations for the category. + * @param action_tooltip_text Optional tooltip text for the action. + * @param action_icon_name Optional icon name for the action. + * @return The created SourceListBox for the category. + */ + private StationListBox create_category_specific + ( Gtk.Stack stack, + Granite.Widgets.SourceList source_list, + Granite.Widgets.SourceList.ExpandableItem category, + string name, + string icon, + string title, + string subtitle, + StationSet station_set, + string? action_tooltip_text = null, + string? action_icon_name = null + ) + { + var genre = StationListBox.create + ( stack, + source_list, + category, + name, + icon, + title, + subtitle, + false, + station_set, + action_tooltip_text, + action_icon_name + ); + + return genre; + } // create_category_specific + + + /** + * @brief Creates genre-specific categories in the source list. + * @param stack The stack widget. + * @param source_list The source list widget. + * @param category The category to add to. + * @param directory The directory controller. + * @param genres The array of genres. + */ + private void create_category_genre + ( Gtk.Stack stack, + Granite.Widgets.SourceList source_list, + Granite.Widgets.SourceList.ExpandableItem category, + DirectoryController directory, + string[] genres + ){ + foreach (var genre in genres ) + { + create_category_specific(stack, + source_list, + category, + genre, + "playlist-symbolic", + genre, + genre, + directory.load_by_tag (genre.down ())); + } + } // create_category_genre +} // Display diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index 4e444e2..fb7d848 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -1,66 +1,117 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file HeaderBar.vala + * + * @brief HeaderBar classes + * */ -/** +using Gtk; + +/* * @class Tuner.HeaderBar - * @brief Custom header bar for the Tuner application. * - * This class extends Gtk.HeaderBar to create a specialized header bar + * @brief Custom header bar that centrally displays station info and + * packs app controls either side. + * + * This class extends HeaderBar to create a specialized header bar * with play/pause controls, volume control, station information display, * search functionality, and preferences menu. * - * @extends Gtk.HeaderBar + * @extends HeaderBar */ -public class Tuner.HeaderBar : Gtk.HeaderBar { +public class Tuner.HeaderBar : Gtk.HeaderBar +{ /* Constants */ // Default icon name for stations without a custom favicon private const string DEFAULT_ICON_NAME = "internet-radio-symbolic"; - // Search delay in milliseconds - private const int SEARCH_DELAY = 400; + // Search delay in milliseconds + private const int SEARCH_DELAY = 400; + + // Search delay in milliseconds + private const uint REVEAL_DELAY = 400u; + + private static Image STAR = new Image.from_icon_name ("starred", IconSize.LARGE_TOOLBAR); + private static Image UNSTAR = new Image.from_icon_name ("non-starred", IconSize.LARGE_TOOLBAR); + /* Public */ - /** - * @enum PlayState - * @brief Enumeration of possible play states for the play button. - */ - public enum PlayState { - PAUSE_ACTIVE, - PAUSE_INACTIVE, - PLAY_ACTIVE, - PLAY_INACTIVE - } // Public properties - public Gtk.Button play_button { get; set; } - public Gtk.VolumeButton volume_button; // Signals - public signal void star_clicked_sig (bool starred); - public signal void searched_for_sig (string text); - public signal void search_focused_sig (); + public signal void searching_for_sig (string text); + public signal void search_has_focus_sig (); + + + /* + Private + */ + + protected static Image FAVICON_IMAGE = new Image.from_icon_name (DEFAULT_ICON_NAME, IconSize.DIALOG); - /* Private */ + private const string STREAM_METADATA = _("Stream Metadata"); - // Private member variables - private Gtk.Button _star_button; - private bool _starred = false; - private Model.Station _station; - private Gtk.Label _title_label; - private RevealLabel _subtitle_label; - private Gtk.Image _favicon_image; + /* + main display assets + */ + private Fixed _tuner = new Fixed(); + private Button _star_button = new Button.from_icon_name ( + "non-starred", + IconSize.LARGE_TOOLBAR + ); + private PlayButton _play_button = new PlayButton (); + private MenuButton _prefs_button = new MenuButton (); + private SearchEntry _search_entry = new SearchEntry (); + private ListButton _list_button = new ListButton.from_icon_name ("mark-location-symbolic", IconSize.LARGE_TOOLBAR); + /* + secondary display assets + */ + private Overlay _tuner_icon = new Overlay(); + private Image _tuner_on = new Image.from_icon_name("tuner-on", IconSize.DIALOG); + + // data and state variables + + private Model.Station _station; + private Mutex _station_update_lock = Mutex(); // Lock out concurrent updates + private bool _station_locked = false; + private ulong _station_handler_id = 0; + + private VolumeButton _volume_button = new VolumeButton(); // Search-related variables private uint _delayed_changed_id; private string _searchentry_text = ""; + private PlayerInfo _player_info; + + /** @property {bool} starred - Station starred. */ + private bool _starred = false; + private bool starred { + get { return _starred; } + set { + _starred = value; + if (!_starred) + { + _star_button.image = UNSTAR; + } + else + { + _star_button.image = STAR; + } + } + } // starred + /** * @brief Construct block for initializing the header bar components. @@ -69,261 +120,453 @@ public class Tuner.HeaderBar : Gtk.HeaderBar { * station info display, play button, preferences button, search entry, * star button, and volume button. */ - construct { - show_close_button = true; - - // - // Create and configure station info display - // - var station_info = new Gtk.Grid (); - station_info.width_request = 200; - station_info.column_spacing = 10; - - _title_label = new Gtk.Label (_("Choose a station")); - _title_label.get_style_context ().add_class (Granite.STYLE_CLASS_H4_LABEL); - _title_label.ellipsize = Pango.EllipsizeMode.MIDDLE; - _subtitle_label = new RevealLabel (); - _favicon_image = new Gtk.Image.from_icon_name (DEFAULT_ICON_NAME, Gtk.IconSize.DIALOG); - - station_info.attach (_favicon_image, 0, 0, 1, 2); - station_info.attach (_title_label, 1, 0, 1, 1); - station_info.attach (_subtitle_label, 1, 1, 1, 1); - - custom_title = station_info; - - - // - // Create and configure play button - // - play_button = new Gtk.Button (); - play_button.valign = Gtk.Align.CENTER; - play_button.action_name = Window.ACTION_PREFIX + Window.ACTION_PAUSE; - pack_start (play_button); - - - // - // Create and configure preferences button - // - var prefs_button = new Gtk.MenuButton (); - prefs_button.image = new Gtk.Image.from_icon_name ("open-menu", Gtk.IconSize.LARGE_TOOLBAR); - prefs_button.valign = Gtk.Align.CENTER; - prefs_button.sensitive = true; - prefs_button.tooltip_text = _("Preferences"); - prefs_button.popover = new Tuner.PreferencesPopover();; - pack_end (prefs_button); - - - // - // Create and configure search entry - // - var searchentry = new Gtk.SearchEntry (); - searchentry.valign = Gtk.Align.CENTER; - searchentry.placeholder_text = _("Station name"); - searchentry.changed.connect (() => { - _searchentry_text = searchentry.text; - reset_timeout(); - }); - searchentry.focus_in_event.connect ((e) => { - search_focused_sig (); - return true; - }); - pack_end (searchentry); - - - // - //Create and configure star button - // - _star_button = new Gtk.Button.from_icon_name ( - "non-starred", - Gtk.IconSize.LARGE_TOOLBAR - ); - _star_button.valign = Gtk.Align.CENTER; - _star_button.sensitive = true; - _star_button.tooltip_text = _("Star this station"); - _star_button.clicked.connect (() => { - star_clicked_sig (starred); // FIXME refresh faves? + public HeaderBar(Window window) + { + Object(); + + get_style_context ().add_class ("header-bar"); + + /* + LHS Controls + */ + + // Tuner icon + _tuner_icon.add(new Image.from_icon_name("tuner-off", IconSize.DIALOG)); + _tuner_icon.add_overlay(_tuner_on); + _tuner_icon.valign = Align.START; + + _tuner.add(_tuner_icon); + _tuner.set_valign(Align.CENTER); + _tuner.set_margin_bottom(5); // 20px padding on the right + _tuner.set_margin_start(5); // 20px padding on the right + _tuner.set_margin_end(5); // 20px padding on the right + _tuner.tooltip_text = _("Data Provider"); + _tuner.query_tooltip.connect((x, y, keyboard_tooltip, tooltip) => + { + if (app().is_offline) + return false; + tooltip.set_text(_(@"Data Provider: $(window.directory.provider())")); + return true; + }); + + // Volume + _volume_button.set_valign(Align.CENTER); + _volume_button.value_changed.connect ((value) => { + app().player.volume = value; }); - pack_start (_star_button); - - - // - // Create and configure volume button - // - volume_button = new Gtk.VolumeButton (); - volume_button.value = Application.instance.settings.get_double ("volume"); - volume_button.value_changed.connect ((value) => { - Application.instance.settings.set_double ("volume", value); + app().player.volume_changed_sig.connect((value) => { + _volume_button.value = value; }); - pack_start (volume_button); - - set_playstate (PlayState.PAUSE_INACTIVE); - } - - - /* Public */ - // Properties for title and subtitle - public new string title { - get { return _title_label.label; } - set { _title_label.label = value; } - } - - public new string subtitle { - get { return _subtitle_label.label; } - set { _subtitle_label.label = value; } - } + // Star button + _star_button.valign = Align.CENTER; + _star_button.sensitive = true; + _star_button.tooltip_text = _("Star this station"); + _star_button.clicked.connect (() => + { + if (_station == null) + return; + starred = _station.toggle_starred(); + }); + + + // + // Create and configure play button + // + _play_button.valign = Align.CENTER; + _play_button.action_name = Window.ACTION_PREFIX + Window.ACTION_PAUSE; // Toggles player state + + + /* + RHS Controls + */ + + // Search entry + _search_entry.placeholder_text = _("Station Search"); + _search_entry.set_margin_start(5); // 5 pixels padding on the left + _search_entry.valign = Align.CENTER; + + _search_entry.changed.connect (() => { + _searchentry_text = _search_entry.text; + reset_search_timeout(); + }); + + _search_entry.focus_in_event.connect ((e) => { + search_has_focus_sig (); + return true; + }); + + // Preferences button + _prefs_button.image = new Image.from_icon_name ("open-menu", IconSize.LARGE_TOOLBAR); + _prefs_button.valign = Align.CENTER; + // _prefs_button.sensitive = true; + _prefs_button.tooltip_text = _("Preferences"); + _prefs_button.popover = new Tuner.PreferencesPopover(); + + _list_button.valign = Align.CENTER; + _list_button.tooltip_text = _("History"); + + /* + Layout + */ + + // pack LHS + pack_start (_tuner); + pack_start (_volume_button); + pack_start (_star_button); + pack_start (_play_button); + + _player_info = new PlayerInfo(window); + custom_title = _player_info; // Station display + + // pack RHS + pack_end (_prefs_button); + pack_end (_list_button); + + /* Test fixture */ + // private Button _off_button = new Button.from_icon_name ("list-add", IconSize.LARGE_TOOLBAR); + // pack_end (_off_button); + // _off_button.clicked.connect (() => { + // app().is_online = !app().is_online; + // }); + + pack_end (_search_entry); + show_close_button = true; + + + /* + Tuner icon and online/offline behavior + */ + app().notify["is-online"].connect(() => { + check_online_status(); + }); + + check_online_status(); + + /* + Hook up title to metadata as tooltip + */ + custom_title.tooltip_text = STREAM_METADATA; + custom_title.query_tooltip.connect((x, y, keyboard_tooltip, tooltip) => + { + if (_station == null) + return false; + tooltip.set_text(_(@"$(_station.popularity())\n\n$(_player_info.metadata)")); + return true; + }); + + _player_info.info_changed_completed_sig.connect(() => + // _player_info is going to signal when it has completed and the lock can be released + { + if (!_station_locked) + return; + _station_update_lock.unlock(); + _station_locked = false; + }); + + + app().player.metadata_changed_sig.connect ((station, metadata) => + { + _list_button.append_station_title_pair(station, metadata.title); + }); + + _list_button.item_station_selected_sig.connect((station) => + { + window.handle_play_station(station); + }); + + } // HeaderBar + + + /* + Public + */ + + + /** + * @brief Update the header bar with information from a new station. + * + * Requires a lock so that too many clicks do not cause a race condition + * + * @param station The new station to display information for. + */ + public bool update_playing_station(Model.Station station) + { + if ( app().is_offline || ( _station != null && _station== station ) ) + return false; + + if (_station_update_lock.trylock()) + // Lock while changing the station to ensure single threading. + // Lock is released when the info is updated on emit of info_changed_completed_sig + { + _station_locked = true; + _player_info.metadata = STREAM_METADATA; + + Idle.add (() => + // Initiate the fade out on a non-UI thread + { + + if (_station_handler_id > 0) + // Disconnect the old station starred handler + { + _station.disconnect(_station_handler_id); + _station_handler_id = 0; + } + + _player_info.change_station.begin(station, () => + { + _station = station; + starred = _station.starred; + _station_handler_id = _station.station_star_changed_sig.connect((starred) => + { + this.starred = starred; + }); + }); + + return Source.REMOVE; + },Priority.HIGH_IDLE); + + return true; + } // if + return false; + } // update_playing_station + + + /** + * @brief Override of the realize method from Widget for an initial animation + * + * Called when the widget is being realized (created and prepared for display). + * This happens before the widget is actually shown on screen. + */ + public override void realize() + { + base.realize(); + + _player_info.transition_type = RevealerTransitionType.SLIDE_UP; // Optional: add animation + _player_info.set_transition_duration(REVEAL_DELAY*3); + + // Use Timeout to delay the reveal animation + Timeout.add(REVEAL_DELAY*3, () => { + _player_info.set_reveal_child(true); + return Source.REMOVE; + }); + } // realize - public Gtk.Image favicon { - get { return _favicon_image; } - set { _favicon_image = value; } - } /** - * @brief Handle changes in the current station. - * - * This method updates the starred state when the current station changes. */ - public void handle_station_change () { - starred = _station.starred; - } + public void stream_info(bool show) + { + _player_info.title_label.show_metadata = show; + } // stream_info + /** - * @brief Update the header bar with information from a new station. - * - * @param station The new station to display information for. */ - public void update_from_station (Model.Station station) { - if (_station != null) { - _station.notify.disconnect (handle_station_change); - } - load_favicon (station); // Kick off first as its async and long running in comparison - _station = station; - _station.notify.connect ( (sender, property) => { - handle_station_change (); - }); - title = station.title; - subtitle = _("Playing"); - starred = station.starred; - } - + public void stream_info_fast(bool fast) + { + _player_info.title_label.metadata_fast_cycle = fast; + } // stream_info_fast + + + /* + Private + */ + + /** + * @brief Checks and sets per the online status + * + * Desensitive when off-line + */ + private void check_online_status() + { + if (app().is_offline) + { + _player_info.favicon_image.opacity = 0.5; + _tuner_on.opacity = 0.0; + _star_button.sensitive = false; + _play_button.sensitive = false; + _play_button.opacity = 0.5; + _volume_button.sensitive = false; + _list_button.sensitive = false; + _search_entry.sensitive = false; + + } + else + { + _player_info.favicon_image.opacity = 1.0; + _tuner_on.opacity = 1.0; + _star_button.sensitive = true; + _play_button.sensitive = true; + _play_button.opacity = 1.0; + _volume_button.sensitive = true; + _list_button.sensitive = true; + _search_entry.sensitive = true; + } + } // check_online_status - /* Private */ /** - * @brief Reset the search timeout. - * - * This method removes any existing timeout and sets a new one for delayed search. - */ - private void reset_timeout(){ + * @brief Reset the search timeout. + * + * This method removes any existing timeout and sets a new one for delayed search. + */ + private void reset_search_timeout() + { if(_delayed_changed_id > 0) Source.remove(_delayed_changed_id); - // _delayed_changed_id = Timeout.add(SEARCH_DELAY, search_timeout); - _delayed_changed_id = Timeout.add(SEARCH_DELAY, () => { - - _delayed_changed_id = 0; // Reset timeout ID after scheduling - searched_for_sig (_searchentry_text); // Emit the custom signal with the search query - - return Source.REMOVE; - }); - } - - - // Property for starred state - private bool starred { - get { return _starred; } - set { - _starred = value; - if (!_starred) { - _star_button.image = new Gtk.Image.from_icon_name ("non-starred", Gtk.IconSize.LARGE_TOOLBAR); - } else { - _star_button.image = new Gtk.Image.from_icon_name ("starred", Gtk.IconSize.LARGE_TOOLBAR); - } - } - } + _delayed_changed_id = Timeout.add(SEARCH_DELAY, () => + { + _delayed_changed_id = 0; // Reset timeout ID after scheduling + searching_for_sig (_searchentry_text); // Emit the custom signal with the search query + return Source.REMOVE; + }); + } // reset_search_timeout - /** - * @brief Set the play state of the header bar. - * - * This method updates the play button icon and sensitivity based on the new play state. - * - * @param state The new play state to set. - */ - public void set_playstate (PlayState state) { - switch (state) { - case PlayState.PLAY_ACTIVE: - play_button.image = new Gtk.Image.from_icon_name ( - "media-playback-start-symbolic", - Gtk.IconSize.LARGE_TOOLBAR - ); - play_button.sensitive = true; - _star_button.sensitive = true; - break; - - case PlayState.PLAY_INACTIVE: - play_button.image = new Gtk.Image.from_icon_name ( - "media-playback-pause-symbolic", - Gtk.IconSize.LARGE_TOOLBAR - ); - play_button.sensitive = false; - _star_button.sensitive = false; - break; - - case PlayState.PAUSE_ACTIVE: - play_button.image = new Gtk.Image.from_icon_name ( - "media-playback-stop-symbolic", - Gtk.IconSize.LARGE_TOOLBAR - ); - play_button.sensitive = true; - _star_button.sensitive = true; - break; - - case PlayState.PAUSE_INACTIVE: - play_button.image = new Gtk.Image.from_icon_name ( - "media-playback-stop-symbolic", - Gtk.IconSize.LARGE_TOOLBAR - ); - play_button.sensitive = false; - _star_button.sensitive = false; - break; - } - } /** - * @brief Load and display the favicon for a station. - * - * This method asynchronously loads the favicon anew for - * the given station and updates the favicon image. - * If the favicon is not available from the site, - * it will load the cached favicon or use the default icon. - * - * @param station The station whose favicon should be loaded. + * @brief Custom PlayerInfo for the HeadeBar based on Revealer + * + * This is the PlayerInfo for the Player. */ - private void load_favicon(Model.Station station) + private class PlayerInfo : Revealer { - this.favicon.clear (); - - // Load and force a refresh of the favicon to freshen the cache - Favicon.load_async.begin (station, true, (favicon, res) => { - var pxbuf = Favicon.load_async.end (res); - if (pxbuf != null) { - this.favicon.set_from_pixbuf (pxbuf); - this.favicon.set_size_request (48, 48); - return; - } - }); + public Label station_label { get; private set; } + public CyclingRevealLabel title_label { get; private set; } + public StationContextMenu menu { get; private set; } + public Image favicon_image = new Image.from_icon_name (DEFAULT_ICON_NAME, IconSize.DIALOG); + public string metadata { get; internal set; } + + private Model.Station _station; + private uint grid_min_width = 0; + + internal signal void info_changed_completed_sig (); + - // If favicon is not available, use the current cached favicon or default icon - Favicon.load_async.begin (station, false, (favicon, res) => { - var pxbuf = Favicon.load_async.end (res); - if (pxbuf != null) { - this.favicon.set_from_pixbuf (pxbuf); - } else { - // If favicon is not available, use default icon - this.favicon.set_from_icon_name (DEFAULT_ICON_NAME, Gtk.IconSize.DIALOG); + + /** + * Creates a new PlayerInfo widget. + * + * @param window The parent window this header bar belongs to + * + * @since 1.0 + */ + public PlayerInfo(Window window) + { + Object(); + + transition_duration = REVEAL_DELAY; + transition_type = RevealerTransitionType.CROSSFADE; + + station_label = new Label ( "Tuner" ); + station_label.get_style_context ().add_class ("station-label"); + station_label.ellipsize = Pango.EllipsizeMode.MIDDLE; + + title_label = new CyclingRevealLabel (window,100); + title_label.get_style_context ().add_class ("track-info"); + title_label.halign = Align.CENTER; + title_label.valign = Align.CENTER; + title_label.show_metadata = window.settings.stream_info; + title_label.metadata_fast_cycle =window.settings.stream_info_fast; + + var station_grid = new Grid (); + station_grid.column_spacing = 10; + station_grid.set_halign(Align.FILL); + station_grid.set_valign(Align.CENTER); + + station_grid.attach (favicon_image, 0, 0, 1, 2); + station_grid.attach (station_label, 1, 0, 1, 1); + station_grid.attach (title_label, 1, 1, 1, 1); + + station_grid.size_allocate.connect((allocate) => + { + if (grid_min_width == 0) + grid_min_width = allocate.width; + }); + + add(station_grid); + reveal_child = false; // Make it invisible initially + + metadata = STREAM_METADATA; + app().player.metadata_changed_sig.connect (handle_metadata_changed); + + } // PlayerInfo + + + /** + * @brief Handles display transition as station is changed + * + * Desensitive when off-line + */ + internal async void change_station( Model.Station station ) + { + reveal_child = false; + + Idle.add(() => + { + Timeout.add (5*REVEAL_DELAY/3, () => + // Clear the info well after the fade has completed + { + favicon_image.clear(); + title_label.clear(); + station_label.label = ""; + _metadata = STREAM_METADATA; + return Source.REMOVE; + }); + + Timeout.add (3*REVEAL_DELAY, () => + // Redisplay after fade out and clear have completed + { + station.update_favicon_image.begin(favicon_image, true, DEFAULT_ICON_NAME,() => + { + _station = station; + station_label.label = station.name; + reveal_child = true; + title_label.cycle(); + + info_changed_completed_sig(); + }); + return Source.REMOVE; + }); + return Source.REMOVE; + }, Priority.HIGH_IDLE); + } // change_station + + + /** + * @brief Processes changes to stream metadata as they come in + * + * Desensitive when off-line + */ + public void handle_metadata_changed ( Model.Station station, PlayerController.Metadata metadata ) + { + if (_metadata == metadata.pretty_print) + return; // No change + + _metadata = metadata.pretty_print; + // Empty metadata stream + if ( _metadata == "" ) + { + _metadata = STREAM_METADATA; + return; } - this.favicon.set_size_request (48, 48); - }); - } -} + + // title_label.set_text( metadata.title ); + title_label.add_sublabel(1, metadata.genre,metadata.homepage); + title_label.add_sublabel(2, metadata.audio_info); + title_label.add_sublabel( 3, (metadata.org_loc) ); + + if ( !title_label.set_text( metadata.title ) ) + { + Timeout.add_seconds (3, () => + // Redisplay after fade out and clear have completed + { + title_label.set_text( metadata.title ); + return Source.REMOVE; + }); + } // if + } // handle_metadata_changed + } // PlayerInfo +} // Tuner.HeaderBar diff --git a/src/Widgets/HeaderLabel.vala b/src/Widgets/HeaderLabel.vala deleted file mode 100644 index 17ffe42..0000000 --- a/src/Widgets/HeaderLabel.vala +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -/** - * @class HeaderLabel - * @brief A custom label widget for headers. - * - * This class extends Gtk.Label to create a specialized label - * for use as headers in the application. - * - * @extends Gtk.Label - */ -public class Tuner.HeaderLabel : Gtk.Label { - - public HeaderLabel (string label, int xpad = 0, int ypad = 0 ) { - Object ( - label: label, - xpad: xpad, - ypad: ypad - ); - } - - construct { - halign = Gtk.Align.START; - xalign = 0; - get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL); - } - -} diff --git a/src/Widgets/ListButton.vala b/src/Widgets/ListButton.vala new file mode 100644 index 0000000..e8bd202 --- /dev/null +++ b/src/Widgets/ListButton.vala @@ -0,0 +1,186 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file ListButton.vala + * + * @brief ListButton classes + * + */ + +using Gtk; + +/** + * @class ListButton + * @brief A custom button with a dropdown menu for station selection and context actions + * + * The ListButton class provides a button that displays a dropdown menu of stations + * and allows for context actions such as copying the list to the clipboard or clearing + * all items from the menu. + * + * @extends Gtk.Button + */ +public class Tuner.ListButton : Gtk.Button +{ +/** + * @signal item_station_selected_sig + * @brief Emitted when a station is selected from the dropdown menu. + * @param station The selected station. + */ + public signal void item_station_selected_sig(Model.Station station); + + private Gtk.Menu dropdown_menu; + private Gtk.Menu context_menu; + private List menu_items; + private StringBuilder clipboard_text = new StringBuilder(); + + Model.Station last_station; + string last_title; + Gtk.MenuItem last_menu_item; + +/** + * @brief Constructs a new ListButton with an icon. + * @param icon_name The name of the icon to display on the button. + * @param size The size of the icon. + */ + public ListButton.from_icon_name(string? icon_name, IconSize size = IconSize.BUTTON) + { + Object(); + var image = new Image.from_icon_name(icon_name, size); + this.set_image(image); + this.dropdown_menu = new Gtk.Menu(); + this.clicked.connect(() => { + if (menu_items.length() > 0) + { + this.dropdown_menu.popup_at_widget(this, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null); + limit_dropdown_menu_width(); + } + }); + initialize_context_menu(); + } + +/** + * @brief Constructs a new ListButton without an icon. + */ + public ListButton() + { + Object(); + this.dropdown_menu = new Gtk.Menu(); + this.clicked.connect(() => { + if (menu_items.length() > 0) + { + this.dropdown_menu.popup_at_widget(this, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null); + limit_dropdown_menu_width(); + } + }); + initialize_context_menu(); + } + + +/** + * @brief Initializes the context menu with copy and clear actions. + */ + private void initialize_context_menu() + { + menu_items = new List(); + context_menu = new Gtk.Menu(); + + var copy_item = new Gtk.MenuItem.with_label("Copy List to Clipboard"); + copy_item.activate.connect(() => { + copy_list_to_clipboard(); + context_menu.popdown(); + dropdown_menu.popdown(); + }); + context_menu.append(copy_item); + + var clear_item = new Gtk.MenuItem.with_label("Clear All Items"); + clear_item.activate.connect(() => { + clear_all_items(); + context_menu.popdown(); + dropdown_menu.popdown(); + }); + context_menu.append(clear_item); + context_menu.show_all(); + } + +/** + * @brief Copies the list of menu items to the clipboard. + */ + private void copy_list_to_clipboard() + { + var clipboard = Gtk.Clipboard.get_default(Gdk.Display.get_default()); + clipboard.set_text(clipboard_text.str, -1); + } + +/** + * @brief Clears all items from the dropdown menu. + */ + private void clear_all_items() + { + foreach (var item in menu_items) + { + dropdown_menu.remove(item); + } + menu_items = new List(); + last_menu_item = null; + last_station = null; + last_title = ""; + clipboard_text.truncate(); + } + +/** + * @brief Appends a station-title pair to the dropdown menu. + * @param station The station to add. + * @param title The title associated with the station. + */ + public void append_station_title_pair(Model.Station station, string title) + { + if (station == last_station && title == last_title) + return; + if (station == last_station && "" == last_title && last_menu_item != null) + { + dropdown_menu.remove(last_menu_item); + int pos = clipboard_text.str.index_of("\n", clipboard_text.str.index_of("\n") + 1) + 1; + clipboard_text.erase(0, pos); + } + var label = station.name + "\n\t" + title; + var item = new Gtk.MenuItem.with_label(label); + menu_items.append(item); + + item.button_press_event.connect((event) => { + if (event.button == 1) // Left click + { + item_station_selected_sig(station); + dropdown_menu.popdown(); + return true; + } + else if (event.button == 3) // Right click + { + context_menu.popup_at_pointer(event); + return true; + } + return false; + }); + + item.show(); + dropdown_menu.prepend(item); + last_menu_item = item; + last_station = station; + last_title = title; + clipboard_text.prepend(label + "\n"); + } + +/** + * @brief Limits the width of the dropdown menu to 2/3 of the header bar width. + */ + private void limit_dropdown_menu_width() + { + var header_bar = this.get_toplevel() as Gtk.HeaderBar; + if (header_bar != null) + { + var max_width = header_bar.get_allocated_width() * 2 / 3; + dropdown_menu.set_size_request(max_width, -1); + } + } +} diff --git a/src/Widgets/PlayButton.vala b/src/Widgets/PlayButton.vala new file mode 100644 index 0000000..68b090b --- /dev/null +++ b/src/Widgets/PlayButton.vala @@ -0,0 +1,108 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file PlayButton.vala + * @author technosf + * @date 2024-12-01 + * @since 2.0.0 + * @brief Player 'PLAY' button + */ + +using Gtk; + +/** + * @class PlayButton + * + * @brief A custom widget that shows player state. + * + * PlayButton can control the player and does so by an ActionEvent linkage defined in the HeaderBar + * + * @extends Gtk.Button + */ +public class Tuner.PlayButton : Gtk.Button +{ + +/* Constants */ + + private Image PLAY = new Gtk.Image.from_icon_name ( + "media-playback-start-symbolic", + IconSize.LARGE_TOOLBAR + ); + + private Image BUFFERING = new Gtk.Image.from_icon_name ( + "media-playback-pause-symbolic", + IconSize.LARGE_TOOLBAR + ); + + private Image STOP = new Gtk.Image.from_icon_name ( + "media-playback-stop-symbolic", + IconSize.LARGE_TOOLBAR + ); + + private Image ERROR = new Gtk.Image.from_icon_name ( + "dialog-error-symbolic", + IconSize.LARGE_TOOLBAR + ); + + +/* Public */ + +/** + * @class PlayButton + * + * @brief Create the play button and hook it up to the PlayerController + * + */ + public PlayButton() + { + Object(); + + image = PLAY; + sensitive = true; + + app().player.state_changed_sig.connect ((station, state) => + // Link the button image to the inverse of the player state + { + set_inverse_symbol (state); + }); + } + + +/** + * @brief Set the play button symbol and sensitivity + * + * This method is instigated from a Gst.Player state change signal. + * Performing any UI actions directly while handling the signal + * causes a segmentation fault. To get around this, threads_add_idle + * is used. + * + * @param state The new play state string. + */ + private void set_inverse_symbol (PlayerController.Is state) + { + switch (state) + { + case PlayerController.Is.PLAYING: + image = STOP; + image.opacity = 1.0; + break; + + case PlayerController.Is.BUFFERING: + image = BUFFERING; + image.opacity = 0.5; + break; + + case PlayerController.Is.STOPPED_ERROR: + image = ERROR; + image.opacity = 0.5; + break; + + default: // STOPPED: + image = PLAY; + image.opacity = 1.0; + break; + } + } // set_reverse_symbol +} // PlayButton diff --git a/src/Widgets/PreferencesPopover.vala b/src/Widgets/PreferencesPopover.vala index 7ccebf8..298a4a8 100644 --- a/src/Widgets/PreferencesPopover.vala +++ b/src/Widgets/PreferencesPopover.vala @@ -8,123 +8,212 @@ */ - /** +/** * * @brief Tuner preferences and selections. */ -public class Tuner.PreferencesPopover : Gtk.Popover { - - construct { - var about_menuitem = new Gtk.ModelButton (); - about_menuitem.text = _("About"); - about_menuitem.action_name = Window.ACTION_PREFIX + Window.ACTION_ABOUT; - - // Voting - var disable_tracking_item = new Gtk.ModelButton (); - disable_tracking_item.text = _("Do not track"); - disable_tracking_item.action_name = Window.ACTION_PREFIX + Window.ACTION_DISABLE_TRACKING; - disable_tracking_item.tooltip_text = _("If enabled, we will not send usage info to radio-browser.org"); - - //Theme - var theme_combo = new Gtk.ComboBoxText (); - theme_combo.append("system", _("Use System")); - theme_combo.append("light", _("Light mode")); - theme_combo.append("dark", _("Dark mode")); - theme_combo.halign = Gtk.Align.CENTER; - theme_combo.active_id = Application.instance.settings.get_string("theme-mode"); - - theme_combo.changed.connect ((elem) => { - warning(@"Theme changed: $(elem.active_id)"); - Application.instance.settings.set_string("theme-mode", elem.active_id); - }); - - var theme_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3); - theme_box.pack_end (theme_combo, true, true, 5); - theme_box.pack_end (new Gtk.Label(_("Theme")), false, false, 12); - - // Autoplay - var autoplay_item = new Gtk.ModelButton (); - autoplay_item.text = _("Auto-play last station on startup"); - autoplay_item.action_name = Window.ACTION_PREFIX + Window.ACTION_ENABLE_AUTOPLAY; - autoplay_item.tooltip_text = _("If enabled, when Tuner starts it will automatically start to play the last played station"); - - // Export starred - var export_starred = new Gtk.ModelButton (); - export_starred.text = _("Export Starred Sations to Playlist"); - export_starred.button_press_event.connect (() => - { - export_m3u8 (); - }); - - - // Layout - uint8 vpos = 0; - var menu_grid = new Gtk.Grid (); - menu_grid.margin_bottom = 10; - menu_grid.margin_top = 10; - menu_grid.margin_start = 5; - menu_grid.margin_end = 5; - menu_grid.row_spacing = 3; - menu_grid.orientation = Gtk.Orientation.VERTICAL; - - menu_grid.attach (theme_box, 0, vpos++); - - menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); - - menu_grid.attach (autoplay_item, 0, vpos++, 4, 1); - menu_grid.attach (disable_tracking_item, 0, vpos++, 4, 1); - - menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); - - menu_grid.attach (export_starred, 0, vpos++, 4, 1); - - menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); - menu_grid.attach (about_menuitem, 0, vpos++, 4, 1); - menu_grid.show_all (); - - this.add (menu_grid); - } // construct - - - /** - * @brief Export Starred Stations as a m3u playlist - * - * - */ - public void export_m3u8() - { - try { - string temp_file; - GLib.FileUtils.open_tmp ("XXXXXX.starred.m3u8", out temp_file); - GLib.FileUtils.set_contents(temp_file, Application._instance.window.store.export_m3u8 ()); - - // Create the file chooser dialog for saving - var dialog = new Gtk.FileChooserDialog( - "Save File", - null, - Gtk.FileChooserAction.SAVE - ); - - // Add buttons to the dialog - dialog.add_button("_Cancel", Gtk.ResponseType.CANCEL); - dialog.add_button("_Save", Gtk.ResponseType.ACCEPT); - - // Suggest a default filename - dialog.set_current_name("tuner-starred.m3u8"); - - if (dialog.run() == Gtk.ResponseType.ACCEPT) - { - string save_path = dialog.get_filename(); - // Copy the temp file to the chosen location - var source_file = GLib.File.new_for_path(temp_file); - var dest_file = GLib.File.new_for_path(save_path); - source_file.copy(dest_file, GLib.FileCopyFlags.OVERWRITE); // Overwrite - } - - dialog.destroy(); - - } catch (GLib.Error e) { - warning("Error: $(e.message)"); - } - } // export_m3u8 -} // PreferencesPopover +public class Tuner.PreferencesPopover : Gtk.Popover +{ + + construct { + var about_menuitem = new Gtk.ModelButton (); + about_menuitem.text = _("About"); + about_menuitem.action_name = Window.ACTION_PREFIX + Window.ACTION_ABOUT; + + // Voting + var disable_tracking_item = new Gtk.ModelButton (); + disable_tracking_item.text = _("Do not participate in Station voting"); + disable_tracking_item.action_name = Window.ACTION_PREFIX + Window.ACTION_DISABLE_TRACKING; + disable_tracking_item.tooltip_text = _("If checked, your starred and streamed stations will not be used to calculate the Station index popularity vote, nor the popular and trending stations"); + + + //Theme + var theme_combo = new Gtk.ComboBoxText (); + theme_combo.append(THEME.SYSTEM.get_name (), _("Use System")); + theme_combo.append(THEME.LIGHT.get_name (), _("Light mode")); + theme_combo.append(THEME.DARK.get_name (), _("Dark mode")); + theme_combo.halign = Gtk.Align.CENTER; + theme_combo.active_id = app().settings.theme_mode; // Initial state from settings + + theme_combo.changed.connect ((elem) => { + apply_theme_name(elem.active_id); + app().settings.theme_mode = elem.active_id; + }); + + var theme_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3); + theme_box.pack_end (theme_combo, true, true, 5); + theme_box.pack_end (new Gtk.Label(_("Theme")), false, false, 12); + + + // Autoplay + var autoplay_item = new Gtk.ModelButton (); + autoplay_item.text = _("Auto-play last station on startup"); + autoplay_item.action_name = Window.ACTION_PREFIX + Window.ACTION_ENABLE_AUTOPLAY; + autoplay_item.tooltip_text = _("If enabled, when Tuner starts it will automatically start to play the last played station"); + + + // Start on Starred + var start_on_starred = new Gtk.ModelButton (); + start_on_starred.text = _("Open to Starred Stations"); + start_on_starred.action_name = Window.ACTION_PREFIX + Window.ACTION_START_ON_STARRED; + start_on_starred.tooltip_text = _("If enabled, when Tuner starts it will open to the starred stations view"); + + + // Play Display + var stream_info = new Gtk.ModelButton (); + stream_info.text = _("Show stream info when playing"); + stream_info.action_name = Window.ACTION_PREFIX + Window.ACTION_STREAM_INFO; + stream_info.tooltip_text = _("Cycle through the metadata from the playing stream"); + + + var stream_info_fast = new Gtk.ModelButton (); + stream_info_fast.text = _("Faster cycling through stream info"); + stream_info_fast.action_name = Window.ACTION_PREFIX + Window.ACTION_STREAM_INFO_FAST; + stream_info_fast.tooltip_text = _("Fast cycle through the metadata from the playing stream if show stream info is enabled"); + + + // Export starred + var export_starred = new Gtk.ModelButton (); + export_starred.text = _("Export Starred Sations to Playlist"); + export_starred.button_press_event.connect (() => + { + export_m3u8 (); + }); + + + // Import starred + var import_starred = new Gtk.ModelButton (); + import_starred.text = _("Import Station UUIDs as Starred Sations"); + import_starred.button_press_event.connect (() => + { + import_stationuuids (); + }); + + + // Layout + uint8 vpos = 0; + var menu_grid = new Gtk.Grid (); + menu_grid.margin_bottom = 10; + menu_grid.margin_top = 10; + menu_grid.margin_start = 5; + menu_grid.margin_end = 5; + menu_grid.row_spacing = 3; + menu_grid.orientation = Gtk.Orientation.VERTICAL; + + menu_grid.attach (theme_box, 0, vpos++); + + menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); + + menu_grid.attach (autoplay_item, 0, vpos++, 4, 1); + menu_grid.attach (start_on_starred, 0, vpos++, 4, 1); + menu_grid.attach (stream_info, 0, vpos++, 4, 1); + menu_grid.attach (stream_info_fast, 0, vpos++, 4, 1); + + menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); + + menu_grid.attach (export_starred, 0, vpos++, 4, 1); + + menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); + + menu_grid.attach (import_starred, 0, vpos++, 4, 1); + + menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); + + menu_grid.attach (disable_tracking_item, 0, vpos++, 4, 1); + + menu_grid.attach (new Gtk.SeparatorMenuItem (), 0, vpos++, 4, 1); + + menu_grid.attach (about_menuitem, 0, vpos++, 4, 1); + menu_grid.show_all (); + + this.add (menu_grid); + } // construct + + +/** + * @brief Export Starred Stations as a m3u playlist + * + * + */ + public void export_m3u8() + { + try + { + string temp_file; + GLib.FileUtils.open_tmp ("XXXXXX.starred.m3u8", out temp_file); + GLib.FileUtils.set_contents(temp_file, app().stars.export_m3u8 ()); + + // Create the file chooser dialog for saving + var dialog = new Gtk.FileChooserDialog( + "Save File", + null, + Gtk.FileChooserAction.SAVE + ); + + // Add buttons to the dialog + dialog.add_button("_Cancel", Gtk.ResponseType.CANCEL); + dialog.add_button("_Save", Gtk.ResponseType.ACCEPT); + + // Suggest a default filename + dialog.set_current_name("tuner-starred.m3u8"); + + if (dialog.run() == Gtk.ResponseType.ACCEPT) + { + string save_path = dialog.get_filename(); + // Copy the temp file to the chosen location + var source_file = GLib.File.new_for_path(temp_file); + var dest_file = GLib.File.new_for_path(save_path); + source_file.copy(dest_file, GLib.FileCopyFlags.OVERWRITE); // Overwrite + } + + dialog.destroy(); + + } catch (GLib.Error e) + { + warning("Error: $(e.message)"); + } + } // export_m3u8 + + +/** + * @brief Select and read a file for Station UUIDs to be imported as Satrred + * + * + */ + public void import_stationuuids() + { + var dialog = new Gtk.FileChooserDialog( + "Choose a file", + app().window, + Gtk.FileChooserAction.OPEN, + "_Cancel", Gtk.ResponseType.CANCEL, + "_Open", Gtk.ResponseType.ACCEPT + ); + string filepath; + + if (dialog.run() == Gtk.ResponseType.ACCEPT) + { + filepath = dialog.get_filename(); + + try + { + var file = File.new_for_path(filepath); + FileInputStream stream = file.read(); + + // Read content into a string buffer + DataInputStream data_stream = new DataInputStream(stream); + + app().stars.import_stationuuids (data_stream); + + stream.close(); + } catch (Error e) + { + warning(@"Error reading file: $(e.message)"); + } + } // if + + dialog.destroy(); + + } // import_stationuuids + +} diff --git a/src/Widgets/RevealLabel.vala b/src/Widgets/RevealLabel.vala deleted file mode 100644 index 1653f9b..0000000 --- a/src/Widgets/RevealLabel.vala +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -/** - * @class RevealLabel - * @brief A custom widget that reveals a label with animation. - * - * This class extends Gtk.Revealer to create a label that can be revealed - * and hidden with smooth transitions. - * - * @extends Gtk.Revealer - */ -public class Tuner.RevealLabel : Gtk.Revealer { - - /** - * @brief Default duration for fade-in animation in milliseconds. - */ - private const uint DEFAULT_FADEIN_DURATION = 1000u; - - /** - * @brief Default duration for fade-out animation in milliseconds. - */ - private const uint DEFAULT_FADEOUT_DURATION = 500u; - - /** - * @property label_child - * @brief The Gtk.Label widget contained within the revealer. - */ - public Gtk.Label label_child { get; construct set; } - - /** - * @property label - * @brief The text displayed in the label. - * - * Setting this property triggers the reveal animation. - */ - public string label { - get { - return label_child.label; - } - set { - // Prevent transition if same title is submitted multiple times - if (label_child.label == value) return; - - GLib.Idle.add (() => { - transition_duration = DEFAULT_FADEOUT_DURATION; // milliseconds - reveal_child = false; - return GLib.Source.REMOVE; - }); - - GLib.Timeout.add (1000u, () => { - transition_type = Gtk.RevealerTransitionType.SLIDE_LEFT; - transition_duration = DEFAULT_FADEIN_DURATION; // milliseconds - label_child.label = value; - reveal_child = true; - - return GLib.Source.REMOVE; - }); - } - } - - /** - * @brief Initializes the RevealLabel with default properties. - */ - construct { - label_child = new Gtk.Label ("test"); - label_child.ellipsize = Pango.EllipsizeMode.MIDDLE; - child = label_child; - } - - - -} - diff --git a/src/Widgets/StationBox.vala b/src/Widgets/StationBox.vala deleted file mode 100644 index ad35111..0000000 --- a/src/Widgets/StationBox.vala +++ /dev/null @@ -1,142 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -/** - * @class StationBox - * @brief A custom button widget representing a radio station. - * - * The StationBox class extends the WelcomeButton class to create a specialized - * button for displaying radio station information. It includes the station's - * title, location, codec, bitrate, and favicon. - * - * @extends Tuner.WelcomeButton - */ -public class Tuner.StationBox : Tuner.WelcomeButton { - - /** - * @brief Default icon name for stations without a custom favicon. - */ - private const string DEFAULT_ICON_NAME = "internet-radio"; - - /** - * @property station - * @brief The radio station represented by this StationBox. - */ - public Model.Station station { get; construct; } - - /** - * @property menu - * @brief The context menu associated with this StationBox. - */ - public StationContextMenu menu { get; private set; } - - /** - * @brief Constructs a new StationBox instance. - * @param station The radio station to represent. - */ - public StationBox (Model.Station station) { - Object ( - description: make_description (station.location), - title: make_title (station.title, station.starred), - tag: make_tag (station.codec, station.bitrate), - favicon: new Gtk.Image.from_icon_name (DEFAULT_ICON_NAME, Gtk.IconSize.DIALOG), - station: station - ); - } - - /** - * @brief Additional initialization for the StationBox. - * - * This method is automatically called after construction and sets up - * the favicon, style context, and event handling for the StationBox. - */ - construct { - debug (@"StationBox construct $(station.title)"); - - load_favicon(); - - get_style_context().add_class("station-button"); - always_show_image = true; - - this.station.notify["starred"].connect ( (sender, prop) => { - this.title = make_title (this.station.title, this.station.starred); - }); - - - event.connect ((e) => { - if (e.type == Gdk.EventType.BUTTON_PRESS && e.button.button == 3) { - // Optimization: - // Create menu on demand not on construction - // because it is rarely used for all stations - if (menu == null) { - menu = new StationContextMenu (this.station); - menu.attach_to_widget (this, null); - menu.show_all (); - } - - menu.popup_at_pointer (); - return true; - } - return false; - }); - } - - /** - * @brief Creates a title string for the station. - * @param title The station's title. - * @param starred Whether the station is starred (favorited). - * @return The formatted title string. - */ - private static string make_title (string title, bool starred) { - if (!starred) return title; - return Application.STAR_CHAR + title; - } - - /** - * @brief Creates a tag string combining codec and bitrate information. - * @param codec The station's codec. - * @param bitrate The station's bitrate. - * @return The formatted tag string. - */ - private static string make_tag (string codec, int bitrate) { - var tag = codec; - if (bitrate > 0) - { - tag = tag + " " + bitrate.to_string() + "k"; - } - - return tag; - } - - /** - * @brief Creates a description string based on the station's location. - * @param location The station's location. - * @return The formatted description string. - */ - private static string make_description (string location) { - if (location.length > 0) - return _(location); - else - return location; - } - - /** - * @brief Asynchronously loads the station's favicon. - * - * This method attempts to load the station's custom favicon and - * updates the StationBox's icon if successful. - */ - private void load_favicon() - { - Favicon.load_async.begin (station, false, (favicon, res) => { - var pxbuf = Favicon.load_async.end (res); - if (pxbuf != null) { - this.favicon.set_from_pixbuf (pxbuf); - this.favicon.set_size_request (48, 48); - } - }); - } - -} diff --git a/src/Widgets/StationButton.vala b/src/Widgets/StationButton.vala new file mode 100644 index 0000000..4158228 --- /dev/null +++ b/src/Widgets/StationButton.vala @@ -0,0 +1,157 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * @class StationBox + * @brief A custom button widget representing a radio station. + * + * The StationBox class extends the WelcomeButton class to create a specialized + * button for displaying radio station information. It includes the station's + * title, location, codec, bitrate, and favicon. + * + * @extends Tuner.DisplayButton + */ +public class Tuner.StationButton : Tuner.DisplayButton +{ + + private const string DEFAULT_ICON_NAME = "internet-radio"; + + /** + * @property station + * @brief The radio station represented by this StationBox. + */ + public Model.Station station { get; construct; } + + /** + * @property menu + * @brief The context menu associated with this StationBox. + */ + public StationContextMenu menu { get; private set; } + + private ulong favicon_handler_id; + + /** + * @brief Constructs a new StationBox instance. + * @param station The radio station to represent. + */ + public StationButton (Model.Station station) + { + Object ( + description: make_description (station.countrycode), + title: make_title (station.name, station.starred, station.is_up_to_date), + tag: make_tag (station.codec, station.bitrate), + favicon_image: new Gtk.Image.from_icon_name (DEFAULT_ICON_NAME, Gtk.IconSize.DIALOG), + station: station + ); + + get_style_context().add_class("station-button"); + always_show_image = true; + + station.station_star_changed_sig.connect (() => + { + this.title = make_title (this.station.name, this.station.starred); + }); + + + event.connect ((e) => { + if (e.type == Gdk.EventType.BUTTON_PRESS && e.button.button == 3) { + // Optimization: + // Create menu on demand not on construction + // because it is rarely used for all stations + if (menu == null) { + menu = new StationContextMenu (this); + menu.attach_to_widget (this, null); + menu.show_all (); + } + + menu.popup_at_pointer (); + return true; + } + return false; + }); + + /* + Set the button image. Connect to the flag that the Station has loaded the favicon + and when it set, update the image. Check that if its already loaded, load now. + */ + favicon_handler_id = station.station_favicon_sig.connect(() => + { + station.disconnect(favicon_handler_id); + station.update_favicon_image.begin (_favicon_image); + }); + if ( station.favicon_loaded > 0 ) + { + station.disconnect(favicon_handler_id); + station.update_favicon_image.begin (_favicon_image); + } + } // StationButton + + + /** + * @brief Updates the station button with current information from the provider. + * + * @param starred Whether the station is starred (favorited). + */ + public void update(bool starred = false) + { + _station = station.updated(); + station.update_favicon_image.begin (_favicon_image); + _station.starred = starred; + description = make_description (station.countrycode); + title = make_title (station.name, station.starred, station.is_up_to_date); + tag = make_tag (station.codec, station.bitrate); + app().stars.update (station); + } // update + + + /** + * @brief Creates a title string for the station. + * @param title The station's title. + * @param starred Whether the station is starred (favorited). + * @param is_up_to_date Whether the station information is up to date. + * @return The formatted title string. + */ + private static string make_title (string title, bool starred,bool is_up_to_date = true) + { + if (!starred) + return title; + if (!is_up_to_date) + return Application.EXCLAIM_CHAR + title; + return Application.STAR_CHAR + title; + } // make_title + + + /** + * @brief Creates a tag string combining codec and bitrate information. + * @param codec The station's codec. + * @param bitrate The station's bitrate. + * @return The formatted tag string. + */ + private static string make_tag (string codec, int bitrate) { + var tag = codec; + if (bitrate > 0) + { + tag = tag + " " + bitrate.to_string() + "k"; + } + + return tag; + } // make_tag + + + /** + * @brief Creates a description string based on the station's location. + * @param location The station's location. + * @return The formatted description string. + */ + private static string make_description (string? location) + { + if (location != null && location.length > 0) + return _(location); + else + return location; + } // make_description +} // class StationButton diff --git a/src/Widgets/StationContextMenu.vala b/src/Widgets/StationContextMenu.vala index f78979e..adb6249 100644 --- a/src/Widgets/StationContextMenu.vala +++ b/src/Widgets/StationContextMenu.vala @@ -1,8 +1,12 @@ -/* +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * + * @file StationContextMenu.vala */ - + /** * @class StationContextMenu * @brief A context menu for radio stations. @@ -13,20 +17,23 @@ * * @extends Gtk.Menu */ -public class Tuner.StationContextMenu : Gtk.Menu { +public class Tuner.StationContextMenu : Gtk.Menu +{ /** * @property station * @brief The radio station associated with this context menu. */ - public Model.Station station { get; construct; } + public StationButton station_button { get; construct; } + private Model.Station _station; /** * @brief Constructs a new StationContextMenu. - * @param station The radio station for which this menu is created. + * @param station_button The radio station for which this menu is created. */ - public StationContextMenu (Model.Station station) { + public StationContextMenu (StationButton station_button) + { Object ( - station: station + station_button: station_button ); } @@ -34,73 +41,126 @@ public class Tuner.StationContextMenu : Gtk.Menu { * @brief Initializes the menu items and sets up event handlers. */ construct { - var label = new Gtk.MenuItem.with_label (this.station.title); - label.sensitive = false; - this.append (label); - var label2 = new Gtk.MenuItem.with_label (this.station.location); - label2.sensitive = false; - this.append (label2); + _station = station_button.station; + + // Name + var name = new Gtk.MenuItem.with_label (station_button.station.name); + name.sensitive = false; + append (name); + + + // Country + if ( _station.countrycode != null && _station.countrycode.length > 0 ) + { + var sb = new StringBuilder (_station.countrycode); + if ( _station.state != null && _station.state.length > 0 ) + sb.append (" - ").append (_station.state); + + // Language + if ( station_button.station.language != null && station_button.station.language.length > 0 ) + { + sb.append ("\t[").append (station_button.station.language).append ("]"); + } + + var info = new Gtk.MenuItem.with_label (sb.str); + info.sensitive = false; + append (info); + } - this.append (new Gtk.SeparatorMenuItem ()); + append (new Gtk.SeparatorMenuItem ()); - if (this.station.homepage != null && this.station.homepage.length > 0) { - var website_label = new Gtk.MenuItem.with_label (_("Visit Website")); - this.append (website_label); - website_label.activate.connect (on_website_handler); - } - var label3 = new Gtk.MenuItem.with_label (_("Copy Stream-URL to clipboard")); - label3.sensitive = true; - this.append (label3); - label3.activate.connect (on_streamurl_handler); + // ---------------------------------------------- - this.append (new Gtk.SeparatorMenuItem ()); - var m1 = new Gtk.MenuItem (); - set_star_context (m1); - m1.activate.connect (on_star_handler); - this.append (m1); + var popularity = new Gtk.MenuItem.with_label (station_button.station.popularity ()); + popularity.sensitive = false; + append (popularity); - this.station.notify["starred"].connect ( (sender, property) => { - set_star_context (m1); + append (new Gtk.SeparatorMenuItem ()); + + + // ---------------------------------------------- + + + var not_up_to_date = new Gtk.MenuItem.with_label (_("Station info updated Online - View changes")); + var make_up_to_date = new Gtk.MenuItem.with_label (_("Update Station from Online")); + make_up_to_date.activate.connect (() => + { + station_button.update(true); + remove(not_up_to_date); + remove(make_up_to_date); }); - } - /** - * @brief Handles the star/unstar action. - */ - private void on_star_handler () { - station.toggle_starred (); + var website = new Gtk.MenuItem.with_label (_("Visit Website")); + if (_station.homepage != null && _station.homepage.length > 0) + { + website.activate.connect (on_website_handler); + } + + var stream_url = new Gtk.MenuItem.with_label (_("Copy Stream-URL to clipboard")); + stream_url.sensitive = true; + stream_url.activate.connect (on_streamurl_handler); + + + // Star + var star = new Gtk.MenuItem (); + set_context_star (star); + star.activate.connect (() => + { + _station.toggle_starred (); + set_context_star (star); + }); // Context starred action + + + if (!_station.is_up_to_date) + { + not_up_to_date.tooltip_text = _station.up_to_date_difference; + if (!_station.is_in_index) + make_up_to_date.sensitive = false; + append (not_up_to_date); + append (make_up_to_date); + append (new Gtk.SeparatorMenuItem ()); + } + append (website); + append (stream_url); + append (star); } + /** - * @brief Handles the action to open the station's website. - */ - private void on_website_handler () { - try { - Gtk.show_uri_on_window (Application._instance.window, station.homepage, Gdk.CURRENT_TIME); - } catch (Error e) { - warning (@"Unable to open website: $(e.message)"); - } + * @brief Handles the action to open the station's website. + */ + private void on_website_handler () + { + try + { + Gtk.show_uri_on_window (app().window, _station.homepage, Gdk.CURRENT_TIME); + } catch (Error e) + { + warning (@"Unable to open website: $(e.message)"); + } + } - } - /** - * @brief Handles copying the stream URL to clipboard. - */ - private void on_streamurl_handler () { - Gdk.Display display = Gdk.Display.get_default (); + /** + * @brief Handles copying the stream URL to clipboard. UrlResolved is the stream url, url can be playlists + */ + private void on_streamurl_handler () + { + Gdk.Display display = Gdk.Display.get_default (); Gtk.Clipboard clipboard = Gtk.Clipboard.get_for_display (display, Gdk.SELECTION_CLIPBOARD); - clipboard.set_text (this.station.url, -1); + clipboard.set_text (( _station.urlResolved == null || _station.urlResolved == "" ) ? _station.url : _station.urlResolved, -1); } /** - * @brief Updates the star menu item's label based on the station's starred status. - * @param item The menu item to update. - */ - private void set_star_context (Gtk.MenuItem item) { - item.label = station.starred ? Application.UNSTAR_CHAR + _("Unstar this station") : Application.STAR_CHAR + _("Star this station"); - } + * @brief Updates the star menu item's label based on the station's starred status. + * @param item The menu item to update. + */ + private void set_context_star (Gtk.MenuItem item) + { + item.label = _station.starred ? Application.UNSTAR_CHAR + _("Unstar this station") : Application.STAR_CHAR + _("Star this station"); + } } diff --git a/src/Widgets/StationList.vala b/src/Widgets/StationList.vala deleted file mode 100644 index 104aa15..0000000 --- a/src/Widgets/StationList.vala +++ /dev/null @@ -1,119 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -using Gee; - -/** - * @class StationList - * @brief A widget for displaying and managing a list of radio stations. - * - * The StationList class extends AbstractContentList to provide a specialized - * widget for displaying radio stations. It manages station selection, favorites, - * and provides signals for various state changes. - * - * @extends AbstractContentList - */ -public class Tuner.StationList : AbstractContentList { - - /** - * @signal selection_changed - * @brief Emitted when a station is selected. - * @param station The selected Model.Station. - */ - public signal void selection_changed (Model.Station station); - - /** - * @signal station_count_changed - * @brief Emitted when the number of stations changes. - * @param count The new number of stations. - */ - public signal void station_count_changed (uint count); - - /** - * @signal favourites_changed - * @brief Emitted when a station's favorite status changes. - */ - public signal void favourites_changed (); - - /** - * @property selected_station - * @brief The currently selected station. - */ - public Model.Station selected_station; - - /** - * @property stations - * @brief The list of stations to display. - * - * When set, this property clears the existing list and populates it with - * the new stations. It also sets up signal connections for each station. - */ - public ArrayList stations { - set construct { - clear (); - if (value == null) return; - - foreach (var s in value) { - s.notify["starred"].connect ( () => { - favourites_changed (); - }); - var box = new StationBox (s); - box.clicked.connect (() => { - selection_changed (box.station); - selected_station = box.station; - }); - add (box); - } - item_count = value.size; - } - } - - /** - * @brief Constructs a new StationList instance. - * - * Initializes the StationList with default properties for layout and behavior. - */ - public StationList () { - Object ( - homogeneous: false, - min_children_per_line: 1, - max_children_per_line: 3, - column_spacing: 5, - row_spacing: 5, - border_width: 20, - valign: Gtk.Align.START, - selection_mode: Gtk.SelectionMode.NONE - ); - } - - /** - * @brief Constructs a new StationList instance with a predefined list of stations. - * @param stations The ArrayList of Model.Station objects to populate the list. - */ - public StationList.with_stations (Gee.ArrayList stations) { - this (); - this.stations = stations; - } - - /** - * @brief Clears all stations from the list. - * - * This method removes and destroys all child widgets from the StationList. - */ - public void clear () { - var childs = get_children(); - foreach (var c in childs) { - c.destroy(); - } - } - - /** - * @property item_count - * @brief The number of stations in the list. - * - * This property implements the abstract property from AbstractContentList. - */ - public override uint item_count { get; set; } -} diff --git a/src/Widgets/Theme.vala b/src/Widgets/Theme.vala deleted file mode 100644 index 0f9d5a3..0000000 --- a/src/Widgets/Theme.vala +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - -namespace Tuner { - -public class Theme : Gtk.Widget -{ - private static GLib.Once _instance; - - public static unowned Theme instance () - { - return _instance.once (() => { return new Theme (); }); - } - - public bool delet_is_theme_dark() - { - var settings = Gtk.Settings.get_default(); - var theme = Environment.get_variable("GTK_THEME"); - - var dark = settings.gtk_application_prefer_dark_theme || (theme != null && theme.has_suffix(":dark")); - - if (!dark) { - var stylecontext = get_style_context(); - Gdk.RGBA rgba; - var background_set = stylecontext.lookup_color("theme_bg_color", out rgba); - - if (background_set && rgba.red + rgba.green + rgba.blue < 1.0) - { - dark = true; - } - } - - return dark; - } -} - -} \ No newline at end of file diff --git a/src/Widgets/Window.vala b/src/Widgets/Window.vala index 7bd7d3a..e0f95b7 100644 --- a/src/Widgets/Window.vala +++ b/src/Widgets/Window.vala @@ -1,434 +1,260 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ /** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * * @file Window.vala + * * @brief Defines the main application window for the Tuner application. * * This file contains the Window class, which is responsible for creating and - * managing the main application window. It handles the layout, user interface - * elements, and interactions with other components of the application. + * managing the main application window. It handles the major layout, user interface + * elements, and interactions with other non-display components of the application. * * The Window class inherits from Gtk.ApplicationWindow and implements various - * features such as a header bar, source list, content stack, and player controls. + * features such as a header bar, main display and player controls. * It also manages application settings and handles user actions like playback * control, station selection, and theme adjustments. * * @see Tuner.Application * @see Tuner.PlayerController * @see Tuner.DirectoryController + * @see Tuner.HeaderBar + * @see Tuner.Display */ using Gee; +using Granite.Widgets; + /** - Window -*/ -public class Tuner.Window : Gtk.ApplicationWindow { + * The main application window for the Tuner app. + * + * This class extends Gtk.ApplicationWindow and serves as the primary container + * for all other widgets and functionality in the Tuner application. + */ +public class Tuner.Window : Gtk.ApplicationWindow +{ /* Public */ - public const string WINDOW_NAME = "Tuner"; - public const string ACTION_PREFIX = "win."; - public const string ACTION_PAUSE = "action_pause"; - public const string ACTION_QUIT = "action_quit"; - public const string ACTION_HIDE = "action_hide"; - public const string ACTION_ABOUT = "action_about"; - public const string ACTION_DISABLE_TRACKING = "action_disable_tracking"; - public const string ACTION_ENABLE_AUTOPLAY = "action_enable_autoplay"; + public const string WINDOW_NAME = "Tuner"; + public const string ACTION_PREFIX = "win."; + public const string ACTION_PAUSE = "action_pause"; + public const string ACTION_QUIT = "action_quit"; + public const string ACTION_HIDE = "action_hide"; + public const string ACTION_ABOUT = "action_about"; + public const string ACTION_DISABLE_TRACKING = "action_disable_tracking"; + public const string ACTION_ENABLE_AUTOPLAY = "action_enable_autoplay"; + public const string ACTION_START_ON_STARRED = "action_starred_start"; + public const string ACTION_STREAM_INFO = "action_stream_info"; + public const string ACTION_STREAM_INFO_FAST = "action_stream_info_fast"; - public GLib.Settings settings { get; construct; } - public Gtk.Stack stack { get; set; } - public PlayerController player { get; construct; } - public Model.StationStore store { get; construct; } + public Settings settings { get; construct; } + public PlayerController player_ctrl { get; construct; } + public DirectoryController directory { get; construct; } + + public bool active { get; private set; } // Window is active + public int width { get; private set; } + public int height { get; private set; } /* Private */ - private const ActionEntry[] ACTION_ENTRIES = { - { ACTION_PAUSE, on_toggle_playback }, - { ACTION_QUIT , on_action_quit }, - { ACTION_ABOUT, on_action_about }, - { ACTION_DISABLE_TRACKING, on_action_disable_tracking, null, "false" }, - { ACTION_ENABLE_AUTOPLAY, on_action_enable_autoplay, null, "false" } - }; - - private DirectoryController _directory; - private HeaderBar _headerbar; - private Granite.Widgets.SourceList source_list; - - private signal void refresh_favourites_sig (); - - - /* Construct Static*/ - static construct { - var provider = new Gtk.CssProvider (); - provider.load_from_resource ("com/github/louis77/tuner/Application.css"); - Gtk.StyleContext.add_provider_for_screen ( - Gdk.Screen.get_default (), - provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); - } + private const string CSS = "io/github/louis77/tuner/Application.css"; + private const string NOTIFICATION_PLAYING_BACKGROUND = _("Playing in background"); + private const string NOTIFICATION_CLICK_RESUME = _("Click here to resume window. To quit Tuner, pause playback and close the window."); + private const string NOTIFICATION_APP_RESUME_WINDOW = "app.resume-window"; + private const string NOTIFICATION_APP_PLAYING_CONTINUE = "continue-playing"; + + private const int RANDOM_CATEGORIES = 5; + + private const int GEOMETRY_MIN_HEIGHT = 440; + private const int GEOMETRY_MIN_WIDTH = 600; + + private const ActionEntry[] ACTION_ENTRIES = { + { ACTION_PAUSE, on_toggle_playback }, + { ACTION_QUIT, on_action_quit }, + { ACTION_ABOUT, on_action_about }, + { ACTION_DISABLE_TRACKING, on_action_disable_tracking, null, "false" }, + { ACTION_ENABLE_AUTOPLAY, on_action_enable_autoplay, null, "false" }, + { ACTION_START_ON_STARRED, on_action_start_on_starred, null, "false" }, + { ACTION_STREAM_INFO, on_action_stream_info, null, "true" }, + { ACTION_STREAM_INFO_FAST, on_action_stream_info_fast, null, "false" }, + }; + + /* + Assets + */ + + private HeaderBar _headerbar; + private Display _display; + private bool _start_on_starred = false; + + private signal void refresh_saved_searches_sig (bool add, string search_text); + + + // /* Construct Static*/ + // static construct { + // var provider = new Gtk.CssProvider (); + // provider.load_from_resource (CSS); + // Gtk.StyleContext.add_provider_for_screen ( + // Gdk.Screen.get_default (), + // provider, + // Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + // ); + // } // static construct /** * @brief Constructs a new Window instance. + * * @param app The Application instance. * @param player The PlayerController instance. */ - public Window (Application app, PlayerController player) { + public Window (Application app, PlayerController player, Settings settings, DirectoryController directory ) + { Object ( application: app, - player: player, - settings: Application.instance.settings + player_ctrl: player, + settings: settings, + directory: directory ); - application.set_accels_for_action (ACTION_PREFIX + ACTION_PAUSE, {"5"}); - application.set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"q"}); - application.set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"w"}); - } - - - /* Construct */ - construct { - this.set_icon_name("com.github.louis77.tuner"); - this.size_allocate.connect(on_window_resize); - - _headerbar = new HeaderBar (); - set_titlebar (_headerbar); - set_title (WINDOW_NAME); - - player.state_changed.connect (handleplayer_state_changed); - player.station_changed.connect (_headerbar.update_from_station); - player.title_changed.connect ((title) => { - _headerbar.subtitle = title; - }); - player.volume_changed.connect ((volume) => { - _headerbar.volume_button.value = volume; - }); - _headerbar.volume_button.value_changed.connect ((value) => { - player.volume = value; - }); - - adjust_theme(); // TODO Theme management needs research in flatpak as nonfunctional - settings.changed.connect( (key) => { - if (key == "theme-mode") { - debug("theme-mode changed"); - adjust_theme(); - } - }); - - var granite_settings = Granite.Settings.get_default (); - granite_settings.notify.connect( (key) => { - debug("theme-mode changed"); - adjust_theme(); - }); + add_widgets(); + check_online_status(); - add_action_entries (ACTION_ENTRIES, this); + if ( settings.start_on_starred ) choose_starred_stations(); // Start on starred - window_position = Gtk.WindowPosition.CENTER; - set_default_size (900, 680); - change_action_state (ACTION_DISABLE_TRACKING, settings.get_boolean ("do-not-track")); - change_action_state (ACTION_ENABLE_AUTOPLAY, settings.get_boolean ("auto-play")); - move (settings.get_int ("pos-x"), settings.get_int ("pos-y")); + show_all (); - set_geometry_hints (null, Gdk.Geometry() {min_height = 440, min_width = 600}, Gdk.WindowHints.MIN_SIZE); - resize (settings.get_int ("window-width"), settings.get_int ("window-height")); + application.set_accels_for_action (ACTION_PREFIX + ACTION_PAUSE, {"5"}); + application.set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"q"}); + application.set_accels_for_action (ACTION_PREFIX + ACTION_QUIT, {"w"}); + } // Window + + + /* + Construct + */ + construct + { + set_icon_name(Application.APP_ID); + add_action_entries (ACTION_ENTRIES, this); + set_title (WINDOW_NAME); + window_position = Gtk.WindowPosition.CENTER; + set_geometry_hints (null, Gdk.Geometry() { + min_height = GEOMETRY_MIN_HEIGHT, min_width = GEOMETRY_MIN_WIDTH + }, Gdk.WindowHints.MIN_SIZE); + change_action_state (ACTION_DISABLE_TRACKING, settings.do_not_vote); + change_action_state (ACTION_ENABLE_AUTOPLAY, settings.auto_play); + change_action_state (ACTION_START_ON_STARRED, settings.start_on_starred); + change_action_state (ACTION_STREAM_INFO, settings.stream_info); + change_action_state (ACTION_STREAM_INFO_FAST, settings.stream_info_fast); + + + /* + Setup + */ delete_event.connect (e => { return before_destroy (); }); - var stack = new Gtk.Stack (); - stack.transition_type = Gtk.StackTransitionType.CROSSFADE; - var favorites_file = Path.build_filename (Application.instance.data_dir, "favorites.json"); - store = new Model.StationStore (favorites_file); - _directory = new DirectoryController (store); + /* + Online checks & behavior - var primary_box = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); - - - var selections_category = new Granite.Widgets.SourceList.ExpandableItem (_("Selections")); - selections_category.collapsible = false; - selections_category.expanded = true; - - var searched_category = new Granite.Widgets.SourceList.ExpandableItem (_("Library")); - searched_category.collapsible = false; - searched_category.expanded = true; - - var genres_category = new Granite.Widgets.SourceList.ExpandableItem (_("Genres")); - genres_category.collapsible = true; - genres_category.expanded = true; - - source_list = new Granite.Widgets.SourceList (); - - // Discover Box - var item1 = new Granite.Widgets.SourceList.Item (_("Discover")); - item1.icon = new ThemedIcon ("face-smile"); - selections_category.add (item1); - - var c1 = create_content_box ("discover", item1, - _("Discover Stations"), "media-playlist-shuffle-symbolic", - _("Discover more stations"), - stack, source_list); - var s1 = _directory.load_random_stations(20); - c1.realize.connect (() => { - try { - var slist = new StationList.with_stations (s1.next ()); - slist.selection_changed.connect (handle_station_click); - slist.favourites_changed.connect (handle_favourites_changed); - c1.content = slist; - } catch (SourceError e) { - c1.show_alert (); - } + Keep in mind that network availability is noisy + */ + app().notify["is-online"].connect(() => + { + check_online_status(); }); - c1.action_activated_sig.connect (() => { - try { - var slist = new StationList.with_stations (s1.next ()); - slist.selection_changed.connect (handle_station_click); - slist.favourites_changed.connect (handle_favourites_changed); - c1.content = slist; - } catch (SourceError e) { - c1.show_alert (); - } - }); - - // Trending Box - var item2 = new Granite.Widgets.SourceList.Item (_("Trending")); - item2.icon = new ThemedIcon ("playlist-queue"); - selections_category.add (item2); - - var c2 = create_content_box ("trending", item2, - _("Trending in the last 24 hours"), null, null, - stack, source_list); - var s2 = _directory.load_trending_stations(40); - c2.realize.connect (() => { - try { - var slist = new StationList.with_stations (s2.next ()); - slist.selection_changed.connect (handle_station_click); - slist.favourites_changed.connect (handle_favourites_changed); - c2.content = slist; - } catch (SourceError e) { - c2.show_alert (); - } + } // construct - }); - - // Popular Box - var item3 = new Granite.Widgets.SourceList.Item (_("Popular")); - item3.icon = new ThemedIcon ("playlist-similar"); - selections_category.add (item3); - - var c3 = create_content_box ("popular", item3, - _("Most-listened over 24 hours"), null, null, - stack, source_list); - var s3 = _directory.load_popular_stations(40); - c3.realize.connect (() => { - try { - var slist = new StationList.with_stations (s3.next ()); - slist.selection_changed.connect (handle_station_click); - slist.favourites_changed.connect (handle_favourites_changed); - c3.content = slist; - } catch (SourceError e) { - c3.show_alert (); - } - }); - // Country-specific stations list - var item4 = new Granite.Widgets.SourceList.Item (_("Your Country")); - item4.icon = new ThemedIcon ("emblem-web"); - ContentBox c_country; - c_country = create_content_box ("my-country", item4, - _("Your Country"), null, null, - stack, source_list, true); - var c_slist = new StationList (); - c_slist.selection_changed.connect (handle_station_click); - c_slist.favourites_changed.connect (handle_favourites_changed); - - // Favourites Box - var item5 = new Granite.Widgets.SourceList.Item (_("Starred by You")); - item5.icon = new ThemedIcon ("starred"); - searched_category.add (item5); - var c4 = create_content_box ("starred", item5, - _("Starred by You"), null, null, - stack, source_list, true); - - var slist = new StationList.with_stations (_directory.get_stored ()); - slist.selection_changed.connect (handle_station_click); - slist.favourites_changed.connect (handle_favourites_changed); - c4.content = slist; - - // Search Results Box - var item6 = new Granite.Widgets.SourceList.Item (_("Recent Search")); - item6.icon = new ThemedIcon ("folder-saved-search"); - searched_category.add (item6); - var c5 = create_content_box ("searched", item6, - _("Search"), null, null, - stack, source_list, true); - - // Genre Boxes - foreach (var genre in Model.genres ()) { - var item8 = new Granite.Widgets.SourceList.Item (_(genre.name)); - item8.icon = new ThemedIcon ("playlist-symbolic"); - genres_category.add (item8); - var cb = create_content_box (genre.name, item8, - genre.name, null, null, stack, source_list); - var tags = new ArrayList.wrap (genre.tags); - var ds = _directory.load_by_tags (tags); - cb.realize.connect (() => { - try { - var slist1 = new StationList.with_stations (ds.next ()); - slist1.selection_changed.connect (handle_station_click); - slist1.favourites_changed.connect (handle_favourites_changed); - cb.content = slist1; - } catch (SourceError e) { - cb.show_alert (); - } - }); - } - - _headerbar.star_clicked_sig.connect ( (starred) => { - player.station.toggle_starred (); - }); - - refresh_favourites_sig.connect ( () => { - var _slist = new StationList.with_stations (_directory.get_stored ()); - _slist.selection_changed.connect (handle_station_click); - _slist.favourites_changed.connect (handle_favourites_changed); - c4.content = _slist; - }); + /** + * Selects and displays the user's starred (favorite) radio stations. + * + * This method handles the process of showing the user's favorite stations + * in the station list view. It filters and displays only the stations that + * have been marked as favorites by the user. + */ + public void choose_starred_stations() + { + _start_on_starred = true; + if (_active) + _display.choose_starred_stations(); + } // choose_star - source_list.root.add (selections_category); - source_list.root.add (searched_category); - source_list.root.add (genres_category); - source_list.ellipsize_mode = Pango.EllipsizeMode.NONE; - source_list.selected = source_list.get_first_child (selections_category); - source_list.item_selected.connect ((item) => { - var selected_item = item.get_data ("stack_child"); - stack.visible_child_name = selected_item; + /** + Add widgets after Window creation + */ + private void add_widgets() + { + /* + Headerbar hookups + */ + _headerbar = new HeaderBar (this); + + _headerbar.search_has_focus_sig.connect (() => + // Show searched stack when cursor hits search text area + { + _display.search_focused_sig( ); }); - _headerbar.searched_for_sig.connect ( (text) => { - if (text.length > 0) { - load_search_stations.begin(text, c5); - } + _headerbar.searching_for_sig.connect ( (text) => + // process the searched text, stripping it, and sensitizing the save + // search star depending on if the search is already saved + { + _display.searched_for_sig( text); }); - _headerbar.search_focused_sig.connect (() => { - stack.visible_child_name = "searched"; - }); + set_titlebar (_headerbar); - primary_box.pack1 (source_list, false, false); - primary_box.pack2 (stack, true, false); - add (primary_box); - show_all (); + /* + Display + */ + _display = new Display(directory); + _display.station_clicked_sig.connect (handle_play_station); // Station clicked -> change station + add (_display); // Auto-play - if (settings.get_boolean("auto-play")) { - debug (@"Auto-play enabled"); - var last_played_station = settings.get_string("last-played-station"); - debug (@"Last played station is: $last_played_station"); - - var source = _directory.load_station_uuid (last_played_station); - - try { - foreach (var station in source.next ()) { - handle_station_click(station); + if (_settings.auto_play) + { + _directory.load (); + var source = _directory.load_station_uuid (_settings.last_played_station); + + try + { + foreach (var station in source.next_page ()) + { + handle_play_station(station); break; } - } catch (SourceError e) { + } catch (SourceError e) + { warning ("Error while trying to autoplay, aborting..."); } - - } - } - - - /** - * @brief Handles window resizing. - * @param self The widget being resized. - * @param allocation The new allocation for the widget. - */ - private void on_window_resize (Gtk.Widget self, Gtk.Allocation allocation) { - int width = allocation.width; - int height = allocation.height; - - debug (@"Window resized: w$(width) h$(height)"); - } - - - /** - * @brief Creates a new ContentBox and adds it to the stack. - * @param name The name of the content box. - * @param item The SourceList item associated with the content box. - * @param full_title The full title of the content box. - * @param action_icon_name The name of the action icon (or null if none). - * @param action_tooltip_text The tooltip text for the action (or null if none). - * @param stack The Gtk.Stack to add the content box to. - * @param source_list The SourceList to update when the content box is selected. - * @param enable_count Whether to enable item counting for the content box. - * @return The created ContentBox. - */ - private ContentBox create_content_box ( - string name, - Granite.Widgets.SourceList.Item item, - string full_title, - string? action_icon_name, - string? action_tooltip_text, - Gtk.Stack stack, - Granite.Widgets.SourceList source_list, - bool enable_count = false) - { - item.set_data ("stack_child", name); - var c = new ContentBox ( - null, - full_title, - null, - action_icon_name, - action_tooltip_text - ); - c.map.connect (() => { - source_list.selected = item; - }); - if (enable_count) { - c.content_changed_sig.connect (() => { - if (c.content == null) return; - var count = c.content.item_count; - item.badge = @"$count"; - }); } - stack.add_named (c, name); + } // add_widgets - return c; - } + /* -------------------------------------------------------- + + Methods - /** - * @brief Adjusts the application theme based on user settings. - */ - private static void adjust_theme() { - var theme = Application.instance.settings.get_string("theme-mode"); - info(@"current theme: $theme"); - - var gtk_settings = Gtk.Settings.get_default (); - var granite_settings = Granite.Settings.get_default (); - if (theme != "system") { - gtk_settings.gtk_application_prefer_dark_theme = (theme == "dark"); - } else { - gtk_settings.gtk_application_prefer_dark_theme = (granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK); - } - } - + ---------------------------------------------------------- + */ // ---------------------------------------------------------------------- // - // Handlers + // Actions // // ---------------------------------------------------------------------- @@ -436,28 +262,30 @@ public class Tuner.Window : Gtk.ApplicationWindow { /** * @brief Handles the quit action. */ - private void on_action_quit () { + private void on_action_quit () + { close (); - } + } // on_action_quit /** * @brief Handles the about action. */ - private void on_action_about () { + private void on_action_about () + { var dialog = new AboutDialog (this); dialog.present (); - } - + } // on_action_about /** * @brief Toggles playback state. */ - public void on_toggle_playback() { - info ("Stop Playback requested"); - player.play_pause (); - } + public void on_toggle_playback() + { + info (_("Stop Playback requested")); + player_ctrl.play_pause (); + } // on_toggle_playback /** @@ -465,12 +293,12 @@ public class Tuner.Window : Gtk.ApplicationWindow { * @param action The SimpleAction that triggered this method. * @param parameter The parameter passed with the action (unused). */ - public void on_action_disable_tracking (SimpleAction action, Variant? parameter) { - var new_state = !settings.get_boolean ("do-not-track"); - action.set_state (new_state); - settings.set_boolean ("do-not-track", new_state); - debug (@"on_action_disable_tracking: $new_state"); - } + public void on_action_disable_tracking (SimpleAction action, Variant? parameter) + { + settings.do_not_vote = !settings.do_not_vote; + action.set_state (settings.do_not_vote); + debug (@"on_action_disable_tracking: $(settings.do_not_vote)"); + } // on_action_disable_tracking /** @@ -478,143 +306,115 @@ public class Tuner.Window : Gtk.ApplicationWindow { * @param action The SimpleAction that triggered this method. * @param parameter The parameter passed with the action (unused). */ - public void on_action_enable_autoplay (SimpleAction action, Variant? parameter) { - var new_state = !settings.get_boolean ("auto-play"); - action.set_state (new_state); - settings.set_boolean ("auto-play", new_state); - debug (@"on_action_enable_autoplay: $new_state"); - } - - // ---------------------------------------------------------------------- - // - // Handlers - // - // ---------------------------------------------------------------------- + public void on_action_enable_autoplay (SimpleAction action, Variant? parameter) + { + settings.auto_play = !settings.auto_play; + action.set_state (settings.auto_play); + debug (@"on_action_enable_autoplay: $(settings.auto_play)"); + } // on_action_enable_autoplay /** - * @brief Handles a station selection. - * @param station The selected station. + * @brief Handles the enable autoplay action. + * @param action The SimpleAction that triggered this method. + * @param parameter The parameter passed with the action (unused). */ - public void handle_station_click (Tuner.Model.Station station) { - debug (@"handle station click for $(station.title)"); - _directory.count_station_click (station); - player.station = station; + public void on_action_start_on_starred (SimpleAction action, Variant? parameter) + { + settings.start_on_starred = !settings.start_on_starred; + action.set_state (settings.start_on_starred); + debug (@"on_action_enable_autoplay: $(settings.auto_play)"); + } // on_action_enable_autoplay - debug (@"Storing last played station: $(station.id)"); - settings.set_string("last-played-station", station.id); - set_title (WINDOW_NAME+": "+station.title); - } + public void on_action_stream_info (SimpleAction action, Variant? parameter) + { + settings.stream_info = !settings.stream_info; + action.set_state (settings.stream_info); + _headerbar.stream_info (action.get_state ().get_boolean ()); + } // on_action_enable_stream_info - /** - * @brief Handles changes to the favorites list. - */ - public void handle_favourites_changed () { - refresh_favourites_sig (); - } - /** - * @brief Handles player state changes. - * @param state The new player state. - */ - public void handleplayer_state_changed (Gst.PlayerState state) { - switch (state) { - case Gst.PlayerState.BUFFERING: - debug ("player state changed to Buffering"); - Gdk.threads_add_idle (() => { - _headerbar.set_playstate (HeaderBar.PlayState.PAUSE_ACTIVE); - return false; - }); - break;; - case Gst.PlayerState.PAUSED: - debug ("player state changed to Paused"); - Gdk.threads_add_idle (() => { - if (player.can_play()) { - _headerbar.set_playstate (HeaderBar.PlayState.PLAY_ACTIVE); - } else { - _headerbar.set_playstate (HeaderBar.PlayState.PLAY_INACTIVE); - } - return false; - }); - break;; - case Gst.PlayerState.PLAYING: - debug ("player state changed to Playing"); - Gdk.threads_add_idle (() => { - _headerbar.set_playstate (HeaderBar.PlayState.PAUSE_ACTIVE); - return false; - }); - break;; - case Gst.PlayerState.STOPPED: - debug ("player state changed to Stopped"); - Gdk.threads_add_idle (() => { - if (player.can_play()) { - _headerbar.set_playstate (HeaderBar.PlayState.PLAY_ACTIVE); - } else { - _headerbar.set_playstate (HeaderBar.PlayState.PLAY_INACTIVE); - } - return false; - }); - break; - } + public void on_action_stream_info_fast (SimpleAction action, Variant? parameter) + { + settings.stream_info_fast = !settings.stream_info_fast; + action.set_state (settings.stream_info_fast); + _headerbar.stream_info_fast (action.get_state ().get_boolean ()); + } // on_action_stream_info_fast - return; - } - /** - * @brief Performs cleanup actions before the window is destroyed. - * @return true if the window should be hidden instead of destroyed, false otherwise. - */ - public bool before_destroy () { - int width, height, x, y; + // ---------------------------------------------------------------------- + // + // Handlers + // + // ---------------------------------------------------------------------- - get_size (out width, out height); - get_position (out x, out y); - settings.set_int ("pos-x", x); - settings.set_int ("pos-y", y); - settings.set_int ("window-height", height); - settings.set_int ("window-width", width); + /** + * @brief Handles a station selection and plays the station + * @param station The selected station. + */ + public void handle_play_station (Model.Station station) + { + if ( app().is_offline || !_headerbar.update_playing_station(station) ) + return; // Online and not already changing station - if (player.current_state == Gst.PlayerState.PLAYING) { + player_ctrl.station = station; + _settings.last_played_station = station.stationuuid; + _directory.count_station_click (station); + + set_title (WINDOW_NAME+": "+station.name); + } // handle_station_click + + + // ---------------------------------------------------------------------- + // + // State management + // + // ---------------------------------------------------------------------- + + /** + * @brief Performs cleanup actions before the window is destroyed. + * @return true if the window should be hidden instead of destroyed, false otherwise. + */ + public bool before_destroy () + { + get_size (out _width, out _height); // Echo ending dimensions so Settings can pick them up + _settings.save (); + + if (player_ctrl.player_state == PlayerController.Is.PLAYING) { hide_on_delete(); - var notification = new GLib.Notification("Playing in background"); - notification.set_body("Click here to resume window. To quit Tuner, pause playback and close the window."); - notification.set_default_action("app.resume-window"); - Application.instance.send_notification("continue-playing", notification); + var notification = new GLib.Notification(NOTIFICATION_PLAYING_BACKGROUND); + notification.set_body(NOTIFICATION_CLICK_RESUME); + notification.set_default_action(NOTIFICATION_APP_RESUME_WINDOW); + app().send_notification(NOTIFICATION_APP_PLAYING_CONTINUE, notification); return true; } return false; - } - - /** - * @brief Loads search stations based on the provided text and updates the content box. - * Async since 1.5.5 so that UI is responsive during long searches - * @param searchText The text to search for stations. - * @param contentBox The ContentBox to update with the search results. - */ - private async void load_search_stations(string searchText, ContentBox contentBox) { - - debug(@"Searching for: $(searchText)"); // FIXME warnings to debugs - var station_source = _directory.load_search_stations(searchText, 100); - debug(@"Search done"); - - try { - var stations = station_source.next(); - debug(@"Search Next done"); - if (stations == null || stations.size == 0) { - contentBox.show_nothing_found(); - } else { - debug(@"Search found $(stations.size) stations"); - var _slist = new StationList.with_stations(stations); - _slist.selection_changed.connect(handle_station_click); - _slist.favourites_changed.connect(handle_favourites_changed); - contentBox.content = _slist; - } - } catch (SourceError e) { - contentBox.show_alert(); - } - } -} + } // before_destroy + + + /** + * @brief Checks changes in online state and updates the app accordingly + * + */ + private void check_online_status() + { + if (active && app().is_offline) + /* Present Offline look */ + { + this.accept_focus = false; + active = false; + } + + if (!active && app().is_online) + // Online but not active + { + this.accept_focus = true; + active = true; + } + _display.update_state (active, _start_on_starred ); + } // check_online_status +} // Window diff --git a/src/Widgets/base/CyclingRevealLabel.vala b/src/Widgets/base/CyclingRevealLabel.vala new file mode 100644 index 0000000..67d85ac --- /dev/null +++ b/src/Widgets/base/CyclingRevealLabel.vala @@ -0,0 +1,342 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file CyclingRevealLabel.vala + */ + + using Gee; + using Gtk; + using GLib; + + + /** + * @class CyclingRevealLabel + * @brief A custom widget that reveals a cycling label with animation. + * + * This class extends Tuner.RevealLabel to add cycling through of label text + * and with damping between the different labels so grids do not bounce too much + * + * @extends Tuner.RevealLabel + */ +public class Tuner.CyclingRevealLabel : RevealLabel { + + private const int SUBTITLE_MIN_DISPLAY_SECONDS = 3; + private const int LABEL_WIDTH_MIN = 100; + private const int LABEL_RESIZE_BUFFER = 10; + // private const int DISPLAY_WIDTH_OFFSET = 761; + // private const int BORDER_WIDTH_OFFSET = 7; + + public bool show_metadata { get; set; } + + public bool metadata_fast_cycle { + get { return _metadata_fast_cycle; } + set { + if ( _metadata_fast_cycle == value ) return; + if (value) + // slow>fast + { + _metadata_fast_cycle = true; + _cycle_phases = _cycle_phases_fast; + } + else + // fast>slow + { + _metadata_fast_cycle = false; + _cycle_phases = _cycle_phases_slow; + } //else + } // set + } // metadata_fast_cycle + + private signal void flourish_complete_sig(); + + private bool _metadata_fast_cycle; + private int _last_parent_width = 0; // tracks window width private int _window_width_previous = 0; // tracks window width + private int _parent_unused_growth = 0; // Tracks the maximum width the label can occupy + private int _min_label_width; // Minimum label width + private int _peak_label_width; // Peak label width + private bool _followed_width_change; // Followed width + // private int _current_label_width; // Current label width + + private uint _label_cycle_id = 0; + private uint _flourish_id = 0; + private int _min_count_down; + private uint16 _display_seconds = 0; // Mix up the cycle phase start point + private uint16[] _cycle_phases_fast = {5,11,17,19,23}; // Fast cycle times - primes so everyone gets a chance + private uint16[] _cycle_phases_slow = {23,37,43,47,53}; // Ttitle, plus four subtitles + private uint16[] _cycle_phases; + + private Gee.Map sublabels = new Gee.HashMap(); + + + public CyclingRevealLabel (Widget follow, int min_label_width, string? str = null) + { + Object(); + + label_child.set_line_wrap(false); + label_child.set_justify(Justification.CENTER); + base.label_child.set_text( str); + + _min_label_width = min_label_width; + + follow.size_allocate.connect((widget, allocation) => + { + if ( _last_parent_width == 0 ) _last_parent_width = allocation.width; + var delta = allocation.width - _last_parent_width; + + if ( delta == 0 ) return; + + if ( delta > 0 ) + // Growing parent + { + _parent_unused_growth += delta; + } + else + // Shrinking parent + { + if ( -delta <= _parent_unused_growth) + // Track the delta back + { + debug(@"Shrink delta: $delta _parent_unused_growth: $_parent_unused_growth"); + _parent_unused_growth += delta; + return; + } + + debug(@"Delta: $delta Peak old: $_peak_label_width new: $(_peak_label_width + delta + _parent_unused_growth) follow: $_last_parent_width"); + _peak_label_width += ((2*delta) + _parent_unused_growth); + _peak_label_width = int.max(_peak_label_width, LABEL_WIDTH_MIN); + _parent_unused_growth = 0; + set_size_request(_peak_label_width, -1); + _followed_width_change = true; + } + _last_parent_width = allocation.width; + }); + + size_allocate.connect((allocation) => + { + if (!_followed_width_change && allocation.width <= ( _peak_label_width + LABEL_RESIZE_BUFFER )) + { + _followed_width_change = false; + return; + } + debug(@"Size Alloc: $(allocation.width) Peak: $(_peak_label_width)"); + _peak_label_width = int.max(_peak_label_width,allocation.width - LABEL_RESIZE_BUFFER); + set_size_request(9*_peak_label_width/10, -1); + }); + + _cycle_phases = _cycle_phases_fast; + } // CyclingRevealLabel + + + public new string label { + get { return get_text(); } + set { set_text ( value ); } + } + + + /** + * @brief gets/Sets the label + * + */ + public new bool set_text( string text ) + { + if ( text == base.get_text() ) return true; + + + // Make the peak width smaller than allocated by the apprent size of the boarder, plus a fudge + // _peak_label_width = int.max(_peak_label_width,get_allocated_width()-BORDER_WIDTH_OFFSET); + + debug(@"CL set text: $(base.get_text()) > $text"); + if ( base.set_text(text) ) + { + + debug(@"CL set text - Success: $text"); + // Measure the natural width of the label with the new text + // int min_width, natural_width; + // get_preferred_width(out min_width, out natural_width); + + // // Update _max_label_width only if the new text exceeds it + // if (natural_width > _max_label_width) { + // _max_label_width = natural_width; + // } + + // // Apply the new width constraints + // update_size( ); + return true; + } + + debug(@"CL set text - Failed: $text"); + return false; + } // label + + + // /** + // */ + // private void update_size(bool flourish = true) + // { + // if ( _flourish_id > 0 ) + // { + // Source.remove(_flourish_id); + // _flourish_id = 0; + // } + + // // var size = int.max(int.min(_max_label_width, _peak_label_width), _min_label_width ); + // // if ( size == _current_label_width ) return; + // // if ( !flourish || size == _min_label_width ) + // // { + // // set_size_request( size, -1); + // // _current_label_width = size; + // // return; + // // } + + // // Flourish + + + // // Idle.add (() => + // // // Initiate the fade out in another thread + // // { + // // _flourish_id = Timeout.add_full(Priority.DEFAULT, 3, () => + // // { + // // if ( _current_label_width >= size ) + // // { + // // _flourish_id = 0; + // // flourish_complete_sig(); + // // return Source.REMOVE; + // // } + + // // _current_label_width++;// += 4; + // // set_size_request( _current_label_width, -1); + + // // return Source.CONTINUE; // Leave timer to be recalled + // // }); + // // debug(@"Flourish target: $size from: $_current_label_width id: $_flourish_id"); + // // return Source.REMOVE; + // // }); + // } // update_size + + + /** + * @brief Adds a sublabel at the given position + * + */ + public void add_sublabel(int position, string? sublabel1, string? sublabel2 = null) + { + if ( position <= 0 || position >= _cycle_phases.length ) return; // Main label not sublabel, or too deep + + if ( sublabel1 == null || sublabel1.strip().length == 0 ) + { + sublabels.unset(position); + } + else + { + var text = (sublabel2 == null || sublabel2.strip() == "" ) ? sublabel1.strip() : sublabel1.strip()+" - "+sublabel2.strip() ; + sublabels.set(position, text); + } + } // add_sublabel + + + /** + * @brief Adds a sublabel at the given position + * + */ + // public void add_stacked_sublabel(int position, string? sublabel1, string? sublabel2 = null) + // { + // if ( position <= 0 || position >= cycle_phases.length ) return; // Main label not sublabel, or too deep + + // if ( sublabel1 == null || sublabel1.strip().length == 0 ) + // { + // sublabels.unset(position); + // } + // else + // { + // sublabels.set(position, (sublabel2 == null || sublabel2.strip() == "" ) ? sublabel1.strip() : sublabel1.strip()+"\n"+sublabel2.strip() ); + // } + // } // add_sublabel + + + /** + * @brief Stops cycling the labels + * + */ + public void stop() + { + if ( _label_cycle_id > 0 ) + { + Source.remove(_label_cycle_id); + _label_cycle_id = 0; + } + + if ( _flourish_id > 0 ) + { + Source.remove(_flourish_id); + _flourish_id = 0; + } + } // stop + + + /** + * @brief Clears the cycling of the subtitles + * + */ + public new void clear() + { + stop(); + base.clear(); + sublabels.clear(); + _peak_label_width = 0; + set_size_request(LABEL_WIDTH_MIN, -1); + } // clear + + + /** + * @brief Cycles the labels + * + */ + public void cycle() + { + stop(); + + uint last_position = 99; + + Idle.add (() => + // Initiate the fade out + { + _label_cycle_id = Timeout.add_seconds_full(Priority.LOW, 1, () => + // New label timer + { + _display_seconds++; + + if ( 0 < _min_count_down-- ) + { + return Source.CONTINUE; + } + + if ( ! child_revealed ) + { + reveal_child = true; + _min_count_down = SUBTITLE_MIN_DISPLAY_SECONDS; + return Source.CONTINUE; // Still processing reveal + } + + foreach ( var position in sublabels.keys) + { + if ( !show_metadata && position != 0 ) break; // Do not show sublabels + + if ( ( _display_seconds % _cycle_phases[position] == 0 ) + && position != last_position + // && sublabels.get(position) != "" + ) + { + set_text(sublabels.get(position)); + last_position = position; + } + } + return Source.CONTINUE; // Leave timer to be recalled + }); + + return Source.REMOVE; + }); + } // cycle +} // CyclingRevealLabel + diff --git a/src/Widgets/WelcomeButton.vala b/src/Widgets/base/DisplayButton.vala similarity index 50% rename from src/Widgets/WelcomeButton.vala rename to src/Widgets/base/DisplayButton.vala index 315b3c6..be2c2ee 100644 --- a/src/Widgets/WelcomeButton.vala +++ b/src/Widgets/base/DisplayButton.vala @@ -3,19 +3,41 @@ * SPDX-FileCopyrightText: 2020-2022 Louis Brauer */ - public class Tuner.WelcomeButton : Gtk.Button { +/** + * @class Tuner.DisplayButton + * @brief A button widget that displays a title, tag, description, and an optional favicon. + * + * This class extends Gtk.Button and is used to represent a display button + * in the tuner application, showing relevant information about a station. + */ +public class Tuner.DisplayButton : Gtk.Button +{ + + private const int TITLE_WIDTH = 25; + + /** + * @brief Default icon name for stations without a custom favicon. + */ + Gtk.Label button_title; ///< The label displaying the title of the button. + Gtk.Label button_tag; ///< The label displaying the tag of the button. + Gtk.Label button_description; ///< The label displaying the description of the button. + Gtk.Grid button_grid; ///< The grid layout for organizing button contents. - Gtk.Label button_title; - Gtk.Label button_tag; - Gtk.Label button_description; - Gtk.Image? _favicon_image; - Gtk.Grid button_grid; + protected Gtk.Image _favicon_image; ///< The image displayed as the favicon. Updated by derived classes + /** + * @brief Gets or sets the title of the button. + * @return The current title of the button. + */ public string title { get { return button_title.get_text (); } set { button_title.set_text (value); } } + /** + * @brief Gets or sets the tag of the button. + * @return The current tag of the button. + */ public string tag { get { return button_tag.get_text (); } set { @@ -25,42 +47,41 @@ } } + /** + * @brief Gets or sets the description of the button. + * @return The current description of the button. + */ public string description { get { return button_description.get_text (); } - set { - button_description.set_text (value); - } + set { button_description.set_text (value); } } - public Gtk.Image? favicon { - get { return _favicon_image; } - set { - if (_favicon_image != null) { - _favicon_image.destroy (); - } - _favicon_image = value; - if (_favicon_image != null) { - _favicon_image.set_pixel_size (48); - _favicon_image.halign = Gtk.Align.CENTER; - _favicon_image.valign = Gtk.Align.CENTER; - button_grid.attach (_favicon_image, 0, 0, 1, 2); - } - } - } - - /* - public WelcomeButton (Gtk.Image? image, string title, string description) { - Object (title: title, description: description, icon: image); - } - - public WelcomeButton.with_tag (Gtk.Image? image, string title, string description, string tag) { - Object (title: title, description: description, icon: image, tag: tag); + /** + * @brief Gets the favicon image. + * @return The current favicon image. + */ + public Gtk.Image favicon_image + { + protected get { return _favicon_image; } + construct { _favicon_image = value; } } -*/ + /** + * @brief Constructs a new DisplayButton instance. + * + * This constructor initializes the button's labels and layout, + * setting up the grid and adding the necessary styles. + */ construct { + + // Favicon + _favicon_image.set_pixel_size (48); + _favicon_image.halign = Gtk.Align.CENTER; + _favicon_image.valign = Gtk.Align.CENTER; + // Title label button_title = new Gtk.Label (null); + button_title.set_max_width_chars (TITLE_WIDTH); button_title.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); button_title.halign = Gtk.Align.START; button_title.valign = Gtk.Align.END; @@ -92,7 +113,9 @@ button_grid.attach (button_title, 1, 0, 2, 1); button_grid.attach (button_tag, 1, 1, 1, 1); button_grid.attach (button_description, 2, 1, 1, 1); + button_grid.attach (_favicon_image, 0, 0, 1, 2); - this.add (button_grid); + add (button_grid); } + } diff --git a/src/Widgets/AbstractContentList.vala b/src/Widgets/base/ListFlowBox.vala similarity index 50% rename from src/Widgets/AbstractContentList.vala rename to src/Widgets/base/ListFlowBox.vala index 8a8785d..d5c36e1 100644 --- a/src/Widgets/AbstractContentList.vala +++ b/src/Widgets/base/ListFlowBox.vala @@ -1,19 +1,22 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2020-2022 Louis Brauer - */ - /** - * @class AbstractContentList - * @brief An abstract base class for content list widgets in the Tuner application. + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file ListFlowBox.vala + * + * @class ListFlowBox + * @brief An base class for content list widgets in the Tuner application. * - * The AbstractContentList class serves as a foundation for creating content list + * The ListFlowBox class serves as a foundation for creating content list * widgets. It extends Gtk.FlowBox and provides a basic structure for implementing * content lists with a customizable item count. * * @extends Gtk.FlowBox */ -public abstract class Tuner.AbstractContentList : Gtk.FlowBox { +public class Tuner.ListFlowBox : Gtk.FlowBox +{ /** * @property item_count @@ -22,6 +25,6 @@ public abstract class Tuner.AbstractContentList : Gtk.FlowBox { * This abstract property must be implemented by derived classes to provide * getter and setter methods for managing the item count of the content list. */ - public abstract uint item_count { get; set; } + public uint item_count { get; set; } -} +} // ListFlowBox diff --git a/src/Widgets/base/RevealLabel.vala b/src/Widgets/base/RevealLabel.vala new file mode 100644 index 0000000..ec5ae9a --- /dev/null +++ b/src/Widgets/base/RevealLabel.vala @@ -0,0 +1,118 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file RevealLabel.vala + */ + + using Gtk; + using GLib; + +/** + * @class RevealLabel + * @brief A custom widget that reveals a label with animation. + * + * This class extends Gtk.Revealer to create a label that can be revealed + * and hidden with smooth transitions. + * Uses Gtk.Label get_text and set_text for label text + * + * @extends Gtk.Revealer + */ +public class Tuner.RevealLabel : Gtk.Revealer +{ + private Mutex _set_text_lock = Mutex(); // Lock out concurrent updates + private string _next_text; + + /** + * @brief Default duration for fade-in animation in milliseconds. + */ + private const uint DEFAULT_FADE_DURATION = 1200u; + + /** + * @property label_child + * @brief The Gtk.Label widget contained within the revealer. + */ + public Label label_child { get; construct set; } + + + /** @property {string} label - Label text. Setting this property triggers the reveal animation. */ + public string label { + get { return get_text(); } + set { set_text ( value ); } + } // label + + + /** + * @brief Initializes the RevealLabel with default properties. + */ + construct { + label_child = new Label (""); + label_child.ellipsize = Pango.EllipsizeMode.MIDDLE; + child = label_child; + transition_type = RevealerTransitionType.CROSSFADE; + transition_duration = DEFAULT_FADE_DURATION; + } // construct + + + protected void clear() { + _next_text = ""; + label_child.label = ""; + } // clear + + + /** + * @brief The text displayed in the label. + * + */ + public unowned string get_text() { + return label_child.label; + } // get_text + + + /** + * @brief The text to display in the label. + * + * Setting this text triggers the reveal animation. + */ + public bool set_text ( string text ) + { + // Prevent transition if same title is submitted multiple times + if ( label_child.label == text) return true; + + if ( _set_text_lock.trylock() == false ) return false; + + reveal_child = false; + _next_text = text; + + if ( label_child.label == "" && text != "") + // Reveal new text without fade if label is empty + { + label_child.label = _next_text; + reveal_child = true; + _set_text_lock.unlock (); + return true; + } + + Timeout.add ( 11*DEFAULT_FADE_DURATION/10, () => + // Clear the text after fade has completed + { + label_child.label = ""; + return Source.REMOVE; + },Priority.HIGH_IDLE); + + Timeout.add (3*DEFAULT_FADE_DURATION/2, () => + // Update and reveal new text after fade and clear have completed + { + label_child.label = _next_text; + reveal_child = true; + _set_text_lock.unlock (); + return Source.REMOVE; + }, Priority.DEFAULT_IDLE); + + return true; + } // set_text + +} + diff --git a/src/Widgets/base/StackLabel.vala b/src/Widgets/base/StackLabel.vala new file mode 100644 index 0000000..31012af --- /dev/null +++ b/src/Widgets/base/StackLabel.vala @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * @class StackLabel + * @brief A custom label widget for stack headers. + * + * This class extends Gtk.Label to create a specialized label + * for use as stack headers in the application. + * + * @extends Gtk.Label + */ +public class Tuner.StackLabel : Gtk.Label +{ + + public StackLabel (string label, int xpad = 0, int ypad = 0 ) + { + Object ( + label: label, + xpad: xpad, + ypad: ypad + ); + } + + construct { + halign = Gtk.Align.START; + xalign = 0; + get_style_context ().add_class ("stack-label"); + } + +} diff --git a/src/Widgets/base/StationList.vala b/src/Widgets/base/StationList.vala new file mode 100644 index 0000000..58946cd --- /dev/null +++ b/src/Widgets/base/StationList.vala @@ -0,0 +1,110 @@ +/** + * SPDX-FileCopyrightText: Copyright © 2020-2024 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * @file StationList.vala + */ + +using Gee; + +/** + * @class StationList + * @brief A widget for displaying and managing a list of radio stations. + * + * The StationList class extends ListFlowBox to provide a specialized + * widget for displaying radio stations. It manages station selection. + * + * @extends ListFlowBox + */ +public class Tuner.StationList : ListFlowBox +{ + /** + * @signal selection_changed + * + * @brief Emitted when a station is selected. + * + * @param station The selected Model.Station. + */ + public signal void station_clicked_sig (Model.Station station); + + + /** + * @brief Constructs a new StationList instance. + * + * Initializes the StationList with default properties for layout and behavior. + */ + public StationList () + { + Object ( + homogeneous: false, + min_children_per_line: 1, + max_children_per_line: 3, + column_spacing: 5, + row_spacing: 5, + border_width: 20, + valign: Gtk.Align.START, + selection_mode: Gtk.SelectionMode.NONE + ); + } // StationList + + + /** + * @brief Constructs a new StationList instance with a predefined list of stations. + * + * Takes the stations, wraps them as buttons and adds them to the flowbox. + * + * @param stations The ArrayList of Model.Station objects to populate the list. + */ + public static StationList? with_stations (Gee.Collection? stations) + { + if (stations == null) + return null; + StationList list = new StationList (); + list.stations = stations; + return list; + } // StationList.with_stations + + + /** + * @brief The list of stations to display. + * + * When set, this property clears the existing list and populates it with + * the new stations. It also sets up signal connections for each station. + */ + public Collection stations + { + // FIXME Wraps stations in SttionButtons, adds to flowbox + set construct { + clear (); + if (value == null) + return; + + foreach (var station in value) + { + var box = new StationButton (station); + box.clicked.connect (() => { + station_clicked_sig (box.station); + }); + add (box); + } + item_count = value.size; + } + } // stations + + + /** + * @brief Clears all stations from the list. + * + * This method removes and destroys all child widgets from the StationList. + */ + public void clear () + { + var childs = get_children(); + foreach (var c in childs) + { + c.destroy(); + } + } // clear +} // StationList diff --git a/src/Widgets/base/StationListBox.vala b/src/Widgets/base/StationListBox.vala new file mode 100644 index 0000000..71199e6 --- /dev/null +++ b/src/Widgets/base/StationListBox.vala @@ -0,0 +1,438 @@ +/* + * SPDX-FileCopyrightText: 2020-2022 Louis Brauer + * SPDX-FileCopyrightText: Copyright © 2024 technosf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +using Gtk; +using Gee; +using Granite.Widgets; + +/** + * @file SourceListBox.vala + * @brief Defines the ContentBox widget for displaying content with a header and action button. + * + * This file contains the implementation of the ContentBox class, which is a custom + * Gtk.Box widget used to display content with a header, optional icon, and an + * optional action button. It provides a flexible layout for presenting various + * types of content within the Tuner application. + * + * @namespace Tuner + */ +namespace Tuner +{ + public interface StationListHookup : Object + { + public abstract void station_list_hookup( StationList station_list ); + } // StationListHookup + + + /** + * @class StationListBox + * @brief A custom Gtk.Box widget for displaying content with a header and action button. + * + * The ContentBox class is a versatile widget used to present various types of content + * within the Tuner application. It features a header with an optional icon and action + * button, and a content area that can display different views based on the current state. + * + * @extends Gtk.Box + */ + public class StationListBox : Gtk.Box { + + /** + * @property header_label + * @brief The label displayed in the header of the ContentBox. + */ + // public HeaderLabel header_label; + + public Button tooltip_button{ get; private set; } + public StationListItem item { get; private set; } + public uint item_count { get; private set; } + public string parameter { get; set; } + public bool show_parameter { get; set; } + + /** + * @brief Updates the badge text for the source list item + * @param badge The text to display in the badge + */ + public void badge (string badge) + { + item.badge = badge; + } // badge + + + /** + * @signal action_button_activated_sig + * @brief Emitted when the action button is clicked. + */ + public signal void action_button_activated_sig (); + + + /** + * Signal emitted when the number of items in the list changes. + * + * @param item_count The new number of items in the list + * @param parameter Additional parameter that provides context for the change + */ + public signal void item_count_changed_sig ( uint item_count, string? parameter ); + + + /** + * @signal content_changed_sig + * @brief Emitted when the content of the ContentBox is changed. + */ + public signal void content_changed_sig (uint count); + + + // ----------------------------------- + + private SourceList.ExpandableItem _category; + private ThemedIcon _icon; + private Box _content = base_content(); + private ListFlowBox _content_list; + private Stack _stack; + private SourceList _source_list; + private Stack _substack = new Stack (); + private StationSet? _data; + + + + /** + * @brief Constructs a new ContentBox instance. + * + * @param icon The optional icon to display in the header. + * @param title The title text for the header. + * @param subtitle An optional subtitle to display below the header. + * @param action_icon_name The name of the icon for the action button. + * @param action_tooltip_text The tooltip text for the action button. + */ + private StationListBox ( + Stack stack, + SourceList source_list, + SourceList.ExpandableItem category, + string name, + string icon, + string title, + string subtitle, + bool prepopulated = false, + StationSet? data, + string? action_tooltip_text, + string? action_icon_name, + bool enable_count) + { + Object ( + name:name, + orientation: Orientation.VERTICAL, + spacing: 0 + ); + + // get_style_context().add_class("station-list-box"); + + var _header = base_header(); + + _stack = stack; + _source_list = source_list; + _category = category; + + _data = data; + _icon = new ThemedIcon (icon); + + item = new StationListItem (title, this, prepopulated); + item.icon = _icon; + item.set_data ("stack_child", name); + + var alert = new AlertView (_("Nothing here"), _("Something went wrong loading radio stations data from radio-browser.info. Please try again later."), "dialog-warning"); + // /* + // alert.show_action ("Try again"); + // alert.action_activated.connect (() => { + // realize (); + // }); + // */ + + _substack.add_named (alert, "alert"); + + var no_results = new AlertView (_("No stations found"), _("Please try a different search term."), "dialog-warning"); + _substack.add_named (no_results, "nothing-found"); + + _header.pack_start (new StackLabel (subtitle, 20, 20 ), false, false); + + if (action_icon_name != null && action_tooltip_text != null) { + tooltip_button = new Button.from_icon_name ( + action_icon_name, + IconSize.LARGE_TOOLBAR + ); + tooltip_button.valign = Align.CENTER; + tooltip_button.tooltip_text = action_tooltip_text; + tooltip_button.clicked.connect (() => { action_button_activated_sig (); }); + _header.pack_start (tooltip_button, false, false); + } + + var _parameter_label = new StackLabel("", 20, 20); + _header.pack_start (_parameter_label, false, false); + notify["parameter"].connect (() => + { + _parameter_label.label = parameter; + }); + + pack_start (_header, false, false); + + // ----------------------------------- + + pack_start (new Separator (Orientation.HORIZONTAL), false, false); + + // ----------------------------------- + + _substack.add_named (content_scroller(_content), "content"); + add (_substack); + + show.connect (() => { + _substack.set_visible_child_full ("content", StackTransitionType.NONE); + }); + + map.connect (() => { + source_list.selected = item; + }); + + category.add (item); + } // SourceListBox + + + /** + * @brief Initializes the ContentBox instance. + * + * This method is called automatically by the Vala compiler and sets up + * the initial style context for the widget. + */ + construct { + get_style_context ().add_class ("color-dark"); + } // construct + + + /** + * @brief Retrieves the next page of stations from the data source + * @return A Set of Model.Station objects, or null if no data source exists + * @throws SourceError If there's an error retrieving the next page + */ + public Set? next_page () throws SourceError + { + if ( _data == null ) return null; + return _data.next_page(); + } // next_page + + + /** + * @brief Displays the alert view in the content area. + */ + public void show_alert () { + _substack.set_visible_child_full ("alert", StackTransitionType.NONE); + } // show_alert + + + /** + * @brief Displays the "nothing found" view in the content area. + */ + public void show_nothing_found () { + _substack.set_visible_child_full ("nothing-found", StackTransitionType.NONE); + } // show_nothing_found + + + /** + * @brief Sets the content list and displays it + * @param content The ContentList to display + */ + public void list(ListFlowBox content) + { + this.content = content; + show_all(); + } // list + + + /** + * @brief Removes this SourceListBox from the stack and category + */ + public void delist() + { + _stack.remove(this); + _category.remove (item); + tooltip_button.sensitive = false; + } // delist + + + /** + * @property content + * @brief Gets or sets the content list displayed in the ContentBox. + * + * When setting this property, it replaces the current content with the new + * AbstractContentList and emits the content_changed_sig signal. + */ + public ListFlowBox content { + set { + + foreach (var child in _content.get_children ()) { child.destroy (); } + + _substack.set_visible_child_full ("content", StackTransitionType.NONE); + _content_list = value; + + _content.add (_content_list); // FIXME analyze why when 'saving a search' content is double wrapped? + item_count = _content_list.item_count; + item_count_changed_sig(item_count, parameter); + show_all (); + } + + get { + return _content_list; + } + } // content + + + // ----------------------------------------------- + + + /** + * @brief Creates a basic header box with horizontal orientation + * @return A new Gtk.Box configured as a header + */ + private static Box base_header() + { + var header = new Box (Orientation.HORIZONTAL, 0); + header.homogeneous = false; + return header; + } // base_header + + + /** + * @brief Creates a basic content box with vertical orientation + * @return A new Gtk.Box configured for content + */ + private static Box base_content() + { + var content = new Box (Orientation.VERTICAL, 0); + content.get_style_context ().add_class ("color-light"); + content.valign = Align.START; + content.get_style_context().add_class("welcome"); + return content; + } // base_content + + + /** + * @brief Creates a scrolled window containing the content box + * @param content The content box to be placed in the scrolled window + * @return A new Gtk.ScrolledWindow containing the content + */ + private static ScrolledWindow content_scroller(Gtk.Box content) + { + var scroller = new ScrolledWindow (null, null); + scroller.hscrollbar_policy = PolicyType.NEVER; + scroller.add (content); + scroller.propagate_natural_height = true; + return scroller; + } // content_scroller + + // -------------------------------------------------- + + + /** + * @brief Factory method to create a new SourceListBox instance + * @param stack The main stack widget + * @param source_list The source list widget + * @param category The category to add this item to + * @param name The name identifier for this box + * @param icon The icon name to display + * @param title The title text to display + * @param subtitle The subtitle text to display + * @param prepopulated Whether the content is pre-populated + * @param data The station data set + * @param action_tooltip_text The tooltip text for the action button + * @param action_icon_name The icon name for the action button + * @return A new SourceListBox instance + */ + public static StationListBox create( + Stack stack, + SourceList source_list, + SourceList.ExpandableItem category, + string name, + string icon, + string title, + string subtitle, + bool prepopulated = false, + StationSet? data = null, + string? action_tooltip_text = null, + string? action_icon_name = null ) + { + var slb = new StationListBox( + stack, + source_list, + category, + name, + icon, + title, + subtitle, + prepopulated, + data, + action_tooltip_text, + action_icon_name, + true); + + stack.add_named (slb, name); + + return slb; + } // create + } // SourceListBox + + + /** + * @class StationListItem + * @brief A custom source list item that manages its own population state + * + * This class extends SourceList.Item to provide functionality for lazy-loading + * content and managing the populated state of radio station listings. + * + * @extends SourceList.Item + */ + public class StationListItem : SourceList.Item + { + private bool populated; + private StationListBox _slb; + + /** + * @brief Constructs a new SourceListItem + * @param title The display title for the item + * @param slb The parent SourceListBox this item belongs to + * @param prepopulated Whether this item starts with populated content + */ + public StationListItem(string title, StationListBox slb, bool prepopulated = false ) + { + base ( + title + ); + _slb = slb; + populated = prepopulated; + } + + /** + * @brief Populates the item with station data if not already populated + * @param display The Display instance to hook up the station list + * + * This method checks if the item needs population and if the app is online, + * then attempts to load the next page of stations. If successful, it hooks + * up the station list to the display and updates the content. + */ + public void populate( StationListHookup station_list, bool force = false ) + { + if ( ( populated && !force ) || app().is_offline ) return; + populated = true; + try { + var? slist = StationList.with_stations (_slb.next_page ()); + if ( slist != null ) + { + station_list.station_list_hookup(slist); + _slb.content = slist; + _slb.content.show_all (); + } + } catch (SourceError e) { + _slb.show_alert (); + } + } // populate + } // SourceListItem +} // Tuner \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 8470267..16cf724 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,34 +2,40 @@ sources = files ( 'Controllers/DirectoryController.vala', 'Controllers/PlayerController.vala', + 'Controllers/SearchController.vala', 'Models/Countries.vala', 'Models/Genre.vala', 'Models/Station.vala', - 'Models/StationStore.vala', + + 'Providers/RadioBrowser.vala', 'Services/DBusMediaPlayer.vala', 'Services/DBusInterface.vala', - 'Services/RadioBrowser.vala', - # 'Services/LocationDiscovery.vala', + 'Services/DataProvider.vala', 'Services/HttpClient.vala', - 'Services/Favicon.vala', + 'Services/StarStore.vala', + + 'Widgets/base/ListFlowBox.vala', + 'Widgets/base/DisplayButton.vala', + 'Widgets/base/RevealLabel.vala', + 'Widgets/base/CyclingRevealLabel.vala', + 'Widgets/base/StationListBox.vala', + 'Widgets/base/StackLabel.vala', + 'Widgets/base/StationList.vala', - 'Widgets/AbstractContentList.vala', - 'Widgets/ContentBox.vala', 'Widgets/HeaderBar.vala', - 'Widgets/HeaderLabel.vala', - 'Widgets/StationBox.vala', + 'Widgets/PlayButton.vala', + 'Widgets/ListButton.vala', + 'Widgets/StationButton.vala', 'Widgets/StationContextMenu.vala', + 'Widgets/Display.vala', 'Widgets/Window.vala', 'Widgets/AboutDialog.vala', - 'Widgets/WelcomeButton.vala', - #'Widgets/Theme.vala', 'Widgets/CountryList.vala', - 'Widgets/StationList.vala', - 'Widgets/RevealLabel.vala', 'Widgets/PreferencesPopover.vala', + 'Settings.vala', 'Application.vala', 'Main.vala' ) diff --git a/tags.csv b/tags.csv new file mode 100644 index 0000000..92777b7 --- /dev/null +++ b/tags.csv @@ -0,0 +1,11161 @@ +name,stationcount +"""bob""",2 +"""bob""dobbs",1 +"""chicago's greatest hits """,1 +"""it's called culture""",1 +"""radio k""",1 +"""rockfords country q98.5""",1 +"""the new sound for old memories""r'n'b hits",1 +"""today's best country""",1 +#,11 +##browserfriendly,2 +#00s,3 +#1 pilihan pantai timur,1 +#101,2 +#80,5 +#80s,8 +#90,1 +#90s,8 +#bordeaux,1 +#breaks,1 +#charts,8 +#chill,2 +#chill #80s #90s,1 +#chillout,3 +#classic,1 +#classical,1 +#club,1 +#dj,2 +#djmix,1 +#electro,2 +#electronic-radio,1 +#exclusive,2 +#free,4 +#freefm,9 +#freefm #80s #90s #80,1 +#freefm80s,1 +#freefmchill,1 +#gaming,1 +#hits,1 +#hitsonly,2 +#house,2 +#mashup,4 +#minimal,2 +#mixtapes,1 +#music,2 +#nightlife,1 +#oldschool,1 +#original,1 +#partybreaks,2 +#partyhard,1 +#pop,2 +#progressive,1 +#public radio,1 +#radio,1 +#radio france,1 +#raproyal,1 +#russiandance,1 +#techno,3 +#top100,3 +#top200,1 +#top40,1 +#usrap,1 +'s,1 ++,3 +-,2 +..,1 +0,2 +0 mhz,2 +00's,23 +000,2 +007,1 +00er,4 +00s,144 +00s dance,1 +00s en español,1 +00’s,1 +1,4 +1 live,1 +1 live fiehe,1 +1.000,1 +1.fm,1 +1.fm jazz,1 +10's,4 +100,1 +100.1,8 +100.1 fm,9 +100.3,5 +100.3 fm,12 +100.4,1 +100.5,3 +100.5 fm,6 +100.6,1 +100.6 fm,1 +100.7,3 +100.7 fm,4 +100.9,4 +100.9 fm,14 +100.9fm,3 +1000,1 +1000 am,1 +1008可乐台,1 +101,2 +101.1,2 +101.1 fm,8 +101.3,8 +101.3 fm,14 +101.5,6 +101.5 fm,10 +101.7,7 +101.7 fm,20 +101.8,2 +101.9,3 +101.9 fm,7 +101.ru,6 +102.1,1 +102.1 fm,6 +102.3,3 +102.3 fm,4 +102.5,6 +102.5 fm,13 +102.7,6 +102.7 fm,12 +102.8,1 +102.8 fm,1 +102.9,8 +102.9 fm,16 +1020 am,1 +1024,1 +103.0,1 +103.0 fm,1 +103.1,3 +103.1 fm,7 +103.3,9 +103.3 fm,16 +103.4,1 +103.5,4 +103.5 fm,5 +103.6,1 +103.7,3 +103.7 fm,9 +103.8,1 +103.9,7 +103.9 fm,13 +1030,2 +1030 am,6 +104.1,9 +104.1 fm,11 +104.3,4 +104.3 fm,6 +104.5,8 +104.5 fm,15 +104.7,1 +104.7 fm,4 +104.9,3 +104.9 fm,14 +105 digital,1 +105.1,4 +105.1 fm,7 +105.2,3 +105.2 fm,3 +105.3,1 +105.3 fm,3 +105.5,4 +105.5 fm,8 +105.7,2 +105.7 fm,5 +105.9,3 +105.9 fm,5 +1050,1 +1050 am,2 +106.1,5 +106.1 fm,11 +106.2,2 +106.2 fm,1 +106.3,1 +106.3 fm,5 +106.5,3 +106.5 fm,5 +106.7,2 +106.7 fm,4 +106.9,1 +106.9 fm,3 +107,1 +107.1,8 +107.1 fm,9 +107.2,1 +107.3,3 +107.3 fm,4 +107.4,1 +107.5,4 +107.5 fm,11 +107.7,3 +107.7 fm,7 +107.75 fm ซันไชน์ เรดิโอ พัทยา,1 +107.8,1 +107.8 fm,1 +107.9,7 +107.9 fm,13 +1070 am,1 +1090 am,1 +10er,2 +10s,24 +10s dance,1 +10’s,1 +11.11,1 +1100,1 +1100 am,1 +1110,1 +1110 am,4 +1120,1 +1120 am,2 +1140,1 +1140 am,1 +1150,2 +1150 am,3 +1160 am,1 +1180,1 +1180 am,4 +"12""",2 +1210 am,1 +1220 am,1 +1224 am,1 +1224am,1 +1250,1 +1250 am,2 +1260,1 +1260 am,3 +1270,1 +1270 am,1 +128,1 +128 aac,3 +128 kbit/s,17 +128 kbps,16 +128 mp3,1 +128kbit,10 +12inch,2 +13 languages,1 +1300 am,4 +1310,3 +1310 am,4 +1340,1 +1340 am,5 +1350 am,3 +1360 am,3 +1370,1 +1370 am,1 +1380 am,1 +140bpm,1 +1420,1 +1420 am,1 +1430,1 +1430 am,3 +1440,1 +1440 am,2 +1460 am,2 +1470,1 +1470 am,2 +1480,2 +1480 am,3 +1490 am,2 +15,4 +1500,1 +1500 am,2 +1530 am,6 +1570 am,1 +1590 am,1 +160 kbit/s,2 +1600,1 +1600 am,2 +1620 am,1 +1670,1 +1670 am,3 +16bit,2 +1700,1 +1700 am,1 +192 kbit/s,1 +1920s,17 +192kbps,16 +1930,13 +1930's,1 +1930s,21 +1940,12 +1940's,1 +1940s,25 +1950,15 +1950s,39 +1950s music,1 +1951s,3 +1952s,1 +1953s,1 +1954s,2 +1955s,2 +1956s,2 +1957s,2 +1958s,2 +1959s,2 +1960,14 +1960s,60 +1960s and 1970s,4 +1961s,2 +1962s,2 +1963s,2 +1964s,2 +1965s,3 +1966s,2 +1967s,2 +1968s,2 +1969s,2 +1970,3 +1970s,88 +1971s,2 +1972s,2 +1973s,2 +1974s,2 +1975s,2 +1976s,2 +1977s,2 +1978s,2 +1979s,2 +1980,4 +1980's,35 +1980s,142 +1980s hits,6 +1990's,1 +1990s,100 +1990s hits,2 +1twente,1 +2 stations,1 +2 unlimited,1 +2-tap,1 +2.bundesliga,1 +20 languages,1 +20's,2 +2000,4 +2000's,13 +2000's country,1 +2000-2021,3 +2000er,32 +2000er und schlager-pop,1 +2000s,229 +2000s alternative,1 +2010,3 +2010's,3 +2010's smoothest music,1 +2010er,12 +2010s,83 +2011,3 +2012,3 +2013,3 +2014,3 +2014 burning man festival,1 +2015,3 +2016,3 +2017,3 +2018,3 +2019,3 +2020,3 +2020er,1 +2020s,21 +2021,4 +2021s,1 +2022,4 +2022s,1 +2023,5 +2023s,2 +2024,8 +2024s,1 +2025s,3 +2026s,3 +2027s,3 +2028s,1 +2029s,1 +2030s,1 +2031s,1 +2032s,1 +2033s,1 +2034s,1 +20c,7 +20s,20 +20th century,14 +21c,2 +21st century,10 +22,2 +22.9444° e,1 +24 horas,3 +24 jam program kristiani,1 +24 kbit/s,1 +24-hour punk/adjacent/ska/hardcore/plus,1 +24/7,12 +24/7 music,1 +24hour,1 +24×4 hit punjabi hindi songs,1 +256 kbit/s,10 +256 kbps,3 +29,2 +2bl/t,1 +2day,2 +"2day ""mobile""",1 +3 doors down,1 +30's,2 +30er,1 +30s,11 +30s-60s,1 +30son,1 +30song,1 +32 kbps,7 +32 kbps ve formátu aac,1 +320 kbit/s,6 +320 kbps,6 +320kbps,11 +320mpbs,1 +32k,4 +33/45,2 +35,1 +3sat,2 +4,1 +40,1 +40's,2 +40.6401° n,1 +40er,1 +40s,13 +47,14 +48 kbit/s,4 +48kbps,2 +4am,1 +4chan,1 +4kz,1 +5,1 +50 plus,1 +50's,7 +500hits,1 +50er,11 +50s,66 +50s bop rockabilly,1 +50s music,1 +53,1 +540,7 +540 am,12 +560,3 +560 am,4 +570,6 +570 am,6 +580,3 +580 am,3 +580am,1 +590,2 +590 am,5 +5g智慧电台,16 +60,5 +60's,43 +60's -90's music,1 +60er,43 +60iger,1 +60s,227 +60s and 70s.,1 +60s girl groups,1 +60s to 90s,4 +60s-90s,1 +620 am,2 +630,1 +630 am,5 +64 kbit/s,3 +64 kbps,1 +64 kbps aac+,2 +642 media,1 +64kbps,10 +650,1 +660 am,1 +690,2 +690 am,5 +6ix,1 +7,1 +70,10 +70 80 baladas varios,1 +70 80 hits,31 +70 80 hits hq,1 +70 hits,1 +70 music,1 +70's,89 +70-80,1 +70-80 hits,1 +70-90-х,1 +70-tal,1 +70-е,1 +700,4 +700 am,5 +70er,66 +70ige,1 +70iger,2 +70s,505 +70s 80s 90s music,4 +70s disco,27 +70s oldies,1 +70s rock,5 +70s rock ballads,4 +70s soft rock,11 +710 am,2 +720 am,1 +730,2 +730 am,3 +740 am,1 +76,1 +76.1,2 +76.2,3 +76.3,3 +76.4,1 +76.5,1 +76.7,1 +76.8,1 +76.9,1 +760,1 +760 am,2 +77.4,1 +77.5,3 +77.6,1 +77.7,3 +77.9,2 +78,6 +78-rpm,5 +78.1,1 +78.9,1 +780,1 +780 am,1 +78rpm,5 +79.1,4 +79.4,1 +79.6,1 +79.7,3 +79.8,1 +8 bit,7 +80,28 +80 hits,3 +80 kbit/s,1 +80 music,1 +80's,200 +80's & 90's,2 +80's hits,2 +80's pop hits,1 +80's trova 90's,1 +80-90,2 +80-s,1 +80-tal,1 +80-е,1 +80.1,1 +80er,130 +80iger,3 +80r,1 +80s,1040 +80s 90s grunge,3 +80s and 90s,1 +80s disco,1 +80s en español,29 +80s hits,1 +80s live radio,3 +80s radio,10 +80s rock,23 +80s soft rock,7 +80s stars,1 +80s style,1 +80s80s,1 +80th,2 +80th rock,1 +80´s pop,1 +80’s,1 +81.5,1 +81.8,1 +810 am,2 +82.2,1 +82.6,1 +820,1 +820 am,1 +83.2,1 +83.4,1 +83.8,1 +830,1 +830 am,3 +84.2,1 +84.4,1 +84.5,1 +85.4,2 +85.6,1 +850,3 +850 am,4 +86.1,1 +86.2,1 +86.8,1 +86.9,1 +860,1 +860 am,5 +87.0,1 +87.3,1 +87.6,2 +87.6 fm,2 +87.7,1 +88.1,7 +88.1 fm,15 +88.2,3 +88.3,4 +88.3 fm,6 +88.5,9 +88.5 fm,15 +88.6,3 +88.6 fm,2 +88.7,8 +88.7 fm,9 +88.8,1 +88.8 fm,2 +88.9,5 +88.9 fm,7 +89.1,11 +89.1 fm,15 +89.2,1 +89.2 fm,1 +89.3,3 +89.3 fm,13 +89.5,1 +89.5 fm,6 +89.5 livemusic nonstop all day all night,1 +89.7,12 +89.7 fm,18 +89.9,4 +89.9 fm,11 +895,1 +8bit,5 +9. may,1 +90,16 +90 80 70,1 +90's,143 +90's and more!,1 +90's country,1 +90-s,1 +90-tal,1 +90-е,2 +90.0,4 +90.0 fm,3 +90.1,12 +90.1 fm,15 +90.1 mix,1 +90.25 fm city radio pattaya thailand,1 +90.3,4 +90.3 fm,9 +90.5,4 +90.5 fm,11 +90.7,3 +90.7 fm,9 +90.9,4 +90.9 fm,9 +900,1 +900 am,2 +905fm,2 +90er,84 +90s,816 +90s alternative,5 +90s dance,16 +90s en español,1 +90s hits,1 +90s indie,1 +90s rock,2 +90s salsa,1 +90s trance,1 +90s y más,32 +90’s,1 +91 dat,1 +91.1,2 +91.1 fm,8 +91.1.,1 +91.3,5 +91.3 fm,8 +91.5,3 +91.5 fm,6 +91.6,1 +91.6 fm,1 +91.7,4 +91.7 fm,9 +91.9,1 +91.9 fm,4 +9128,1 +92.0,1 +92.0 fm,1 +92.1,5 +92.1 capital fm,1 +92.1 fm,12 +92.3,2 +92.3 fm,9 +92.4,1 +92.4 fm,1 +92.5,8 +92.5 fm,13 +92.7,1 +92.7 fm,4 +92.9,7 +92.9 pop,2 +92.9 fm,11 +93.0,1 +93.0 fm,1 +93.1,6 +93.1 fm,12 +93.3,5 +93.3 fm,30 +93.4,2 +93.5,4 +93.5 fm,7 +93.6,1 +93.6 fm,1 +93.7,5 +93.7 fm,9 +93.8,1 +93.9,1 +93.9 fm,8 +930 am,1 +94 fm,1 +94.1,7 +94.1 fm,10 +94.3,2 +94.3 fm,7 +94.4,1 +94.5,5 +94.5 fm,12 +94.7,1 +94.7 fm,2 +94.8,1 +94.8 fm,1 +94.9,6 +94.9 fm,14 +940 am,1 +95.0,1 +95.0 fm,1 +95.1,3 +95.1 fm,6 +95.3,5 +95.3 fm,20 +95.5,4 +95.5 fm,10 +95.6,2 +95.6 fm,1 +95.7,3 +95.7 fm,12 +95.8,1 +95.8 fm,1 +95.9,3 +95.9 fm,8 +950 am,2 +96 kbit/s,1 +96.0,1 +96.0 fm,1 +96.1,10 +96.1 fm,22 +96.2fm,1 +96.3,6 +96.3 fm,10 +96.4,1 +96.5,6 +96.5 fm,15 +96.6,3 +96.7,3 +96.7 fm,4 +96.8,1 +96.8 fm,1 +96.9,7 +96.9 fm,12 +960,1 +960 am,3 +96kbps,3 +97.0,2 +97.0 fm,2 +97.1,7 +97.1 fm,7 +97.2 fm,1 +97.3,6 +97.3 fm,11 +97.4,1 +97.4 fm,1 +97.5,2 +97.5 fm,4 +97.6,1 +97.6 fm,1 +97.7,5 +97.7 fm,10 +97.9,5 +97.9 fm,9 +97.une,1 +970,1 +970 am,4 +97x,1 +97x.fm,1 +97xfm,1 +98,1 +98.0,1 +98.1,6 +98.1 fm,10 +98.2,1 +98.3,3 +98.3 fm,7 +98.4,1 +98.5,9 +98.5 fm,16 +98.6,1 +98.7,7 +98.7 fm,7 +98.9,5 +98.9 fm,14 +98live,1 +99.0,2 +99.0 fm,2 +99.1,4 +99.1 fm,10 +99.3,11 +99.3 fm,23 +99.4,2 +99.4 fm,2 +99.5,2 +99.5 fm,8 +99.7,6 +99.7 fm,15 +99.9,3 +99.9 fm,10 +990 am,2 +9ties,1 +:-),1 +@bwayrecords,1 +@fm,31 +@fm te conecta,31 +\,1 +a,3 +a cappella,1 +a strangely isolated place,1 +aaa,12 +aac,63 +aac 64,1 +aac+,22 +abba,2 +abbotsford,1 +abc,74 +abc australia,1 +abc deportes,1 +abc news,1 +abc noticias,2 +abc radio,16 +abdulbasit,1 +abdulrahman,1 +abdulsamad,1 +abendschau,1 +abilene,3 +abisuak,1 +aboriginal,7 +aboriginal music,1 +abrera,1 +abriss,1 +abrissparty,1 +absolute music,2 +abstract,4 +abstract hip-hop,2 +abstract rap,1 +abtnaundorf,2 +ac,4 +ac dc,2 +ac/dc,17 +academia,1 +acaponeta,1 +acapulco,19 +acayucan,3 +acb,5 +acb media,5 +accapella,1 +accordéon,2 +accoustic,1 +acdc,12 +achkidfm,1 +aci,1 +acicastello,1 +acid,8 +acid house,2 +acid jazz,14 +acid-house,1 +acidcore,1 +acir,89 +acir online,36 +acireale,1 +acores,1 +acousmatic,3 +acoustic,34 +acoustic guitar,3 +acoustic rock,1 +action,1 +activa,1 +active rock,32 +activismo,3 +actu,2 +actualidad,11 +actualidas,1 +actualites,3 +actualités,1 +acustic,3 +acustik radio,3 +acámbaro,3 +ad,1 +ad free,4 +ada,2 +add_free,1 +added by redbullet28,1 +added by shivam,10 +adelaide,6 +adelsberg,1 +adfree,1 +adictiva radio,2 +adictivo radio,2 +adlut contemporary,3 +adom,2 +adom fm,2 +adoracion,2 +adul,1 +adult,36 +adult album alternative,49 +adult alternative,16 +adult alternative pop rock,2 +adult classic pop,1 +adult contempary,2 +adult contemporany,2 +adult contemporary,770 +adult conterporary,2 +adult entertainment,1 +adult hits,120 +adult hits & dance - adult hits/adult,1 +adult pop,3 +adult rock,3 +adult spanish hits,9 +adult standards,12 +adult top 40,8 +adult-contemporary,1 +adulta,11 +adulte contemporain,1 +adulto,2 +adulto contemporaneo,1 +adulto contemporáneo,4 +adulto joven,4 +advent,2 +adventist,7 +adventista,2 +adventure,3 +advertising,5 +aereoporto,1 +aerosmith,3 +aesthetic,1 +afghan,1 +afk,3 +africa,6 +african,15 +african american,1 +african hip hop,2 +african music,89 +african news,1 +african pop,5 +african radio,1 +afrikaans,2 +afrikaburn,1 +afrique,2 +afro,8 +afro disco,1 +afro house,4 +afro jazz,1 +afro soul,1 +afro vocal,1 +afro-cuban jazz,2 +afro-latin music,3 +afro-pop,3 +afro-rock,1 +afrobeat,8 +afrobeats,24 +afrofusion,1 +afrohouse,1 +afropop,6 +afrosoul,1 +afterwork,1 +agape,1 +agencia informativa latinoamericana,1 +aggression,1 +aggrotech,3 +agriculture,16 +agrigento,2 +agronegócio,1 +agua prieta,2 +aguachica,1 +aguascalientes,29 +aguascalientes city,8 +ahlsdorf,4 +ai,1 +ai audio experience,1 +ai generated,1 +ain,1 +air,4 +airchecks,2 +airport,1 +airway heights,1 +ajax,1 +akron,2 +aktionen,1 +aktorska,1 +aktuell,4 +aktuelle stunde,1 +aktueller bericht,1 +akwesasne,1 +al,1 +al aan,1 +al amazighia en direct,1 +al-bilad radio,1 +alaan,1 +alabama,4 +alabama crimson tide sports,1 +alabanzas,2 +alamosa,1 +alan jackson,2 +alanya,2 +alarm,2 +alaska,2 +alaska public radio,2 +alaturka,1 +albacete,2 +albania,1 +albanian music,2 +albany,5 +albavilla,1 +albenga,2 +alberta,1 +album,4 +album adult alternative,1 +album oriented rock,1 +album rock,25 +album tracks,2 +albuquerque,1 +alcúdia,1 +alegria mexicana,1 +alert,3 +alerta nacional,1 +alerta nacional 24hs,1 +alessandria,1 +alfa,2 +alfa rock,1 +ali alhuthaifi,1 +alica medios,7 +alicante,1 +alice cooper,2 +alice springs,1 +alistair begg,1 +aljazeera,1 +all,1 +all 80s,6 +all ages,1 +all india radio,5 +all kind,1 +all music,3 +all the hits,1 +all-time favourites,2 +all-time-classics,5 +allaturca,1 +alle (schlager nur bedingt),2 +alle farben,1 +alle keywords in einer zeile: coloxtra,1 +alle neuen clubhits,2 +allende,1 +allentown,1 +allstedt,4 +allzic,1 +alma radio,1 +almería,6 +aloyoon alkoshi - rewaya warsh a'n nafi',1 +alpen,1 +alpenfetzer,1 +alpenrock,1 +alpicat,1 +alpignano,4 +alpine,1 +alseraj,1 +alseraj radio quran,1 +alsudais,1 +alt country,1 +alt rock,2 +alt-folk,3 +alta,1 +altafulla,1 +altaiskiy,1 +altaiy,3 +altaiyskiy,1 +altavoz radio,2 +altchemnitz,1 +alte,1 +altea,1 +alternate,1 +alternativ,1 +alternativ rock,1 +alternativa,1 +alternativa 98,1 +alternative,557 +alternative / indie,40 +alternative 70s,2 +alternative 80s,2 +alternative classics,7 +alternative country,12 +alternative indie rock,10 +alternative metal,5 +alternative pop,13 +alternative pop rock,4 +alternative r&b,1 +alternative rock,274 +alternative wave,3 +alternative youth culture station,1 +alternative électronique tekno,1 +alternative/indie,2 +alternativer radiosender,1 +alternativo,1 +alternatywa,2 +althen,2 +altmittweida,1 +alto lucero,1 +altstadt,6 +am,165 +am 1420,1 +am con vos,1 +am pop,17 +am1161,1 +am1206,1 +am1380,1 +am1557,2 +am1629,1 +am783,1 +am828,1 +am830,1 +am999,1 +amador,5 +amapiano,8 +amarillo,1 +amateur radio,2 +amateurfunk,11 +amazon cumbia,1 +ambiance,1 +ambiant,1 +ambient,271 +ambient and relaxation music,79 +ambient black metal,1 +ambient dub,1 +ambient easy listening,1 +ambient electronic,1 +ambient jazz,2 +ambient lounge,6 +ambient techn,1 +ambient techno,5 +ambiental,10 +ambiental și lounge,1 +ambiente,2 +amchikonkani,1 +ameca,2 +amecameca,1 +america,1 +america's country,1 +america's pure rock,1 +american,2 +american comedy,2 +american council of the blind,6 +american folk,1 +american forces network,23 +american r&b 90s,2 +american top 40,2 +american underground,1 +americana,43 +amg,7 +amharic,4 +amherst,4 +amherst island,1 +amiga,4 +amlo,1 +ammendorf,4 +amor,40 +amor 101,1 +amor es,1 +amor sólo música romántica,23 +amore,2 +amos,1 +amour,1 +amsterdam,3 +amsterdam-zuidoost,1 +amtrak,1 +américa,349 +analog,2 +analog rock,1 +analysen aus politik und kultur,1 +anan,1 +anarchism,3 +anarcho-punk,1 +anbetung,1 +anchorage,1 +ancient music,4 +ancona,2 +and english,1 +andalucía,6 +andover,1 +andres manuel lopez obrador,1 +andré luiz,1 +anekdot,1 +angels radio,1 +anger,2 +anglo,4 +angura kei,1 +animals,1 +anime,49 +anime groove,3 +anime music from japan. we also play eurobeat and city pop,1 +anime openings,8 +anime ost,1 +anime radio,13 +animegroove,3 +ankara,2 +ankara üniversitesi,1 +ann arbor,3 +annaberg-buchholz,1 +annapolis,1 +annapolis valley,2 +annecy,1 +anni 60,1 +anni 70,3 +anni 80,5 +anni 90,2 +année 2000,1 +année90,1 +années 80,16 +anonymous,2 +anos 2000,6 +anos 70,1 +anos 80,5 +anos 90,2 +anos80,1 +anos90,1 +anrode,4 +antananarivo,1 +antena,2 +antena 1,8 +antena 1 açores,1 +antena 1 madeira,1 +antena 1 memoria,1 +antena 2,1 +antena braşovului,1 +antenna,1 +antenne mv,1 +anthems,8 +anthrax,12 +anticomunist,1 +antifa,10 +antifa news,1 +antifaschismus,1 +antikapitalismus,9 +antinea radio,1 +antirassismus,1 +antisemitism,1 +anzio,1 +anáhuac,1 +análise,1 +aor,17 +aor / slow rock / soft rock,1 +aor / slow rock / soft rock (adult-orientated rock e.g. the eagles.),7 +aor/slow rock/soft rock,2 +aosta,3 +ap grupo radio,1 +apache 207,1 +apache junction,1 +apatzingán,1 +apizaco,1 +apm,43 +apocrypha,1 +apodaca,1 +apollo radio))),5 +app-id=335003717 (apple-itunes-app),1 +appleton,3 +apresski,6 +aqua,1 +aquí nomás,41 +ar rahman tamil hit songs,1 +arab,6 +arab music,2 +arabesk,7 +arabesk fantazi,5 +arabic,25 +arabic hits,1 +arabic music,12 +arabic songs,1 +aracaju,1 +aracatuba,1 +aragon,3 +aragón,1 +arahal,1 +araçatuba,1 +arca,1 +arcade,1 +archive,1 +arctic monkeys,1 +ard,53 +ard zdf,1 +ard zdf deutschlandradio rundfunkbeitrag,9 +ard-aktuell,1 +ard-alpha,1 +ard-mdr,3 +ard-“anne will“,1 +ard-“hart aber fair“,1 +ardió rumbos 670,3 +arena rock,6 +arenys de mar,1 +arenys de munt,1 +arganil,1 +argentia,1 +argentina,37 +argentine football,2 +argentine rock,1 +argentine sports news,1 +arima,1 +aris,1 +arizona,3 +arkhangelsk,1 +armenian,3 +armonia,2 +army,3 +arnsdorf,2 +arnstein,4 +arqueologia,1 +arre en acustik,3 +arroba fm,33 +arroba fm te conecta,31 +arrocha,1 +art,7 +art bell,4 +art music,1 +art pop,2 +art rock,29 +art rock(e.g. talking heads),3 +arte,2 +artesanias,1 +artist,2 +artistes locaux,1 +artists,5 +artrock,1 +arts,31 +aruz,1 +as roma,3 +asamblea,1 +aschersleben,4 +asheville,2 +ashland,3 +ashtabula county,1 +asiago,1 +asian,7 +asian music,3 +asian pop,1 +asian urban hits,1 +asip,1 +asmr,2 +aspen,1 +assembly of god,1 +assisi,7 +associatif,1 +associative,2 +astoria,1 +astral,1 +asturias,1 +at 40,4 +at work,1 +at40,4 +at_orf,28 +at_welle1,5 +atacama,1 +atari,1 +atasehir,1 +atasözleri,1 +atea,1 +ateas,1 +ateismo,1 +ateo,3 +ateos,1 +atessa,4 +ateísmo,1 +athabasca,1 +athens,2 +atherton,2 +atherton tablelands,1 +athos,2 +atlacomulco,1 +atlanta,7 +atlantic,1 +atlantic beach,1 +atlixco,2 +atmospheric,12 +atmospheric black metal,3 +atmospheric space music,1 +atonal,1 +auburn,2 +audio,1 +audio art,1 +audio book,1 +audio books,1 +audio drama,1 +audiobook,7 +audiobooks,5 +audycje pułkownika piotra wrońskiego,1 +aue-bad schlema,1 +augsburg,2 +augustusburg,1 +auslandsfernsehen,2 +aussie rising stars,1 +aussie rock,1 +austausch über kulturelle themen,2 +austin,5 +austin area,1 +australia,7 +australian music,21 +austrian music,5 +austrian pop music,2 +austrohits,1 +austropop,5 +author readings,1 +autism,9 +auto,12 +auto y más,1 +autogestión,2 +automobile,1 +autonomia operaia,1 +autonomia tecnologica,1 +autorace,1 +autorennen,1 +autos y más,3 +autosport,1 +autres,1 +autónoma,1 +auvergne,1 +avanradio,6 +avanradio radiorama,2 +avant garde,12 +avant-funk,1 +avant-garde,28 +avant-garde jazz,3 +avant-garde rock,1 +avante-garde,1 +avantgarde,4 +avellaneda,1 +avellino,2 +avezzano,1 +aviano,1 +avon,1 +axel springer,2 +axé,2 +ayacucho,1 +azeri,1 +azonto,1 +azores,1 +açores,1 +aşk,1 +aşık,1 +b,1 +baalsdorf,2 +baba,1 +baby,3 +bach,12 +bachata,59 +bachata 70 & 80 varios,2 +bachelor,1 +back fm,1 +background,2 +background reports,2 +backstreet boys,1 +bad axe,1 +bad bibra,4 +bad dã¼rrenberg,2 +bad dürrenberg,2 +bad lauchstã¤dt,2 +bad lauchstädt,2 +bad lausick,2 +bad schandau,2 +badajoz,1 +badalona,1 +baden,1 +baghdad,2 +bagpipes,2 +bagé,1 +baha'i,1 +bahia,1 +bahrain,3 +bahrain fm 93.3,1 +bahía de banderas,3 +bailable,12 +bailanta,1 +baileyś harbor,1 +bairische popmusik,1 +bairische rockmusik,1 +baja california,56 +baja california sur,8 +bajío,3 +baker city,1 +bakersfield,2 +baku,2 +balada,78 +balada en español,88 +balada pop,80 +balada romántica,27 +baladas,114 +baladas 70 80 cumbia tecno varios,1 +baladas 70 80 cumbia varios,1 +baladas 70 80 varios,1 +baladas clásicos tecno 70 80 varios,3 +baladas cumbia 70 80 varios,1 +baladas en español,191 +baladas en ingles,20 +baladas en inglés,9 +baladas música popular colombiana nacional varios,3 +baladas nacional corridos varios,1 +baladas rock alternativa varios,2 +baladas tecno 70 80 varios,1 +balaguer,1 +balearic,3 +balearic beat,3 +balearic house,5 +balearica house,1 +balera,1 +balgstã¤dt,2 +balgstädt,2 +balikpapan,1 +balkan,7 +ballad,1 +ballade,2 +balladen,14 +ballads,57 +ballady,1 +ballermann,2 +ballet,1 +ballroom,1 +baltimore,3 +bamako,3 +banadabi,1 +banadabi radyo,1 +band,1 +band news,1 +banda,245 +banda norteña,117 +banda tradicional,72 +bangalore,1 +bangla,7 +bangla fm,1 +bangla music,1 +bangla natok,1 +bangla radio,1 +bangladesh,3 +bangladeshi,1 +bangor,1 +bangra,1 +banja luka,3 +banyumas,1 +bar,1 +bar band,1 +barbara schöneberger,3 +barbaradio,3 +barbra,1 +barbra streisand,2 +barcelona,2 +bard,4 +bari,2 +barletta,1 +barnaul,4 +barnstã¤dt,2 +barnstädt,2 +baroque,24 +baroque pop,2 +barranquilla,1 +barrie,2 +barry manilow,1 +bartlesville oklahoma,1 +barão,1 +barça,1 +baseball,2 +bashkir,2 +basket,1 +basketball,1 +basque music,4 +bass,25 +bass house,7 +bassline,1 +bastia,1 +batavia,1 +batcave,1 +batista,1 +baton rouge,2 +battle creek,2 +bauchi,2 +bauer radio,5 +bauhaus,1 +baumarkttechno,1 +baumersroda,4 +bautzen,3 +bayerischer rundfunk,7 +bayern,12 +bbc,33 +bbc news,1 +bbc world news,1 +bbcr,1 +bbq- barbeque,1 +bcs broadcast sachsen,5 +bdsm,1 +beach,7 +beach music,4 +beachclub,3 +bearn,1 +beat,13 +beatgo,1 +beatles,8 +beatles kanal,1 +beatles radio,2 +beatport,2 +beatrice,1 +beats,22 +beatsteaks,1 +beaumont,1 +beautiful music,17 +beaver island,1 +bebop,15 +becancour,1 +beesen,4 +beesenstedt,4 +beethoven,3 +beichlingen,4 +beijing,8 +beirut,1 +beisbol,1 +bel air,1 +belarus,2 +belarusian news,2 +belcanto,1 +belfast,1 +belgershain,2 +belgian,2 +belgiqie,1 +belgique,1 +belgium,1 +belgrade,1 +belgrano,1 +belice,1 +belivdere,1 +bell,1 +bell island,1 +bell media,5 +bella música,2 +belle chasse,1 +belle river,1 +belleville,3 +bellingham,1 +belluno,1 +belvedere marittimo,1 +belvidere,1 +benasque,1 +benavente,1 +bend,1 +bendigo,1 +benemérita universidad autónoma de puebla,2 +benfica podcast,1 +bengali,2 +bengali music,1 +bengalore,1 +benghazi,1 +benidorm,1 +benito juárez,1 +benndorf,4 +bennewitz,2 +bennstedt,4 +bentonville,1 +benás,1 +berbice,1 +berbér,1 +berchtesgadener land,1 +berga,4 +bergamo,1 +bergstedt,4 +bergwinkel,4 +berichte über theater- und opernpremieren,2 +berita,1 +berkeley,2 +berkshire,1 +berlin,12 +berlin school,4 +bermsgrün,1 +bermudafunk,1 +bernd das brot,1 +bernsdorf,3 +beschwingte ryhtmen,1 +besonderes radio,1 +best,5 +best 92.6,1 +best hits,1 +best music,5 +best music for you,2 +best of,2 +best of rock,2 +best of the eighties,2 +best of the nineties,3 +best podcast in the universe,1 +best punjabi radio,1 +bestara,1 +bester rock 'n pop,3 +beyit,1 +bfbs,2 +bfm,1 +bfr,1 +bg only,2 +bgi,1 +bhagvadgita radio,1 +bhakti,1 +bhakti sagar durga maa,1 +bhakti sangeet,3 +bhangra,5 +bhangrah,1 +bhojpuri,10 +bhojpuri dhamal,1 +bhojpuri gold,1 +bhojpuri hits,1 +bhojpuri hot song,1 +bhojpuri radio,1 +bibel,10 +bible,91 +bible study,2 +bible teaching,3 +biblia,15 +biblica,4 +biblie,1 +bielefeld,3 +bier,1 +biesiada,2 +big,1 +big 3,1 +big band,55 +big band sunday,1 +big bands,5 +big beat,4 +big kickin 103fm,1 +big radio,1 +big room,3 +big room house,1 +bigben,1 +bigfm,1 +biggpt,1 +biglayla,1 +bigroom,5 +bih,2 +bikers,1 +bilbao,2 +bild,2 +bildung,1 +bildungsfernsehen,1 +bilingual,4 +billboard,1 +billings,2 +billy joel,1 +binghamton,2 +biondini,1 +bioneers,2 +birmingham,5 +bischofswerda,2 +bisexual,2 +bitcoin,1 +bitterfeld wolfen,1 +bizzare,1 +black,40 +black doom metal,1 +black history,1 +black metal,23 +black music,10 +black sabbath,1 +blackened doom,1 +blackgaze,1 +blacksburg,1 +blagues,2 +blair,1 +blairmore,1 +blanes,1 +blankenburg,4 +blankenheim,4 +blanschwitz,4 +blasewitz,2 +blasmusik,10 +blaugrana,1 +bleeplove,1 +blenheim,1 +blind,10 +blind radio,2 +blindness,2 +blink-182,1 +blk tv,1 +bloemfontein,2 +bloomfield hills,1 +blountville,1 +blue eyed soul,1 +bluegrass,31 +bluemars,2 +blues,263 +blues music,1 +blues rock,54 +blues-rock,1 +bluesrock,3 +bluestate country,1 +bluewater,1 +bluffton,1 +bnagladesh,1 +bnr,4 +bnsf,1 +boario terme,1 +bob,1 +bob dylan,3 +bob's,1 +bobritzsch-hilbersdorf,1 +boca,2 +boca del río,20 +bochum,1 +bockau,1 +bodio lomnago,1 +bogota,3 +bogotá,4 +bois blancs,1 +boise,2 +bojnice,1 +bolero,16 +boleros,10 +bolivia,1 +bolivian music,1 +bollywood,49 +bollywood classics,1 +bollywood music,1 +bologna,4 +bolzano,2 +bomba,1 +bon jovi,3 +bondi cultural,1 +bongo flava,1 +bonn,1 +bonnyville,1 +boogie,9 +boogie rock,1 +boogie woogie,2 +boogie-woogie,1 +book,1 +books,3 +boom fm,1 +bop,9 +borgholzhausen,1 +borgo san michele,2 +borna,2 +borna-heinersdorf,1 +bornstedt,4 +borovichi,1 +borsdorf,2 +bosna,5 +bosnia,2 +boss radio,1 +bossa,5 +bossa nova,25 +bossanova,5 +bosse,1 +boston,19 +boston blues,1 +boston symphony orchestra,4 +boulder,1 +boulder creek,1 +bournemouth,3 +bouyon,2 +bowling green,8 +bowling green ky,1 +boy band,1 +boysband,1 +bozeman,1 +bozie slovo,1 +bpm,1 +bpm sports,2 +br,3 +br fernsehen,2 +br nord,1 +br süd,1 +br-alpha,1 +bradford,1 +brahms,1 +braidwood,1 +bram,1 +bramfm,1 +brand-erbisdorf,1 +brandenburg,1 +brandenburg aktuell,1 +brandis,2 +brandnew,1 +brasil,20 +brasileira,3 +brasilia,3 +brass,6 +bratislava,1 +braunsbedra,4 +braunsroda,4 +bravefm,3 +bravo,1 +brawley,1 +brazil,6 +brazilian jazz,2 +brazilian music,53 +brazilian pop,11 +brazilian rock,5 +braşov,1 +brc,1 +brc bauchi,1 +breakbeat,24 +breakbeat hardcore,4 +breakcore,3 +breaks,15 +breakz,1 +breakzfm,1 +brega,3 +brehna,4 +breitenbrunn,1 +breitenstein,4 +breizh,3 +bremen next,1 +bressanone,1 +bretagne,2 +bretnig-hauswalde,2 +breton,2 +brezhoneg,1 +bridgewater,1 +briesnitz,2 +brighton,1 +brill building,1 +brisbane,5 +bristol,3 +brit pop,11 +britische beatband,1 +british,1 +british blues,4 +british comedy,5 +british folk rock,1 +british forces,5 +british invasion,4 +british metal,4 +british psychedelia,3 +british punk,1 +british rock,2 +britney spears,1 +britpop,14 +brizno,2 +broadcasts of various conferences,1 +broadway,14 +broilers,1 +broken beat,7 +broken beats,1 +broken bow,1 +brokered programming,4 +brony,8 +brooklyn,3 +brooks,2 +brookville,2 +brownsville,6 +browserfriendly,2 +bruce springsteen,4 +bruk,3 +bruno mars,1 +brutal death metal,1 +bruxelles,1 +bryan adams,3 +brã¼cken-hackpfã¼ffel,2 +brücken-hackpfüffel,2 +btuner,1 +bubblegum,1 +buble,1 +bucaramanga,2 +bucha,4 +buckeyes,1 +buddhism,6 +budots,2 +buenisiima,11 +buenos aires,14 +bues,1 +buffalo,7 +bulgarian,2 +bundesliga,1 +bundesregierung,1 +bundestag,3 +bundestagsdebatten live,1 +bura multimedia,2 +burgenlandkreis,1 +burghausen,2 +burgliebenau,4 +burgscheidungen,4 +burgstädt,1 +burgã¶rner,2 +burgörner,2 +burkau,2 +burkburnett,1 +burkhardtsdorf,1 +burlington,7 +business,18 +business news,21 +business programs,4 +busselton,1 +busseto,1 +buten un binnen,1 +buxton,1 +buzzkill,1 +by lost identity,1 +byzantine,9 +bã¶llberg,2 +bã¼rgel,2 +bã¼rgermedien,2 +bã¼rgerradio,2 +bã¼schdorf,2 +bärenstein,1 +bésame,1 +bíblica,4 +böhlen,2 +böhlitz-ehrenberg,2 +böllberg,2 +bürgel,2 +bürgerfernsehen,1 +bürgerfunk,1 +bürgermedien,8 +bürgermedium,1 +bürgerradio,24 +büschdorf,2 +c-pop,1 +c-span,1 +c2cam,2 +c64,6 +ca,1 +cabale,1 +caballo,1 +cabaret,4 +cabo mil,1 +caborca,2 +cadena,2 +cadena azul,1 +cadena rasa,21 +cadenaser,1 +cadence,1 +cadiz,2 +cafe,9 +cafe background music,1 +café romántico radio,1 +cagliari,3 +cahnsdorf,4 +caipira,3 +cairns,2 +cajun,3 +calafell,1 +calaveras,5 +calcio,2 +caldera,1 +caleta olivia,1 +calexico,3 +calgary,13 +cali,1 +california,25 +california medios,1 +call-in,2 +call-out culture,9 +callin,2 +callins,1 +calm,12 +caltagirone,1 +calverton-roanoke,1 +calvillo,2 +calypso,6 +camarillo,1 +cambridge,1 +camburg,4 +camden,1 +campana,1 +campbellton,1 +campeche,13 +campeche city,2 +campeira,1 +camping,1 +campirano,1 +campo san martino,1 +campobasso,2 +campursari,1 +campus,1 +campus radio,51 +camrose,2 +canada,15 +canada 80s,2 +canada's #1,1 +canada’s best comedy and talk,1 +canadian artists,1 +canale generale di intrattenimento,1 +cananea,1 +canarias,1 +canberra,4 +cancel culture,9 +cancun,1 +cancún,8 +candela,11 +candombe,1 +canelli,1 +canmore,1 +canosa di puglia,2 +cantares,1 +canterbury,1 +canton,1 +cantonese,3 +cantopop,2 +canzoni,3 +canzoni d'amore,1 +canzoni romantiche,1 +canção nova brasil,1 +cap-aux-meules,1 +cape breton,1 +cape girardeau,1 +capital,1 +capital digital,1 +capital fm,1 +capital fm kosovo,1 +capital media,7 +caporal,1 +capri,1 +caprice,9 +caprice radio,4 +capris obala je zakon slo slovenski slovene slovenian,1 +caracas,1 +caracol radio,1 +caravaca,1 +carblanez33,1 +carbondale,2 +carbonear,1 +cardonal,1 +caribbean,13 +caribbean music,14 +caribbean rhythm,2 +caribe,1 +caribe fm,1 +caribeño,1 +carlos barbosa,1 +carlyle lake,1 +carnatic,2 +carnaval,8 +carnival,2 +carolina hurricanes,1 +carols,1 +carpi,3 +carrboro,1 +cartoni animati,1 +cartoon network,1 +casarano,1 +casekirchen,4 +casino,3 +cassette tape,1 +cassic american top 40 with casey kasem,1 +cassino,1 +cast recordings,2 +castelletto d'orba,1 +castelnovo ne' monti,1 +castilla y león,2 +castingshows,1 +castle rock,1 +castlefm,1 +castries,4 +castrovillari,2 +casual,1 +cat stevens,1 +catalunya,3 +catanduanes,2 +catania,1 +catanzaro,1 +cathedral,1 +catholic,157 +catholic radio,21 +catholic talk,5 +catholique,3 +catolic,1 +catolic tradition,2 +catolica,19 +catolicidad,1 +catolicismo,1 +catolico,1 +católica,4 +cauerwitz,4 +cave city,1 +caxcán fm,1 +cbc,3 +cbc music,1 +cbc radio 2,1 +cbn,2 +cbn maceió,1 +cbs,1 +cbs sports radio,8 +cbsrmt,1 +cc,1 +ccm,7 +ccrdn,1 +cctv,1 +cdd_fm,1 +cdmx,126 +cdr,2 +cdu spd fdp afd linke grãœne bsw,2 +cdu spd fdp afd linke grüne bsw,7 +cebu,1 +cecina,2 +cedar rapids,2 +cedarville,1 +celaya,5 +celebration,1 +celebrities,1 +celine,1 +celine dion,2 +cello,2 +celtic,26 +celtic rock,1 +center moriches,1 +central coast,3 +central de radios,2 +central texas college,1 +centroamérica,16 +cerignola,1 +cerler,1 +cernusco sul naviglio,1 +cesinali,1 +chachacha,1 +chalco,1 +chalga,3 +chamber,8 +chamber music,6 +chamber pop,1 +champaign,1 +champeta,9 +chania,1 +chann,1 +chanson,16 +chanson française,5 +chansons,5 +chansons françaises,31 +chant,2 +chapel hill,3 +charitable,1 +charity,1 +charivari,3 +charleston,2 +charleston wv,1 +charlevoix,1 +charlotte,5 +charlottesville,1 +charlottetown,4 +charme,1 +chart,1 +chart-hits,3 +chartbreaker,1 +charts,172 +chartshow,1 +chat,1 +chata,1 +chatham,3 +chatham-kent,1 +chatolic,3 +chañaral,1 +che guevara,1 +cheboksary,1 +checkeins,1 +chekiang,8 +chemnitz,6 +chennai,1 +cherepovets,1 +cherie fm tours,1 +cherokee,1 +chesapeake,1 +chetumal,7 +chetumal fm,1 +chełm,1 +chh,1 +chiapas,21 +chic,1 +chicago,29 +chicago blues,5 +chicago sports,1 +chicago weather radio station kwo-39 service lake,1 +chicha,13 +chicha ecuatoriana,4 +chiemgau,1 +chiemsee,1 +chihuahua,72 +chihuahua city,15 +chil,1 +chilcothe,1 +child,10 +children,98 +children music,4 +children tales,1 +children tv,1 +children's music,1 +childrens music,14 +chile,10 +chill,133 +chill beats,6 +chill house,11 +chill out,4 +chill-out,2 +chillaut,1 +chilled,4 +chilled trap,6 +chillhop,8 +chillhouse,2 +chilli,3 +chilliwack,2 +chillout,342 +chillout radio,2 +chillout+lounge,45 +chillstep,6 +chillsynth,4 +chillwave,5 +chilpancingo,7 +china,2 +chinese,13 +chinese classical,1 +chinese pop,7 +chiptune,18 +chiptunes,9 +chirstian,4 +chisasibi,1 +chitra,1 +choir,8 +choluteca,1 +chopin,3 +choral,13 +chorale,1 +chorus,1 +chotis,1 +chr,18 +chrdn,1 +chrismas,2 +christ,1 +christan,1 +christelijk,2 +christiam,1 +christian,925 +christian blues,1 +christian commentary/messages,1 +christian contemporary,80 +christian country,2 +christian dance,1 +christian dance music,2 +christian edm,2 +christian folk,1 +christian hip hop,4 +christian hip-hop,1 +christian hit,1 +christian hits,1 +christian hymns,1 +christian indie,1 +christian lounge,1 +christian lounge music,1 +christian metal,2 +christian modern rock,1 +christian music,46 +christian orthodox,15 +christian pop,3 +christian praise,1 +christian praise&worship,33 +christian preaching,2 +christian punk,1 +christian r&b,1 +christian radio,1 +christian rap,3 +christian reggae,1 +christian rock,14 +christian talk,11 +christian talks,2 +christian teaching,5 +christian top40,1 +christian worship,1 +christian-gospel,25 +christianclassicrock.net,1 +christianhardrock.net,1 +christianity,6 +christianpowerpraise.net,2 +christianrock.net,1 +christiansted,2 +christlich,5 +christliche musik,4 +christmad,1 +christmas,80 +christmas carols,1 +christmas music,169 +christmas rock,2 +christmas songs,4 +christus,1 +chroniques,1 +chrsitian,1 +chrétien,44 +chtistian,1 +chuck berry,1 +church,14 +church of christ,1 +church radio,1 +church services,1 +chuvash,1 +cidade,1 +cihuatlán,1 +cincinatti,5 +cincinnati,4 +cinco radio,5 +cine,2 +cinema,14 +cinema music,1 +cinematic,5 +circuit house,1 +cistriana,1 +città di san marino,3 +city,1 +city church,1 +city fm 89 islamabad,1 +city pop,4 +citypop,5 +ciudad acuna,3 +ciudad acuña,10 +ciudad camargo,1 +ciudad de buenos aires,2 +ciudad de mexico,58 +ciudad de méxico,173 +ciudad del carmen,8 +ciudad guzmán,3 +ciudad juarez,1 +ciudad juárez,22 +ciudad mante,1 +ciudad mexico,49 +ciudad obregón,4 +ciudad serdán,1 +ciudad victoria,3 +ciudadana 660,1 +ciudaddemexico,1 +cividate al piano,1 +civil rights,1 +ciência,1 +clanton,1 +clarenville,1 +clarholz,1 +clarion,2 +clarksville,1 +clasic rock,3 +clasica,3 +clasicos,46 +clasicos en ingles,31 +clasics,2 +classi blues,1 +classic,142 +classic hits,1 +classic - instrumental,2 +classic 90s,7 +classic alternative,7 +classic blues,8 +classic british rock,1 +classic country,56 +classic crossover,1 +classic dance,6 +classic disco,2 +classic fm,1 +classic folk,4 +classic hip hop,22 +classic hits,836 +classic hits 60s 70a 80s,19 +classic hits 80's 90's,4 +classic hits 80s,1 +classic house,1 +classic italian pop,13 +classic jazz,41 +classic metal,5 +classic mix,1 +classic music,3 +classic pop,13 +classic r&b,1 +classic radio drama,1 +classic rock,723 +classic rock 50's 60's 70's 80's 90's,2 +classic rock hits,1 +classic rock music,3 +classic rock oldies,13 +classic rock oldies soul,6 +classic rock; alternative rock,2 +classic rock; alternative rock indie,10 +classic salsa,2 +classic songs,1 +classic soul,16 +classic/dance/pop-rock,6 +classic/dance/pop-rock (e. g. rolling stones,1 +classical,1700 +classical 24,1 +classical baroque,12 +classical california,8 +classical choral,3 +classical guitar,11 +classical mozart,2 +classical music,111 +classical organ music,5 +classical piano,22 +classical sleep,1 +classical vocal,1 +classick hits,1 +classics,115 +classics hits,1 +classics oldies hits,1 +classique,5 +claußnitz,1 +clavier,1 +clean,1 +clearchannel,2 +clearfield,2 +clemson,1 +cles,1 +cleveland,4 +cliff richard,1 +clips,1 +clitch,1 +cloud hip-hop,1 +cloud rap,1 +clouddudler,2 +club,114 +club dance,61 +club dance electronic house trance,24 +club hits,6 +club house,32 +club house trance dance,5 +club radio,1 +club sounds,2 +club tracks,1 +clubbing,33 +clásica,3 +clásica romántica,1 +clásicos,96 +clásicos en español,47 +clásicos en español e inglés,11 +clásicos en inglés,40 +clássica,1 +cnbc,1 +cnn,2 +cnr,1 +co-op,1 +coacalco,1 +coachella,1 +coahuila,45 +coalcomán,1 +coalgate,1 +coalville,1 +coast to coast am,6 +coastalaska,1 +coaticook,1 +coatzacoalcos,3 +cobians,1 +cobourg,1 +cochabamba,1 +coeur d'alene,1 +cold lake,1 +cold wave,1 +colditz,2 +coldplay,2 +coldwater,1 +colectivo cultural,2 +colegio,1 +colgne,1 +colima,9 +colima city,3 +collaboratif,1 +college,36 +college - virginia tech,1 +college radio,165 +college rock,6 +collingwood,1 +cologne,4 +cologne kölsch,1 +colombia,8 +colombia vallenata,1 +colombian music,7 +coloradio,7 +coloxtra,2 +columbia,3 +columbus,1 +combo,40 +combos,13 +comedia,1 +comeduy,1 +comedy,67 +comedy scene,3 +comercială,2 +comisión de radio y televisión de tabasco,3 +comitán,7 +commentary,6 +commercial,269 +commercial free,9 +commercial-free,35 +commercials,1 +commodore 64,2 +common,1 +community,204 +community alternative,1 +community everett washington,1 +community messages,1 +community news,11 +community politics music radical,1 +community politics music radical brisbane,1 +community radio,823 +community radio - seniors,2 +community radio for renfrewshire,1 +community radio station,3 +community supported radio station,5 +communiy,1 +complextro,1 +compos,1 +compostela,1 +compton,1 +comunicaciones jfj,1 +comunicación,1 +comunismo,2 +comunitaria,29 +comunitaria cooperativa,2 +comunitario,16 +comunity,17 +comunitária,15 +concert,7 +concert band,2 +concert recordings,1 +concesión comunitaria,2 +concesión pública,3 +concesión social,25 +conco,1 +concretepop,1 +conexión,1 +confirmation bias,9 +conjunto arpa grande,1 +conjuntos,2 +connewitz,2 +conservative,19 +conservative talk,24 +conspiracies,7 +conspiracy,11 +conspiracy theories,16 +constitutionalists,2 +contemporary,83 +contemporary adult,1 +contemporary christian,18 +contemporary christian music,2 +contemporary classical,7 +contemporary country,6 +contemporary disco,1 +contemporary folk,2 +contemporary hit radio,22 +contemporary hits,169 +contemporary hits radio,79 +contemporary jazz,11 +contemporary music,4 +contemporary pop rock,9 +contemporary r&b,9 +contemporary reggae,1 +contemporary rnb,6 +contemporary variety and current affairs,2 +contemporary vocal,1 +contemporary;rock,1 +contemporay christian,1 +contenmporary,1 +content,1 +conteúdo,1 +conversano,1 +conversation,3 +convoy network,1 +cook,1 +cool,10 +cool jazz,24 +cool music seduction,2 +coon rapids,1 +copainalá,1 +cope,2 +copertino,1 +copiapo,1 +copiapó,1 +copy,1 +copyleft,1 +coran,2 +corato,1 +coraxfunk,1 +cordoba,6 +core,1 +corea,1 +corigliano calabro,1 +cornellà de llobregat,1 +corner brook,2 +cornwall,9 +corporación bajío comunicaciones,2 +corridos,4 +corridos cumbia 70 80 varios,2 +corrientes,2 +corrèze,1 +corsano,2 +cortez,1 +corus media,6 +corus radio network,1 +corán,1 +cosenza,1 +cosmic,2 +cosmic house,1 +cosmoradio,2 +cossebaude,2 +costa rica,7 +coswig,2 +cotelo,1 +cotta,2 +count,1 +countdown,1 +counterspin,2 +country,631 +country and western,3 +country blues,7 +country folk,2 +country gospel,1 +country music,47 +country music 80s,1 +country pop,15 +country rock,18 +country-kanal,1 +country-rock,2 +county,1 +county music,1 +coupé-decalé,1 +cover,3 +covers,17 +cow punk,1 +cowboy,1 +cowboys,1 +cozumel,2 +cplp,1 +cpop,2 +cppdn,1 +crackpot,1 +crdn,1 +creación sonora de la revuelta de octubre,1 +cream,16 +creative commons,10 +creedence clearwater revival,1 +crente,1 +creo radio,1 +creole,4 +crested butte,1 +crestin,1 +cretan,1 +cretan music,1 +crete,2 +crianças,1 +cricket,1 +crime,6 +cringe,1 +cristi,1 +cristian,9 +cristiana,79 +cristianas,3 +cristianismo,3 +cristiano,5 +cristo,3 +cristocéntrica,2 +cristocéntrico,1 +cristã,3 +cristão,1 +croatia,2 +croatian,7 +croatian pop music,2 +croatian programming,2 +crooner,12 +crooner radio,6 +crooners,23 +cross over,1 +crossen an der elster,4 +crossover,93 +crossover jazz,2 +crossover prog,1 +crosstalk,7 +crottendorf,3 +crunk,1 +crust,3 +cryptocurrency,1 +crystal,2 +cspan,1 +cuarteto,23 +cuatro media,1 +cuauhtémoc,6 +cuautla,2 +cuba,2 +cuban,7 +cuban jazz,1 +cuban music,7 +cubatón y ➕,1 +cubelles,1 +cueca,2 +cuernavaca,12 +culiacan,2 +culiacán,30 +cultura,50 +cultura am,1 +cultura popular,1 +cultural,96 +cultural mexico,9 +cultural news,47 +cultural programming,4 +cultural reports,2 +culture,287 +culture beat,1 +cumbia,205 +cumbia baladas bachata nacional varios,1 +cumbia corridos baladas varios,1 +cumbia ecuatoriana,12 +cumbia folklor baladas varios,2 +cumbia peruana,16 +cumbia salsa tecno 70 80 baladas varios,3 +cumbia total,1 +cumbias,28 +cuneo,2 +cunewalde,2 +cuny,1 +cuorgnè,1 +curación,1 +curent affairs,1 +curitiba,2 +current affairs,87 +current affairs|news,1 +current affairs|sports,1 +current bands,1 +current events,3 +current focal points,1 +current hits,2 +current jazz,1 +curry,2 +cutten,1 +cx12 radio oriental 770 am,1 +cyberpunk,8 +cyprus,3 +cz,1 +cz sk,3 +czech folk music,3 +czech rock,3 +cádiz cf,1 +cã¶lleda,2 +córdoba,16 +cölleda,2 +d&b,4 +d'n'b,1 +d-day,1 +d95,4 +d96,1 +d99,5 +dab+,16 +dace,1 +daily,1 +dakwah,1 +dakwah islamiah,3 +dalian city,2 +dallas,6 +dalmatia,3 +damar,2 +damlalar,1 +dancdhall,1 +dance,1203 +dance & electronic,2 +dance & electronicdiscoeurodanceitalo-discopoptran,1 +dance 80s,3 +dance 90s,1 +dance charting,1 +dance classics,2 +dance etc,1 +dance hall,2 +dance hits,4 +dance house club electronic techhouse,6 +dance house club electronic techhouse,1 +dance house club electronic trance 90s,1 +dance music,58 +dance musik,1 +dance pop,50 +dance punk,1 +dance radio,2 +dance radio online,3 +dance rock,6 +dance rock classic,2 +dance smashes,2 +dance top40,14 +dance-pop,2 +dance/dance-pop,2 +danceable hits,1 +dancecore,1 +dancefloor,8 +dancefloor house,1 +dancehall,45 +dancehits,1 +dangdut,16 +daniel o'donnell,1 +danish,5 +danmarks radio,2 +dannemora,1 +dansk,1 +danske,1 +danzig,11 +danzon,3 +danzones,1 +darbam,1 +dark,14 +dark & wave classics,1 +dark ambient,7 +dark cabaret,1 +dark electro,4 +dark electronica,1 +dark folk,2 +dark house,1 +dark jazz,1 +dark knight,1 +dark music,1 +dark progressive,1 +dark psy,1 +dark psytrace,1 +dark psytrance,1 +dark techno,6 +dark wave,8 +darkfolk,1 +darkpsy,4 +darkstep,1 +darksynth,6 +darktechno,1 +darkwave,36 +darkwave; ebm; futurepop; gothic; electropop; synth-pop; alternative,2 +dartington,1 +dartmouth,1 +darusalam,1 +darwin,2 +das beste von heute,1 +das erste,2 +das junge infoprogramm von deutschlandradio,2 +dash,82 +dashradio,83 +dashville,2 +datawave,1 +datenkollektiv dresden,1 +dave mishkin,1 +david bowie,1 +david hormachea,2 +davis,2 +day dee,1 +dayton,1 +daytona beach,3 +dazzling music,2 +daños de las religiones,1 +dd,1 +ddr,7 +ddr musik,2 +ddr radio,2 +ddr-musik,1 +ddt,2 +dean,1 +dean martin,2 +dearborn heights,1 +death,1 +death metal,25 +death-doom metal,1 +death-metal,1 +deathcore,7 +deathrock,1 +debate,8 +debates & interviews,1 +debates y deportes,34 +debatten,3 +debussy,1 +decades,105 +decatur,4 +dederstedt,4 +deejay,10 +deejay remix,1 +deep,36 +deep ambient,16 +deep ambient electronic,1 +deep bass,2 +deep house,168 +deep house. acoustic,1 +deep indie,1 +deep music,4 +deep purple,2 +deep space,2 +deep tech house,3 +deep techno,13 +deephouse,10 +def leppard,1 +defcon,4 +defected radio,1 +deftones,1 +dein top40 radio,1 +deinfm,1 +del río,1 +delicias,6 +delphos,1 +delta blues,2 +delta fm,1 +delta fm bage,1 +dembow,5 +demitz-thumitz,2 +democracy,1 +demos,1 +demoscene,11 +denc,2 +denison,1 +denmark,1 +denton,1 +denver,14 +deoria,1 +depeche mode,6 +deporte,24 +deportes,131 +deportiva,6 +der dunkle parabelritter,1 +derechos humanos,4 +dersim,1 +des moines,2 +desert oracle,1 +desi,8 +desi world radio,1 +desi-influenced asian,2 +design,1 +desiring god,1 +desporto,2 +dessau,1 +dessau rosslau,1 +destiny's child,1 +detective,2 +detmold,1 +detroit,6 +detroit house,1 +deus,1 +deutsch,17 +deutsch pop,3 +deutsch rock,4 +deutsch-pop,3 +deutsche,1 +deutsche lieder,8 +deutsche musik,6 +deutsche schlager,4 +deutsche welle,4 +deutscher hip-hop,1 +deutschfm,1 +deutschland,11 +deutschlandfunk,1 +deutschlandradio,2 +deutschneudorf,1 +deutschpop,4 +deutschpunk,1 +deutschrap,12 +deutschrock,18 +deutzen,2 +devotion,1 +devotional,2 +dezbateri diverse,1 +dfm,1 +didgeridoo,1 +die antenne für die olaura-zone,1 +die best hits ever,1 +die geilsten hits,1 +die größten hits,1 +die schwäbisch mögen,1 +die schönsten klassik hits,3 +diera-zehren,2 +diesel-song,1 +dieskau,4 +dieu,1 +digby,1 +digimix 95,1 +digital,3 +digital bhojpuri,1 +digital health,1 +digital radio,2 +digitalisierung,1 +digitalkanal,2 +digitally affected,3 +digitalradio,2 +dil bilgisi,1 +dilettantisch,9 +din,1 +dini,1 +dion,1 +dios,3 +diosas,1 +dioses,1 +dire straits,1 +direct,1 +disco,274 +disco & jazz,1 +disco funk,14 +disco hits,1 +disco polo,26 +disco pop,1 +disco soul,1 +disco-kanal,1 +discofox,32 +discofox oldies,7 +discofox+schlager,15 +discokugel,1 +discomusic 70,4 +discomusic 80,4 +discopolo,4 +discovery bay,1 +discussion,2 +discussion rounds,1 +discussions,2 +diskurs,1 +disney,12 +disney channel,1 +diss,1 +distance learning,1 +divan edebiyatı,1 +diversas,1 +diverse music,1 +diverses,1 +diversity,4 +diversión,1 +divicon media,9 +divicon-piratenmux 7a,1 +divulgacion mexico,1 +dixieland,2 +diócesis sololá-chimaltenango,1 +dj,58 +dj bobo,1 +dj ez,1 +dj gilles peterson,1 +dj henri,1 +dj mag top 100 djs,1 +dj mix,20 +dj mixes,46 +dj radio,3 +dj remix,53 +dj sessions,1 +dj sets,50 +django,1 +djent,2 +djft,1 +djjd,1 +djmix,1 +djpatrys,1 +djs,2 +dk,1 +dlf,2 +dnb,28 +do fundo da grota,1 +doberschau-gaußig,2 +dobritz,2 +doctor arroyo,2 +doctrinal,1 +documentales,1 +documentaries,9 +documentary,6 +documentation,3 +dodgeville,2 +dog,2 +dogmatismus,9 +dokumentation,2 +dokumentationen,2 +dokus,1 +domaća,1 +dominican,1 +dominican republic,1 +domnitz,4 +domusnovas,1 +donna summer,1 +donya,2 +doo wop,1 +doo-wop,6 +doom,5 +doom metal,10 +door county,1 +doowop,2 +dopetrackz,1 +dordogne,1 +dorenko,1 +doris day,1 +dorojnoe,1 +dorset,4 +dos palos,1 +dothan,1 +doubs,2 +doujin,2 +dover,1 +downbeat,6 +downeast maine,1 +downtempo,80 +dr,1 +dr. alban,1 +dram&bass,1 +drama,40 +dream,2 +dream dance,1 +dream house,1 +dream pop,3 +dream theater,1 +dream-pop,3 +dreamboats and petticoats,1 +drebach,1 +dreiländereck,2 +dresden,15 +drgnu,23 +driftless,1 +drill,5 +drittes programm,14 +drive,1 +drive time,1 +drone,18 +drone ambient,1 +droyßig,2 +droyãÿig,2 +drum,1 +drum & bass,36 +drum 'n' bass,18 +drum and bass,77 +drum n bass,6 +drum&bass,11 +drum'n'bass,9 +drum-and-bass,2 +drumandbass,1 +drumfunk,1 +drumheller,1 +drumn bass,5 +drums,1 +dt64,1 +dub,47 +dub reggae,5 +dub techno,7 +dubai,1 +dubbo,1 +dubisthalle.de,1 +dubois,5 +dubrovnik,2 +dubstep,54 +dubtechno,2 +dubtep,1 +dubuque,2 +duddle sack,2 +dudelfunk,1 +duetos,1 +duhovna muzika,1 +duluth,3 +duna média,9 +dunedin,2 +dungeon synth,2 +dunmore,1 +dupage,1 +duplicate sputnik news russia,1 +dupst,2 +dupstep,2 +durango,20 +durango city,4 +durham,6 +dutch,17 +dutch hip hop,1 +dutch house,1 +dutch music,1 +dutch pop,4 +dutch rap,1 +dutch rock,2 +dvorak,2 +dw deutsch,1 +dw english,3 +dw hd,1 +dystopic cinema,1 +dz,1 +dã¶blitz,2 +dã¶lau,2 +décadas,3 +développement,1 +dónde suena tù música 🎵,1 +döbeln,1 +döblitz,2 +dölau,2 +dölitz,2 +dölzschen,2 +dösen,2 +dúos,1 +eagle pass,2 +eagles,1 +early 2000s,1 +early 80's,1 +early 80s,1 +early british pop rock,1 +early hardstyle,1 +early music,2 +early pop rock,1 +early r&b,2 +east coast rap,1 +east lansing,7 +east orange,3 +east stroudsburg,1 +eastcoast rap,1 +eastern passage,1 +easy,27 +easy jazz,1 +easy listening,318 +easy listning,3 +easy music,3 +easy pop,1 +easy rock,1 +easy sheet music,1 +ebersbach,3 +ebersdorf,1 +ebm,42 +ebsm,1 +ecclesia,19 +echo chamber,9 +echte volksmusik,1 +echtepiraten,1 +eckartsberga,4 +eclectic,207 +eclectic audio entertainment.,1 +eclectic prog,4 +eclectic programming,24 +eclectic seasonal talk otr ska ebm,1 +ecletcic,1 +ecletic,2 +ecletica,6 +eclética,20 +eclético,1 +eclético radioweb guarulhos,1 +ecole,1 +ecología,1 +economia,3 +economic,4 +economic news,13 +economics,47 +economy,4 +ecuador,3 +ecuador quito,1 +ecumenical,1 +ed sheeran,3 +edebiyat,1 +edgar street,1 +edificante,4 +edinburg,1 +edinburgh,2 +edm,214 +edm podcast,1 +edm radioshows,6 +edmond,1 +edmonton,11 +edson,1 +eduardo galeano,1 +educacao,1 +educacion radio tecnica,1 +educacional,4 +educació,1 +educación,8 +education,52 +educational,57 +educativa,11 +educação,1 +edukation,1 +edwardsville,1 +eglise,1 +ego,1 +egofm,18 +egopure,1 +egypt,7 +egyptian songs,1 +ehr,17 +ehrenfriedersdorf,1 +ehringsdorf,4 +eibenstock,1 +eiffel 65,1 +eighties,11 +einsfestival,1 +einsiedel,1 +eirodance,1 +eis essen,1 +eisleben,4 +el,1 +el exámetro,10 +el fonógrafo,8 +el fuerte,1 +el heraldo radio,7 +el kuino fm,1 +el lobo,2 +el panda zambrano,2 +el paso,18 +el patrón,2 +el salto,1 +el salvador,1 +el sicuicho,1 +el socorro,1 +el vendell,1 +elbland,1 +elecro,1 +elecrto,1 +electra,2 +electric,2 +electric blues,3 +electric guitar,1 +electric light orchestra,1 +electro,242 +electro deep,4 +electro funk,3 +electro hop,1 +electro house,44 +electro industrial darkwave ebm futurepop synthpop goth,2 +electro latino,1 +electro pop,3 +electro rock,1 +electro sets,2 +electro sounds,1 +electro swing,5 +electro techno,11 +electro-pop,3 +electroacoustic,3 +electroclash,2 +electroindustrial,1 +electrolatino,1 +electronic,783 +electronic & alternative,1 +electronic beats,6 +electronic chill-out,1 +electronic dance music,76 +electronic deep,1 +electronic game music,1 +electronic music,29 +electronic rock,2 +electronica,140 +electronica-driven chillout vocals,1 +electronics,1 +electropop,36 +electroswing,1 +electrónica,5 +electrónico,1 +elektro,14 +elektronik,12 +elektronische musik,2 +eletronic,1 +eletrônica,2 +elevator music,2 +elite dangerous,1 +ella fitzgerald,1 +elmhurst,1 +elsteraue,4 +elsterheide,2 +elstertrebnitz,2 +elstra,2 +elterlein,1 +elton,1 +elton john,1 +elvis presley,5 +em sintonia com o seu bom gosto,1 +emanzipation,1 +embutikizi,2 +emergency & public safety,10 +emilautist,2 +eminem,1 +emisora internacional,1 +emisora virtual,2 +emisoras unidas,1 +emission,2 +emo,8 +emocore,2 +emotion,2 +empoli,2 +ems scanner,5 +en perspectiva,1 +en todas partes,41 +en vivo,1 +encouragement,3 +energetic,1 +energia,1 +energy,4 +engels,1 +engelsdorf,2 +engenho do meio,1 +england,1 +englehart,1 +english,31 +english language,1 +english rap,1 +english worship radio,1 +enjoy,2 +enka,1 +enkintabuli kyo muziki,2 +enschede,1 +ensenada,8 +entdecken,1 +entertaiment,1 +entertainer,1 +entertaining segments,1 +entertainment,294 +entravision communications,2 +entrepreneurship,1 +entretenimento,3 +entretenimiento,1171 +entreterimento,1 +entrevistas,22 +entschleunigen,1 +entspannen,2 +environment,3 +environmental justice,2 +environmental news,2 +environnement,1 +enya,1 +ephraim,1 +ephrata,1 +epic,5 +epic metal,1 +epistemología,1 +epoca de oro,1 +era spor,1 +era sport,1 +eraspor,1 +erba,1 +erfenschlag,1 +erfurt,3 +erholen,1 +eric clapton,1 +erin,1 +eritrea,1 +erlangen,1 +ermsleben,4 +erotic,2 +erotik,1 +err,4 +erstes programm,1 +ert,31 +ert deftero,1 +ert kosmos,1 +ert proto,1 +ert trito,1 +erzgebirge,3 +es,1 +esanyu,1 +escepticismo,1 +escobar,1 +escuinapa,1 +escéptico,1 +esd,2 +eskalation,1 +esne radio,1 +espacio deportivo,1 +espanol,2 +españa,6 +español,610 +especial mix,2 +espectáculos,3 +esperanto,1 +espiritismo,2 +espiritual,21 +espn,8 +esporte,1 +esportes,10 +esportes futebol,7 +esports,1 +espérance,1 +espírita,1 +esqu,1 +esquina 32,2 +essay,1 +essequibo,1 +essex,3 +estación,1093 +estado de mexico,19 +estado de méxico,37 +estallido social,2 +estereo clase,1 +estereo romance,1 +estonia,2 +estudiantes,1 +estudio 101.9 fm,2 +estéreo digital,1 +estéreo gallito,1 +estéreo pop,1 +estério vida,1 +etchojoa,1 +ethereal wave,1 +etherpiraten,10 +ethiopia,6 +ethnic,33 +ethnic fusion,1 +ethnic house,1 +ethnic programming,14 +ethnographic film,1 +etnic,4 +etno,8 +etudiante,1 +eugene,5 +euphoric hardstyle,1 +eureka,1 +euro,7 +euro disco,20 +euro hits,8 +euro house,1 +euro pop,1 +euro-pop,1 +eurobeat,5 +eurobodalla,1 +eurobodalla shire,1 +eurodance,134 +eurodisco,4 +eurohit,3 +europa,2 +europa park,1 +europa plus,1 +europawelle,2 +europe,23 +european,11 +european music,7 +europop,5 +eurovision,5 +eurovision song contest,5 +eurythmics,1 +eutritzsch,2 +evangelica,1 +evangelical,9 +evangelical christian,4 +evangelio,41 +evangelo,1 +evangile,3 +evangélica,8 +evanston,1 +evansville,3 +eventkanal,4 +eventlivestream,4 +events,4 +everett,1 +evergreen,7 +evergreen state college,1 +evergreens,19 +ex-yu,2 +ex-yu music,2 +exa,101 +exa corazón,1 +exa fm,101 +exa gaming,2 +exa k-pop,1 +exclusive,1 +exclusively,1 +exitos,14 +exmore,1 +exotic,1 +exotica,1 +expat,3 +experimental,95 +experimental electronic music,4 +experimental hip-hop,1 +experimental radio,2 +experimental rap,1 +experimental rock,3 +experimental techno,2 +experimental/avant garde,1 +experimentell,1 +explicit,21 +explict,2 +extra,4 +extreme metal,1 +extremist,7 +extremo,1 +extremo fm,1 +exyu,6 +eygpt,1 +eyo'bujajja,1 +eğitim,1 +f.c. barcelona,2 +facebook,2 +factor,1 +fado,9 +fahimta,1 +fairbanks,2 +fairbury,1 +fairfield,3 +fairport,1 +fairytales,6 +faith,8 +faith ignited,1 +falk,1 +fallout,9 +falmouth,1 +familia,9 +familiar,2 +familie,1 +family,22 +fan music,8 +fandangos,1 +fandom,1 +fania,1 +fantasma negro,1 +fantastic,3 +fantasy,9 +far north queensland,2 +fargo,7 +farin urlaub,1 +farming,1 +farnstã¤dt,2 +farnstädt,2 +faroe islands,3 +farsi,2 +farsça,1 +farándula,1 +fasching,1 +fashion,7 +fashionable,2 +fav,3 +favourite throwbacks,1 +fayetteville,4 +feature,1 +features,49 +federal radiochanel,1 +fediverse,9 +feel good,3 +feelings,1 +feierabend,1 +feiern,2 +felipe carrillo puerto,1 +felix jaehn,1 +female musicians,1 +female vocalists,17 +female vocals,14 +female voices,5 +femalevocals,5 +femininity,2 +feminism,1 +feminismus,1 +feminist,7 +feminista,2 +ferarock,2 +feria,1 +fermont,1 +ferndale,2 +fernsehen,5 +ferrovie dello stato,1 +festival,6 +festival sanremo,1 +festivales,2 +festivales de jazz,3 +festivals,9 +festive fifty,1 +festtagsmusik,1 +fetenhits,1 +feuilleton,2 +ff,1 +ffe munyo,1 +ffe mwe,1 +ffh,1 +fichtelberg,1 +fictional news,1 +fides,1 +fidget house,1 +fiducial,1 +fiel,1 +field recording,4 +field recordings,1 +fiesta,4 +fiesta latina,1 +fiesta mexicana,3 +fiestalatina,1 +fifties,1 +figueres,1 +fiji,1 +fijian programming,1 +filipino,12 +film,26 +film history,1 +film music,20 +film score,6 +film scores,2 +film talk,1 +filmmusik,3 +filter bubble,9 +finance,7 +finanzas,1 +fine arts,3 +finland,2 +finne,4 +finnish,4 +finsterwalde,4 +fip,6 +fire scanner,10 +firenze,7 +first nations,2 +first punjabi radio,1 +fitchburg,1 +fitness,9 +fitness music,3 +fiumicino,1 +flac,8 +flamenco,12 +flash back,4 +flashback,27 +fleetwood mac,1 +fletcher,1 +flex fm,1 +flint,1 +flo rida,1 +florence,1 +florida,4 +florina,1 +flower power,1 +flute,2 +flöha,1 +fm,943 +fm 89.0,1 +fm 91.75 mhz,1 +fm 94.75,1 +fm 98.5 mhz ลูกทุ่ง ทั้งวัน ทั้งคืน,1 +fm centro,1 +fm globo,20 +fm plus 93.1 rosario,1 +fm radio station,1 +fm simulcast,1 +fm yola,1 +fmmusica romantica,8 +foclore argentino,3 +focus radio,5 +foggia,1 +folcklore,2 +folclor,14 +folclore,48 +foligno,2 +folk,539 +folk classics,1 +folk metal,4 +folk music,74 +folk pop,3 +folk popular am noticias musica,1 +folk rock,28 +folklor,1 +folklore,36 +folkloric,2 +folkmusik,1 +folks,1 +foo fighters,14 +food for your soul,2 +football,33 +football commentary,2 +for and from fugitives,1 +for life,1 +foreign languages,3 +foreign talk,1 +foreigner,2 +forest psytrance,6 +forestpsytrance,4 +forestville,1 +forever,3 +forgotten classics,2 +format change,1 +forro,2 +forró,10 +forró e sertanejo,8 +fort campbell,1 +fort mcmurray,4 +fort myers,1 +fort smith,2 +fort walton beach,1 +fort worth,2 +fort-coulogne,1 +fortaleza,1 +fortyradio,1 +fortín de las flores,7 +foss,1 +fox,3 +fox news,2 +fox sports,1 +framing,9 +francaise,1 +francaise music,1 +francavilla fontana,1 +france,21 +france bleu,1 +france médias monde,3 +franco battiato,1 +francophone,3 +francophone country,3 +frank sinatra,1 +frankenberg,1 +frankfurt,2 +frankfurt am main,2 +frankleben,4 +franklin,2 +frankreich,1 +frap,3 +frauenstein,1 +freak folk,1 +fredericton,3 +free,7 +free fire radio,1 +free fm,6 +free fm rock,1 +free form,1 +free format,1 +free japan music,1 +free jazz,22 +free radio,23 +free software,1 +free-form,1 +freedom,1 +freeform,65 +freeform psytrance,2 +freestate,1 +freestyle,9 +frei,1 +freiberg,2 +freie radios,1 +freie software,1 +freie-radios.net,9 +freies radio,35 +freies radio nkl,1 +freies radio sachsen,1 +freies radio salzkammergut,1 +freirina,5 +freistadt,1 +freizeitpark,1 +french,24 +french chansons,15 +french culture,2 +french hip-hop,2 +french house,1 +french music,16 +french overseas,2 +french pop,3 +french rap,6 +french touch,1 +frenchcore,2 +frent,1 +frequenzfüller,1 +fresco,1 +fresh,22 +fresh air,1 +fresh hits,5 +fresh hits for uganda,1 +fresnillo,7 +fresno,2 +freyburg (unstrut),4 +friedrichstadt,2 +friki,1 +fringe,1 +frisky,1 +friuli,2 +frohburg,2 +frohe weihnachten,1 +frohse,4 +frome,1 +front royal,1 +fsk-hh,1 +ft wayne,1 +fuck,1 +fukien,1 +full hd,1 +full service,87 +full-on,3 +fun,4 +fundación acir,1 +fundamenalist,2 +funeral,2 +funk,244 +funk carioca,1 +funk metal,2 +funk rock,2 +funk2disco,2 +funkhaus halle,4 +funklust,2 +funky,39 +funky 70,3 +funky house,20 +fuori,1 +furci siculo,1 +furlan,1 +furry,4 +fusion,20 +fusion jazz,1 +fusión,3 +fussball,1 +futball,1 +futbol,7 +futebol,13 +future bass,8 +future funk,8 +future garage,2 +future house,11 +future soul,2 +futurefunk,4 +futurepop,13 +futurisme,1 +fuzuli,1 +fuzz,2 +fußball,5 +fácil escucha,2 +fórmula melódica,1 +fúquene,1 +fútbol,3 +g+i=♡,1 +g-funk,1 +gabber,3 +gabberdisco,1 +gaberone,1 +gablenz,1 +gabriel garcia marquez,1 +gachi,3 +gachibass,4 +gachimuchi,4 +gachiremix,4 +gaelic,2 +gafsa,1 +galaxy99,1 +galgenberg,4 +galicia,9 +galiza,6 +gallatin,1 +galway,1 +gambang kromong,1 +game,7 +gamemusic,5 +games,4 +gaming,9 +gaming community,3 +gander,2 +gangst rap,1 +gangsta rap,4 +ganja,1 +garage,24 +garage (1990s),1 +garage house,4 +garage punk,13 +garage punk/psychedelia,2 +garage revival,1 +garage rock,13 +garberville,1 +garmisch-partenkirchen,1 +garth brooks,1 +garzón,1 +gascony,1 +gaspe,1 +gastronomia,1 +gatineau,2 +gaucha,2 +gauchesca,1 +gavardo,2 +gavà,1 +gay,15 +gay club,2 +gay dance,2 +gaydio,1 +gaza,1 +gazal,2 +gazel,1 +gaúcha,4 +gbh,14 +gbn,1 +gdansk,1 +gdl,35 +gedanken,1 +gegen rechts,1 +geheime zender,1 +geheime zender muziek,1 +geheimezender,9 +geiseltal,4 +geithain,2 +gela,1 +gema,1 +gemeinschaft,1 +gemischt,1 +gemischt / mix,1 +gemischt von a-z,1 +gen-x,1 +general,55 +general escobedo,1 +general pico,11 +general programming,1 +general programs,1 +generalist,9 +generalista,1 +generation,3 +geneseo,1 +genesis,2 +genesis to revelation,1 +geneva,1 +genge,1 +gengetone,1 +genova,2 +genre-pop,2 +gentlemen drivers,1 +genève,1 +geoblocking?,2 +geografiarock,1 +george handel,1 +george harrison,1 +george michael,1 +georgetown,2 +georgia,2 +georgia tech,1 +georgian bay,3 +gera,1 +gerais,1 +geraldine,1 +geras fm,1 +geras fm vilnius,1 +gerbstedt,4 +geringswalde,1 +german,30 +german beats,3 +german hip-hop,1 +german pop,13 +german rap,7 +german rock,1 +germany,11 +gers,1 +geschichten,1 +gesellschaft,4 +gesundheitsmagazin,1 +gettysburg,1 +geyer,1 +ghorba,1 +giebichenstein,4 +gig,8 +gisborne,1 +gittersee,2 +glace bay,1 +glam metal,6 +glam rock,22 +glam rock (e.g. t-rex),3 +glastonbury,1 +glaube,1 +glauben,1 +glaucha,4 +glazba,1 +gleina,4 +glenale,1 +glitch,5 +glitch-hop,3 +glitchy synthwave,2 +gliwice,2 +global,21 +global dance music radio station,1 +global news,1 +globalmedia,15 +globo,15 +glösa,1 +gnu,1 +gnu radio,1 +goa,7 +goa trance,2 +goa trance psychedelic trance,3 +gobierno de méxico,2 +gobierno del estado de baja california sur,1 +gobierno del estado de chiapas,1 +gobierno del estado de jalisco,3 +gobierno del estado de méxico,6 +gobierno del estado de nuevo león,6 +gobierno del estado de quintana roo,3 +gobierno del estado de tabasco,3 +gobierno del estado de tlaxcala,5 +gobierno del estado de veracruz,1 +gocedelcev,1 +god,5 +goderich,1 +gohlis,2 +gold,12 +gold coast,2 +gold fm,1 +gold fm radijas,1 +gold hits,1 +golden,2 +golden music,17 +golden oldies,42 +golden valley,1 +goldies,52 +goldwave,2 +good,1 +good news | good sound | good life,2 +good times and greatest hits,2 +google play,1 +goose,1 +goose bay,1 +gop,1 +gopfm,1 +gorbitz,2 +gore metal,1 +gornau,1 +goseck,4 +gosford,3 +goshen,1 +gospel,298 +gospel blues,1 +gospel contemporary,2 +gospel country,2 +gospel dance,2 +gospel edm,1 +gospel eletronica,1 +gospel evangélica,5 +gospel folk,1 +gospel hip-hop,1 +gospel hits,1 +gospel indie,1 +gospel metal,1 +gospel music,20 +gospel pop,5 +gospel praise,1 +gospel punk,1 +gospel r&b,1 +gospel rap,1 +gospel reggae,1 +gospel rock,1 +gospel swahili,1 +gospel top40,1 +gospel worship,1 +gospel/,1 +gostritz,2 +goth,9 +goth rock,1 +gothic,70 +gothic metal,4 +gothic rock,15 +gothic-rock,1 +gothy gloomy synthy dancey stuff,1 +gott,1 +gottfried-keller-siedlung,4 +goulburn,1 +government,34 +government tyranny,1 +govi,1 +govt.,1 +gozoypaz,1 +gpm,6 +gps media,4 +grace to you,1 +graitschen bei bã¼rgel,2 +graitschen bei bürgel,2 +gran fm,1 +granby,1 +grand falls,2 +grand haven,1 +grand lodge,1 +grand ole opry,1 +grand rapids,1 +grand strand,3 +grande cache,1 +grande prairie,1 +grandes males del mundo,1 +grandes éxitos,1 +granger,1 +granollers,1 +grasonville,1 +graus,1 +grays harbor county,1 +grc,1 +grd multimedia,2 +great american songbook,4 +great mix of classic hits,1 +greatest hits,20 +greece,6 +greek,310 +greek basketball league,1 +greek ethnic programming,2 +greek folk,60 +greek folk music,35 +greek hellenic music,2 +greek music,95 +greek pop,81 +greek programming,1 +greek radio,1 +greek rap,1 +greek traditional,21 +green bay,6 +green day,2 +green wave online 106.5 - เพลงดีดีกับความรู้สึกดีด,1 +greenfield,1 +greenland,1 +greenville,2 +greifswald,5 +greiz,2 +grigri,1 +grillen,1 +grime,9 +grimma,2 +grindcore,3 +groitzsch,2 +groningen,1 +groove,37 +groove jazz,1 +groove salad classic,1 +grooves,3 +groovy,3 +grosseto,1 +groton,2 +grottaglie,1 +group harmony,1 +groupthink,9 +großdubrau,2 +großenhain,2 +großgörschen,2 +großharthau,2 +großjena,2 +großnaundorf,2 +großpostwitz,2 +großpörthen,2 +großpösna,2 +großrückerswalde,1 +großschirma,1 +großweitzschen,1 +großzschachwitz,2 +großzschocher,2 +groãÿgã¶rschen,2 +groãÿjena,2 +groãÿpã¶rthen,2 +grubo bm radio,4 +gruna,2 +grunge,53 +grupera,308 +gruperas del recuerdo,3 +grupero,178 +grupo,1 +grupo acir,91 +grupo acustik,3 +grupo alius,2 +grupo as,14 +grupo asva,4 +grupo audiorama comunicaciones,59 +grupo bm radio,5 +grupo cadena,2 +grupo cb,1 +grupo chávez radio,1 +grupo clarín,1 +grupo diario de morelos,1 +grupo digital retroland,3 +grupo encuentro,1 +grupo fm,1 +grupo fórmula,24 +grupo gape radio,1 +grupo garza limón,1 +grupo imagen,4 +grupo kamar,1 +grupo miled,3 +grupo multimedia el diario de coahuila,1 +grupo mundo comunicaciones,2 +grupo olimpica,1 +grupo pazos,14 +grupo peláez domínguez,2 +grupo promomedios,6 +grupo radar,3 +grupo radio alegría,6 +grupo radio carlos c. armas vega,1 +grupo radio carmen,1 +grupo radio cañón,38 +grupo radio centro,40 +grupo radio digital,13 +grupo radio estéreo mayran,3 +grupo radio korita,1 +grupo radio mina,1 +grupo radio palacios,1 +grupo radio zamora,1 +grupo radioasta,1 +grupo radiofónico b-15,3 +grupo radiofónico zer,20 +grupo región,2 +grupo rns,1 +grupo rojaz,2 +grupo rotativo,1 +grupo rsn,16 +grupo siete,11 +grupo sipse,6 +grupo sipse radio,6 +grupo tremor radio,1 +grupo trenu,1 +grupo turquesa,1 +grupo ultra,8 +grupo unidifusión,4 +grupo vibra,4 +grupo vox,3 +grupo vx,2 +grupo zer,15 +grupo zócalo,2 +gruppo radio amore,5 +gröditz,2 +größenwahn,1 +größte pop-hits der 2000er,3 +grüna,1 +grünau,2 +grüne hölle,1 +grünhain-beierfeld,1 +gta,2 +gtrk,1 +guachochi,1 +guadalajara,49 +guadalupe,3 +guadalupe victoria,1 +guaguanco,1 +guagunco,1 +guamúchil,2 +guanajuato,44 +guanajuato city,3 +guasave,6 +guatemala,10 +guatemala city,7 +guaymas,6 +guazapares,1 +gubbio,2 +guelatao,1 +guelph,2 +guerrero,37 +guest mix,1 +guevarista,1 +guitar,23 +guitar jazz,2 +guitar rock,3 +guitare,1 +guitarist,1 +guitarra clasica,2 +guitarra clásica,2 +gujarati,2 +gulf,1 +gunfighter,1 +guns n' roses,6 +guns'n'roses,1 +gunslingers,1 +gurbani,1 +gusle,1 +gute laune,1 +gutenborn,4 +guttau,2 +guyana,1 +gym,1 +gypsy,3 +gã¶tschetal,2 +gã¶ãÿnitz,2 +généraliste,6 +gómez palacio,7 +göda,2 +götschetal,2 +göttingen,1 +gößnitz,2 +gütersloh,2 +güzel,2 +haber,1 +haberler,1 +hablada,82 +hacker,3 +haddaway,1 +hadith,2 +hagerstown,1 +haggatt hall,2 +haifa,1 +hainichen,5 +hair metal,8 +haitian,2 +haitian music,1 +haitian programming,1 +haliburton,1 +halifax,8 +halk müziği,1 +halle,3 +halle (saale),12 +halle leipzig,1 +hallo niedersachsen,4 +halloween,3 +halloween music,1 +ham,1 +ham radio,1 +hamburch,1 +hamburg journal,4 +hamilton,7 +hammerfall,1 +hampton roads,1 +hancock,1 +handball,4 +handmade,1 +hands up,7 +handsup,15 +handsup! rave oldscool 90s 00s 10s trance techno,2 +hannover,1 +hanover,1 +haphop,1 +happy,13 +happy hardcore,18 +happy hits,2 +happy music,11 +happy valley,1 +happy vibes,1 +happyhits,3 +hard,1 +hard bass,1 +hard bassline,1 +hard bop,11 +hard dance,4 +hard house,1 +hard rock,191 +hard rock heavy metal,4 +hard style,1 +hard techno,16 +hard trance,2 +hard-rock,3 +hardbass,3 +hardcore,65 +hardcore rap,2 +hardcore techno,1 +hardrock,13 +hardstep,1 +hardstyle,54 +hardstyle/handsup/edm,5 +hardtechno,3 +harlem,1 +harlingen,1 +harmonica,1 +harp,1 +harpsichord,1 +harrisburg,1 +harsewinkel,1 +hartha,1 +harthau,1 +hartmannsdorf,3 +harz,1 +hasidic,1 +hattrick,2 +hauptprigramm,1 +hauptprogramm,1 +haut-matawinie,1 +haute-savoie,1 +havre,1 +hawaii,4 +hawaiian,4 +hawaiian adult contemporary,3 +hawaiian music,7 +hawaiian pop,2 +hayati inanc,1 +hayati-inanc,1 +hayden,1 +haynsburg,4 +hazelton,1 +hb.hd,1 +hb.low,1 +hd,42 +hd radio,9 +he,1 +healdsburg,1 +healing,13 +health,18 +health programming,3 +heart,2 +heartbeat radio,6 +heartland rock,2 +heave metal,1 +heavy,3 +heavy christmas,1 +heavy metal,159 +heavy metal; wacken; radio bob,1 +heavy power metal,2 +heavy rock,22 +heavy rock (e.g. led zeppelin,5 +heavy-metal,1 +hebden bridge,1 +heber city,1 +hebrew,4 +hedersleben,4 +heide-nord,4 +heide-sã¼d,2 +heide-süd,2 +heidersdorf,1 +heidi,1 +heimat,1 +heiterblick,2 +helbersdorf,1 +helbigsdorf,1 +helbra,4 +helfta,4 +hellenism,1 +hellerau,2 +helsinki,2 +helth,1 +henderson,2 +hendersonville,1 +hendrix,11 +hendrix),5 +heraldo media group,7 +herbert grönemeyer,1 +herceg-bosna,1 +hercegovina,1 +here4ears,1 +hereford,1 +hereford fc,1 +hereford united,1 +herford,1 +hergisdorf,4 +heritage,5 +hermosillo,18 +heroica matamoros,2 +herrengosserstedt,4 +herzebrock,1 +herzklang,1 +hessen,1 +hessischer rundfunk,1 +hettstedt,4 +hevy metal,1 +hfryjh,1 +hi energy,1 +hi-energy,1 +hi-nrg,5 +hi-res,1 +hi-tech,1 +hidalgo,12 +hidalgo del parral,4 +high bitrate,1 +high energy,7 +high enery,2 +high prairie,1 +high quality audio,11 +high school,1 +high school radio,3 +highlands,1 +highlife,2 +hikmet,1 +hilbersdorf,1 +hillsboro,1 +hillsong,2 +hilton head,2 +hilversum,1 +himalaya,1 +hindi,34 +hindi bollywood,4 +hindi latest,1 +hindi songs,3 +hindu,3 +hinduism,1 +hinduismo,1 +hindustani,3 +hindú,1 +hinhören,1 +hinhörkanal,1 +hintergrundberichte,1 +hintergründe,1 +hinton,2 +hip hop,128 +hip hop r&b,2 +hip hop romanian,3 +hip house,1 +hip-hop,153 +hip-hop and alternative music,1 +hip-hop and rap oldies,23 +hip-hop classic,1 +hiphop,275 +hiphop r&b,2 +hiplife,1 +hippie,2 +hippy noise,1 +hirschfeld,5 +hirschstein,2 +historia,2 +historico,2 +history,10 +hit,1 +hit fm,1 +hit music,2 +hit music only,7 +hitfm,1 +hitlist,1 +hitparadenkult,1 +hitradio,1 +hits,1283 +hits 50s,2 +hits 60's,2 +hits 70's,15 +hits 70s 80s,1 +hits 80s,10 +hits 90's,5 +hits 90s,2 +hits fm,7 +hits music,8 +hits oldies,1 +hits pop,3 +hits rock,2 +hits tanpa henti !,1 +hits you love from then to now,2 +hitselection,1 +hitsonly,2 +hkr,1 +hls,12 +hls video,27 +hn,1 +hobart,2 +hobsonville,1 +hochschule merseburg,1 +hochstift,1 +hockey,5 +hogar,2 +hogstory,1 +hohendubrau,2 +hohenhettstedt,4 +hohenmã¶lsen,2 +hohenmölsen,2 +hohndorf,1 +holdenstedt,4 +holiday,36 +holiday music,3 +holiday music (nov-dec),4 +holland,3 +hollands,5 +hollandse hits,1 +holleben,4 +holloween,1 +hollywood,4 +holos,1 +holos svobody,1 +holzhausen,2 +homer,1 +homestuck,2 +honduras,9 +hong kong,7 +hong kong music,1 +honky tonk,6 +honolulu,2 +honor,1 +honsfeld,4 +hoogvliet,1 +hope,3 +hopei,2 +hora exacta,1 +horburg-maßlau,2 +horburg-maãÿlau,2 +horizonte,2 +hormersdorf,1 +hornburg,4 +horror,4 +horror punk,1 +horror rock,1 +horror stories,1 +horrorcore,1 +horse cave,1 +horta sud,1 +hospital radio,8 +hosterwitz,2 +hot,10 +hot ac,44 +hot adult cont,1 +hot adult contemporary,207 +hot country hits,3 +hot hits,27 +hot summer,1 +hot talk,1 +hot-ac,2 +hotel,1 +hothits,1 +houghton,2 +hourly news,2 +house,566 +house - electro - club music - techno,1 +house music,2 +house techno,8 +house/soulful/deep house,5 +houseparty,22 +housetimehd-acc-,1 +housetimelow,1 +houseworld,2 +houston,6 +hoxton,1 +hq,2 +hr,2 +hr-fernsehen,1 +hrt,3 +hrvatska,3 +ht creole,1 +http://ice-the.musicradio.com/capitalxtranationalm,1 +http://mega.netradyom.com:7900/listen.pls,1 +http://stream-uk1.radioparadise.com:80/aac-32,1 +http://stream.laut.fm/radioparty.m3u,1 +http://stream.laut.fm/radioparty.pls,1 +http://stream.zeno.fm/287z97ksdg8uv,2 +http://yayin.netradyom.com:7900/;stream.mp3,1 +https,33 +https://europaplus.ru/,1 +https://stream.0nlineradio.com/schlager?ref=crysta,2 +https://topradio.mobi/screen/1658160063_radio_kame,1 +huasco,2 +huasteca,1 +huauchinango,1 +huayño,1 +hudson,1 +hudson valley,1 +huesca,1 +huila,1 +human rights,2 +humanitarian,1 +humor,25 +humor fm,1 +humor fm minsk,1 +humorist,1 +humour,24 +humourist,1 +hungarian pop music,6 +huntley,2 +huntsville,1 +hupei,8 +hutholz,1 +hyderabad,1 +hydrophone,2 +hymn,1 +hymns,6 +hyperpop,1 +hypertechno,6 +höfchen,1 +hölle (saale),1 +hörbuch 9 und 21 uhr,1 +hören,1 +hörspiel,9 +hörtalk,1 +höxter,1 +i foni tis elladas,1 +i like radio,2 +i love dance first!,1 +i love the sun,1 +iasi,1 +ibarra,1 +ibiza,16 +ibiza club,7 +iconyc,1 +ideas,1 +identitã¤tspolitik,2 +identitätspolitik,7 +idm,19 +idol,3 +idols,2 +ifugao,2 +iglesia catolica,1 +iglesia católica,1 +iglesia universal del reino de dios,1 +iglesias,2 +igreja,1 +iguala,1 +iheart,96 +iheart radio,92 +ilahi,4 +ilfeld,4 +ilha de são tomé,1 +illawarra,1 +illbient,3 +illinois,1 +illinois and lake & porter counties in indiana,1 +ilove dance first!,1 +iloveradio,8 +ilstations,15 +im osten zu hause,1 +imagen,3 +imagen informativa,3 +imagen radio,3 +imagine dragons,1 +imer,24 +immer deine lieblingshits,1 +immerweiter,1 +impactante,5 +imperial,1 +imperial valley,3 +impreza,1 +improvisation,4 +in,1 +in extremo,1 +in the mix,2 +in the morning,5 +in-store,2 +inari,1 +independent,62 +independent artists,1 +independent music,7 +independent news,2 +independent productions and found sound,1 +independent radio,7 +independiente,4 +indi,3 +india,17 +indian,5 +indian classical,1 +indian classical music,2 +indian music,8 +indian pop music,1 +indianapolis,3 +indians,1 +indie,354 +indie dance,3 +indie dance rock,2 +indie electronic,2 +indie folk,4 +indie house,1 +indie music,16 +indie pop,31 +indie rock,113 +indielectronica,1 +indiepop,2 +indierock,1 +indigeneous,3 +indigenous,60 +indigenous current affairs,1 +indigenous music,9 +indigenous radio,2 +indonesia,4 +industrial,57 +industrial music,3 +industrial rock,1 +industrial techno,1 +indy,1 +indépendante,1 +infancia,2 +inferno,1 +influencer,1 +influences,1 +info,26 +info music,1 +info wars,1 +informacion,9 +informacion y musica latina,1 +informaci´,1 +información,63 +informacje,1 +information,507 +informationen,2 +informations,11 +informativa,44 +informativo ntr,14 +informação,2 +infos,2 +infotainment,20 +infowars,2 +ingeniero mashwitz,1 +ingles,19 +inglés,6 +ingolstadt,1 +initiatives,1 +innisfail,1 +inoculation,1 +inolvidable,2 +inotainment,2 +inpi,10 +inspiracom,2 +inspiration,2 +inspiration radio,1 +inspirational,9 +inspirtational,1 +instagram,2 +instituto mexicano de la radio,20 +instituto nacional de los pueblos indígenas,10 +instituto politécnico nacional,1 +instore radio,7 +instro,1 +instrumentaal,1 +instrumentaal en de beste oldie's,1 +instrumental,146 +instrumental hip-hop,2 +instrumental hiphop,2 +instrumental hits radio,1 +instrumental music,14 +instrumental rock,3 +instrumentales,2 +instrumentals,1 +insurgent country,2 +intelligent,1 +intelligent funk,2 +intelligent speech,2 +intercultural,1 +internacionais,1 +internacional,9 +internathional,1 +international,186 +international djs,1 +international evergreens,1 +international hits,2 +international news,16 +international press review,2 +international programming,1 +international tribal,1 +internet,16 +internet radio,72 +internet station,9 +internet-radio,3 +internetradio,7 +internetradiodotcom,4 +interview,6 +interviews,8 +inthesmoker,1 +into the woods,1 +intolerance of dissent,9 +intoleranz,9 +intriguing sounds,1 +intrumentales,1 +inuit,2 +iowa city,1 +ipiales,3 +ipn,1 +iptv,30 +iran,3 +irapuato,4 +iraq,5 +iraq radio,2 +ireland,4 +irish,8 +irish folk,9 +irish language,3 +irish music,4 +irish traditional,4 +iron maiden,15 +ironton,1 +irvine,1 +isernia,1 +isicathamiya,1 +islam,46 +islam music,1 +islamabad,1 +islami,1 +islamic,53 +islamic music,3 +islamic religion,2 +islamic، classical,5 +islam🕋,1 +island,2 +islands,1 +islma,1 +israel,9 +israeli pop,4 +israeli pop oldies,2 +israël,1 +istanbul,3 +istanbul teknik universitesi,1 +italia,2 +italia network,2 +italia-sounds,1 +italian,44 +italian house,1 +italian music,9 +italian oldies,7 +italian opera singers,1 +italian pop,89 +italian programming,1 +italian rock,2 +italiano,1 +italo,13 +italo dance,5 +italo dance anni 90,2 +italo disco,56 +italo disco hits,1 +italo disco new generation,1 +italo pop,1 +italo-dance,1 +italo-disco,4 +italo-pop,1 +italodance,1 +italodisco,4 +italohouse,1 +italy,10 +itm,1 +itr,3 +its,1 +iwi,9 +ixhuatlancillo,6 +ixmiquilpan,1 +ixtlán,1 +izvorna,3 +izúcar de matamoros,2 +i̇letişim fakültesi,1 +i̇rfan,1 +i̇slam,2 +i̇stanbul,1 +i̇stanbul teknik üniversitesi,1 +j,1 +j pop,2 +j-hiphop,1 +j-pop,19 +j-rock,9 +ja radio,1 +jack house,1 +jackin house,4 +jackson,6 +jackson michigan,1 +jacksonville,1 +jacobo,1 +jaffna,1 +jahnsdorf,1 +jalisco,66 +jalisco radio,3 +jalisco radio fm,3 +jalpa,2 +jam bands,4 +jamaica,1 +jamaican,3 +jamay,1 +jamband,6 +jambands,5 +james taylor,10 +james taylor),6 +jamestown,1 +jams,1 +janaki,1 +jangle pop,3 +japan,6 +japanese,23 +japanese idols,9 +japanese music,13 +japanoise,2 +japon,1 +jasper,1 +javanese,2 +javni servis,2 +jay z,1 +jaz,1 +jazz,954 +jazz & chill,1 +jazz christma,1 +jazz easy listening,1 +jazz fm,1 +jazz funk,4 +jazz fusion,27 +jazz lounge,5 +jazz manouche / gypsy jazz / gypsy swing,2 +jazz music,7 +jazz rock,5 +jazz swing,2 +jazz und lounge,3 +jazz vocal,2 +jazz vocal standards,1 +jazz-funk,2 +jazz-rock,4 +jazzhop,1 +jazzrock,1 +jazzstep,1 +jazzy,4 +jazzy beats,1 +jazzy house,2 +jcore,1 +jeck,1 +jefferson,1 +jefferson city,1 +jefferson state of mine,1 +jemp,1 +jena,1 +jerez,3 +jersey,1 +jersey city,4 +jessen (elster),4 +jessie cervantes,11 +jesus,20 +jesus alive radio,1 +jesus hindi,1 +jesús,2 +jethro tull,1 +jeunesse,1 +jewish,8 +jewish community,3 +jigs and reels,1 +jim hightower,2 +jimmy buffett,1 +jinjer,1 +jive,1 +jjk,1 +jmusic,1 +jnnynny,1 +johannesburg,1 +johanngeorgenstadt,1 +johannstadt,2 +john,1 +john farnham,1 +john laws,3 +john lennon,1 +john macarthur,1 +john peel,2 +john piper,1 +johnny cash,1 +johnson city,2 +jojutla,1 +jokes,6 +joliette,1 +jornalismo,6 +joropo,2 +journalism,9 +journey,3 +journey through the mind,1 +jovem,17 +jovem pan,8 +joy division,1 +joya,4 +jpop,38 +jpr,1 +jrock,8 +juanjitos,11 +judaism,2 +judaismo,1 +judgment,1 +judío,1 +jugend,1 +jugendlich,1 +jugendliche,1 +jugendradio,3 +jugendwelle,2 +juke,1 +jukebox,5 +jul,1 +julio cortazar,1 +julio iglesias,1 +julio wajcman,1 +jump blues,2 +jump der neue sound im radio,1 +jump up,1 +jump-up,1 +jumpstyle,7 +jumpup,1 +junction city,2 +juneau,3 +junge erwachsene,1 +jungle,33 +jungle hardcore,1 +junkanoo,2 +jura,1 +just great songs,1 +just music no talking,1 +just rock,1 +justice,2 +justin bieber,1 +juvenil,254 +jésus,1 +jöhstadt,1 +k,1 +k digital,1 +k pop,1 +k-pop,19 +k-rock,1 +kabaret,1 +kabbalah,1 +kabbale,1 +kabelsketal,6 +kabyle,1 +kadife sokak,1 +kaditz,2 +kadongo kamu,1 +kadıköy,1 +kahla,4 +kahnawake,1 +kaitz,2 +kalamazoo,5 +kalamazoo michigan,1 +kalgoorlie,1 +kalman,1 +kamen rider,1 +kamenz,2 +kaministiquia,1 +kamloops,3 +kampinang,1 +kanena,4 +kankakee,1 +kannada,1 +kannadasan,1 +kanpur,1 +kansas city,4 +kansas public radio,2 +kantipur,1 +kanye west,1 +kapa haka,1 +kapa huka,1 +kapiti coast,1 +karachi,1 +karadeniz,1 +karanataka,1 +karelia,1 +karl-marx-stadt,2 +karneval,8 +karnevalsmusik,2 +karratha,1 +karsdorf,4 +karışık,1 +katf,1 +kathmandu,1 +katholic,9 +katholik,11 +katolički,1 +katoliška,1 +katoliški,2 +katy perry,1 +katzhã¼tte,2 +katzhütte,2 +kaviarasar,1 +kawerau,1 +kayokyoku,1 +kazakh music,7 +kazakh pop,2 +kazan,5 +kbs,1 +kcck-fm,1 +kcht czyli kuchenna charakterystyka terenu.,1 +kcjj-am,1 +kcnr,1 +kcnr 1460,1 +kedgewick,1 +keene,1 +keith & kristyn getty,1 +kekc,1 +keks,1 +kelowna,3 +kelyje,1 +kelyje klaipėda,1 +kemberg,4 +kenai,1 +kenai peninsula,2 +kengë shqipe,4 +kenny rogers,2 +kent,2 +kentucky,4 +kentucky public radio,1 +kentville,4 +kerala,2 +kerk- en koormuziek,1 +kerne,1 +kernow,1 +keroncong,1 +kerstliedjes,1 +ketchikan,1 +keutschen,4 +kew,1 +keywords,1 +kfm,1 +ki,1 +kiangsu,3 +kibm,1 +kids,81 +kids’ party music,1 +kigali,1 +kiirtan,2 +kika,1 +kim komando,1 +kim robson 7pm,1 +kimberley,1 +kinder,6 +kinderdisco,1 +kinderfernsehen,1 +kinderkanal,1 +kinderlieder,10 +kinderprogramm 6 und 18 uhr,1 +kinderradio,4 +kingisepp,2 +kingsport,1 +kingston,4 +kingstown,3 +kink,1 +kino,2 +kinshasa,1 +kir98,1 +kiraka,1 +kirche,3 +kirchensender,2 +kirchscheidungen,4 +kirschau,2 +kiss,3 +kiss fm,5 +kiss fm méxico,3 +kitap,1 +kitch,1 +kitchener,2 +kitzscher,2 +kiuu,3 +kiwi,3 +kiwi country,1 +kizoma,2 +kizomba,2 +kk,1 +klaa,1 +klaffenbach,1 +klaipėda,1 +klang,1 +klasik edebiyat,1 +klasik müzik,2 +klassiek,3 +klassik,47 +klassik - was dir gefällt / whatever you like!!!,1 +klassik rock,3 +klassik weihnachten christmas,10 +klassisk,3 +klaus fiehe,1 +klavier,1 +kleinjena,4 +kleinkind,1 +kleinolbersdorf,1 +kleinpestitz,2 +kleinpösna,2 +kleinzschocher,2 +kleve,1 +klipphausen,2 +klosterhã¤seler,2 +klosterhäseler,2 +klostermansfeld,4 +klubb,1 +klídek,1 +kmoj,1 +kmro,2 +knauthain,2 +knautkleeberg,2 +knautnaundorf,2 +knight,1 +knog,1 +knoxville,3 +knr,1 +kochi,1 +kodaikanal,1 +kohren-sahlis,2 +kollywood,3 +kommentare und exklusive recherchen,1 +kompa,5 +konferans,1 +konkani,1 +konkani love,1 +konpa,1 +konya,3 +konzerte,2 +koplo,6 +korea,1 +korean,4 +korean music,4 +korean pop,1 +korean trot,2 +korn,11 +kosmoradio,1 +kosova,4 +kosove,1 +kosovo,3 +kotlas,1 +kozhikode,1 +košice,1 +kpop,22 +kpup,1 +krajiška i narodna,1 +kral,1 +kralendijk,2 +krankenhausradio,1 +krautrock,10 +kreml,1 +kretzschau,4 +kriebstein,1 +kristau irratia - radio chretienne,1 +kritik an coloradio,1 +krqb,1 +krã¶llwitz,2 +kröllwitz,2 +kršćanski,1 +kuduro,2 +kujawy,1 +kult,2 +kulthits,7 +kulthits der 70er,1 +kultschlager,1 +kultur,31 +kulturmd,1 +kulturpolitische debatten,2 +kunst,1 +kuom,1 +kuran,3 +kuratierte musikvielfalt,2 +kurdish,2 +kurdish folk music,4 +kurdish songs,10 +kurdistan,1 +kuschelrock,4 +kuwait,8 +kuwait الكويت,3 +kuzhippalathil,3 +kuzhippalathil radio,1 +kwaito,2 +ky,6 +kyiv,3 +kylie minogue,1 +kymenlaakso,1 +kyrm,1 +kzmrz,1 +kã¶nnern,2 +käbschütztal,2 +köln,2 +kölsch,3 +kölsche balladen,2 +kölsche musik,1 +königsbrück,2 +königswalde,1 +könnern,2 +kültür,1 +la voz del pitic,1 +la 100,1 +la 95,1 +la 99,1 +la arepa paisa,1 +la barca,1 +la bestia grupera,25 +la bomba,4 +la bonita del norte,1 +la bouche,1 +la buena onda,1 +la caliente,8 +la caminera,3 +la ceiba,1 +la comadre,22 +la dorada,2 +la estación exacta,97 +la estación naranja,97 +la fiera,2 +la garriga,1 +la gigante,2 +la gran aw,1 +la grande,1 +la grande de michoacán,1 +la grupera,3 +la guapachosa,1 +la hora de luis miguel,13 +la hora nacional,10 +la huasteca,1 +la inolvidable,1 +la interesante,1 +la invasora,3 +la jefa,1 +la kaliente,1 +la ke buena,27 +la ley,1 +la lupe,26 +la líder,1 +la líder stereo zer,1 +la magnífica,3 +la mas vacana,2 +la maxi,2 +la mejor,44 +la mejor aquí nomás,41 +la mejor fm,42 +la mera jefa,1 +la mera mera,2 +la mera yema,2 +la mexicana,3 +la mexicanita,1 +la morrita,1 +la más buena,3 +la más perrona,2 +la más picuda,4 +la nayarita,1 +la nuestra,1 +la nueva,2 +la nueva candela,10 +la número 1,1 +la octava,5 +la pampa,15 +la patrona,1 +la paz,7 +la piedad,3 +la plakosa,1 +la plata,2 +la poderosa,18 +la primera,2 +la q,2 +la que manda,3 +la radio 100% chanson française,1 +la radio 104.7,1 +la radio de hoy,1 +la radio de sudcalifornia,1 +la radio impactante,5 +la ranchera,3 +la ranchera de cuauhtémoc,1 +la ranchera de paquimé,1 +la rancherita,4 +la raza,1 +la red,1 +la reverenda,1 +la rumorosa,1 +la sabrosita,2 +la selva del camp,1 +la seu d'urgell,1 +la sonora de nogales,1 +la suavecita,2 +la t grande,1 +la tapatía,1 +la tg,1 +la tkr,1 +la top,1 +la tremenda,2 +la uk,1 +la unica radio con cojones,1 +la voix du lat,1 +la voladora radio,1 +la voz de galicia,2 +la voz radio,1 +la z,4 +la zeta,3 +la única,1 +labrador,1 +lac,1 +lac d'annecy,1 +lac la biche,1 +lacombe,2 +lacra de la humanidad,1 +ladin,1 +lady gaga,1 +lady pank,2 +lag,1 +laganini,2 +lagos de moreno,1 +laguna,6 +lahore,1 +laid-back,3 +laid-back jazz,2 +laika,4 +laiki,1 +laiko,1 +laisvoji banga,1 +lake constance,1 +lake george,1 +lakeville,1 +lam3g4 lamegaecuador,1 +lamperts,2 +lancaster,2 +landesfunkhaus,3 +landeswelle,2 +landsberg,4 +landtag,4 +langenberg,1 +languages,5 +langweilig,1 +lanitz-hassel-tal,4 +lansing,2 +laplace,1 +laredo,5 +larmo,1 +las baras,1 +las vegas,4 +last christmas,3 +latest hits,1 +latiano,1 +latidos fm,1 +latin,132 +latin salsa kumbia merenge reggaeton,6 +latin america music,1 +latin america rock,2 +latin dance,2 +latin house,3 +latin jazz,15 +latin music,137 +latin pop,218 +latin rock,2 +latin salsa,17 +latin salsa kumbia merenge reggaeton,3 +latin urban,9 +latina,20 +latinamerican music,4 +latino,255 +latino américa,49 +latino urbano,16 +latino-pop,13 +latinoamerica,53 +latinoamérica,630 +latvian,1 +latvian rock,1 +latvijas radio,5 +laucha an der unstrut,4 +launceston,2 +laura ingraham,1 +lausitz,3 +laut.fm,2 +lauter-bernsbach,1 +laußnitz,2 +lawrence,2 +lawrenceville,1 +lawton,1 +lazio,2 +lazy,1 +lds,1 +le havre,1 +le mars,1 +le son campus,1 +leamington,2 +learn,2 +learn english,2 +learn language,2 +lebanon,2 +lecce,1 +lectures,1 +led zeppelin,12 +lederland,1 +leeres radio,1 +left,1 +left wing,8 +left-field dance,1 +legacy media,1 +legends,5 +legends classic rock,1 +legionary,1 +legionary movement,1 +legislative,1 +legrigri,1 +lehigh valley,1 +leicester,1 +leimbach,4 +leipzig,12 +leiria,1 +leisnig,1 +leland,2 +lemgo,1 +lemko,2 +lenguas indigenas,4 +leon,4 +leonforte,1 +lernen,2 +lesbian,3 +lesungen,1 +lesungen und ausstellungen,2 +lethbridge,1 +leticia,1 +lettin,4 +leuben,2 +leuna,4 +leutzsch,2 +levanto,1 +levenslied,2 +levis,1 +lexington,2 +león,26 +lgbt,15 +lgbtiqa*,5 +lgbtq,3 +lgbtqa,4 +lgbtqia+,12 +lgr,1 +liberal,2 +libertarian,3 +libertas radio,2 +libre,2 +libretime,2 +libya,1 +lichtenau,1 +lichtenberg,2 +lider informativo,1 +liderfm,1 +lidermacher,1 +liebe,4 +liebertwolkwitz,2 +liebesbriefe,5 +liebeslieder,3 +lieblose playlist,1 +liedermacher,6 +lieskau,4 +lietaus radijas,1 +lietus,1 +lietus lietuva,1 +lietuvos zinios,1 +life,12 +life guide,3 +life stories,1 +lifestyle,67 +light,5 +light classics,5 +light favorites,1 +light orchestral,12 +light rock,1 +like fm,2 +lil wayne,1 +lille,1 +lima,3 +limbiate,1 +limburg,3 +limited commercials,1 +linares,5 +lincoln,1 +linda ronstadt,1 +lindenau,2 +lindenthal,2 +lindy,1 +line dance,1 +linkin park,16 +linkinpark,1 +links-grã¼n-versifft,2 +links-grün-versifft,7 +linksextrem,1 +linksextreme amateurfunker,1 +linksextremismus,9 +linux,5 +lippe,1 +liquid,6 +liquid dnb,7 +liquid funk,7 +liquid trap,4 +liscio,1 +lisiera,1 +listener supported,4 +lite,7 +lite music,2 +litera,1 +literature,43 +lithuania,2 +lithuanian news radio,1 +litoral,3 +little eagle,1 +liturgical,1 +live,55 +live air,1 +live audio,1 +live commentary,4 +live concert,19 +live concerts,1 +live dj,1 +live events,1 +live match,1 +live music,7 +live radio,1 +live rock,2 +live show,7 +live shows,1 +live sport,2 +live sports,72 +live sports commentary,1 +liveradio,9 +liverpool,1 +liveset,1 +livesets,1 +livestream,70 +livre,2 +ljubljana,1 +lkr radio,1 +llamamiento,1 +lloydminster,1 +lo-fi,17 +lobos fm,2 +lobpreis,5 +loca news,1 +local,155 +local am 900,1 +local emphasis,1 +local events,1 +local government,16 +local info,4 +local information,36 +local music,193 +local musicians,1 +local news,570 +local news weather,3 +local programming,33 +local radio,332 +local sports,13 +local station,1 +local talk,60 +local traffic & weather,11 +local weather,12 +locale,1 +lockport,1 +lodi,1 +lofi,29 +lofi beats,2 +lofi hip hop,7 +lofoten,1 +lohsa,2 +loiret,1 +lokaal,2 +lokalfernsehen,7 +lokalne,1 +lokalni,3 +lokalprogramm,1 +lokalradio,10 +lokura fm,8 +lollywood,4 +london,17 +london greek radio,1 +londrina,1 +long island,7 +long pieces of classical music,1 +longobardi,1 +longreach,1 +longview,1 +looktung,2 +loona,1 +lord,1 +lorrol,1 +los angeles,22 +los cabos,1 +los mejores momentos,5 +los mochis,17 +los reyes,1 +los40,52 +loschwitz,2 +lossatal,2 +lossless,3 +lot-et-garonne,1 +louisiana,1 +lounge,262 +lounge chillout relax smoothjazz ambient,1 +lounge house,1 +lounge music,50 +lounge radio,7 +louvain-la-neuve,1 +louvor,1 +louvores,1 +love,57 +love fm,7 +love hits,1 +love song,7 +love songs,97 +love-songs,2 +lovebeat,1 +loveparade,1 +lovers rock,1 +lovesongs,24 +low,2 +low bandwidth,13 +low power fm,5 +low stream new,1 +low-power,1 +lowell,1 +lpfm,4 +lubava,1 +lubbock,1 +luchito,1 +luckey,1 +luga,2 +lugau,1 +lullaby,4 +luminous radio,1 +luna de amor,10 +luna medios,1 +lunare,1 +lusatia,2 +lutheran,2 +lutherplatz,4 +lutherstadt eisleben,4 +lutherviertel,1 +lutsk,3 +luz,1 +luznetwork,11 +lviv,1 +lynchburg,2 +lynyrd skynyrd,1 +lyon,1 +lyube,3 +lã¶bejã¼n,2 +lã¼tzen,2 +língua portuguesa,1 +löbejün,2 +lößnig,2 +lößnitz,1 +lübbecke,1 +lüneburg,1 +lützen,2 +lützschena,2 +m s viswanathan hits,1 +m2o,2 +m3u8,44 +m80 80s 70s 90s 00s,3 +maastricht,1 +macedonia,1 +macedonian,4 +macedonian contemporary,1 +macedonian folk,4 +macedonian jazz,1 +macedonian pop,6 +macedonian rock,4 +macerata,1 +machern,2 +mackay,1 +madeira,4 +madewithheart,2 +madhur radio,1 +madina,1 +madison,2 +madonna,2 +madrid,2 +madsen,1 +madurai,1 +magazine,4 +magazines,12 +magdalena de kino,1 +magdeburg,4 +magesh,1 +magia digital,2 +magic london,2 +magnolia,1 +maha periyava charanam,1 +maher al mueaqly,1 +main,2 +mainstream,220 +mainstream jazz,8 +mainstream music,2 +mainstream rock,27 +mainstream urban,16 +mainz,1 +mairlist,1 +makedonia,1 +malayalam,12 +malayalam song,2 +malaysia,3 +malaysian pop,7 +malchin,3 +malgrat de mar,1 +malibu,1 +malle,1 +mallorca,4 +malschwitz,2 +malta,1 +mambo,2 +mamele și petrecere,1 +manantial,1 +manantial fm,1 +manchester,4 +manchester rave,1 +mando diao,1 +manele,35 +manele noi,6 +manele si petrecere,5 +manele vechi,5 +manele și petrecere,14 +mangalore,1 +mango,1 +mangoradio,1 +maniapoto,1 +manitou springs,1 +manizale,1 +manlius,1 +manlleu,1 +mannheim,1 +manouche,3 +manowar,1 +mansfeld,4 +mansfeld südharz,1 +mansfeld-sã¼dharz,2 +mansfeld-südharz,2 +mantova,2 +mantra,11 +mantras,1 +manzanillo,2 +maori culture,8 +mar del plata,2 +mar fm,1 +marathi,2 +maravatío,4 +mariachi,35 +mariah carey,1 +marienberg,1 +marienborn,2 +marienbrunn,2 +marimba,4 +marina di gioiosa ionica,1 +marius müller-westernhagen,1 +mark fm,1 +mark levin,2 +markersdorf,1 +markfm,1 +markham,1 +markkleeberg,2 +markranstädt,2 +markus kavka,2 +markwerben,4 +marotta,1 +mars,1 +marsala,1 +marsch,2 +martha's vineyard,1 +martial industrial,3 +martin,1 +marusha,1 +marusya,1 +marusya fm - new russian hits!,1 +marysville,1 +maryville,2 +marzi,1 +mas flo,3 +mas medios nogales,1 +mas radio,1 +mashup,21 +mashwitz,1 +masjid,1 +maspalomas,1 +mass,1 +massage music,1 +massey university,1 +master fm,1 +mata laxmi durga,1 +matamoros,4 +matavera,1 +match,12 +matehuala,1 +math rock,1 +mathcore,2 +maties,1 +matthias matuschik,1 +maturin,1 +matuschke,1 +mauriti,1 +maxi,3 +maxi - single,1 +maxi france,2 +maxi hits,1 +maxiradio,1 +maxis,2 +maya,2 +mayaguez,1 +mayak,1 +mazatlán,21 +mc,1 +mcalester,1 +mcallen,4 +mch,1 +mckinney,1 +mdf1,1 +mdr,16 +mdr 1 radio sachsen anhalt,1 +mdr aktuell,1 +mdr fernsehen,3 +mdr figaro,1 +mdr info,1 +mdr jump,1 +mdr kultur,1 +mdr life,1 +mdr s-anhalt,3 +mdr sachsen,7 +mdr sachsen-anhalt,3 +mdr thüringen,4 +mdr tweens,1 +mds,1 +mechanicsville,1 +mecklenburg vorpommern,5 +medellin,1 +medellín,4 +medford,1 +media,3 +media discussion,2 +media kristen,10 +mediaset,97 +medicine hat,2 +medienanstalt sachsen-anhalt,7 +medienpolitik,10 +medienpolitik sachsen,1 +medieval,19 +medios alternativos,1 +medios radiofónicos de michoacán,1 +meditación,1 +meditate,1 +meditation,45 +meditation music,1 +meditazione,1 +mediterranean,3 +mega,1 +mega fm,3 +megadeth,12 +megamedios,1 +megapolisfm 89.5,2 +megaradio,6 +meico,1 +meineweh,4 +meisdorf,4 +mek,1 +melancholic,5 +melatonin,2 +melayu,3 +melbourne,10 +melbourne bounce,3 +melchor ocampo,1 +melfi,1 +mellow,8 +mellow album rock,5 +mellow rock artists of the 70s,1 +melodic,8 +melodic death metal,2 +melodic hard rock,4 +melodic heavy metal,1 +melodic house,11 +melodic house & techno,5 +melodic metal,5 +melodic power metal,2 +melodic rock,12 +melodic techno,1 +melodic trance,4 +melodic-rock,1 +melody,2 +meme music,2 +memphis,2 +memphis hip-hop,1 +memphis rap,3 +memphis soul,1 +mendocino,1 +mennonite,1 +menorca,1 +mensajes cristianos,8 +merced,1 +mercer,1 +merengue,93 +merengue bachata,9 +merida,10 +meriden,1 +merry christmas,2 +merseburg,5 +merseybeat,1 +mertendorf,4 +mesnevi,1 +messages,1 +messianic,4 +metadata,4 +metal,294 +metal (e.g. iron maiden),5 +metal ballads,3 +metal fm,1 +metal rock,1 +metalcore,26 +metallica,15 +metaphysics,1 +metepec,3 +metro,1 +metrolnk,1 +meusdorf,2 +mevlana,1 +mex,395 +mexicali,24 +mexican music,237 +mexican pop,2 +mexicana baladas 70 80 varios,1 +mexicanisima,3 +mexicano,1 +mexico,470 +mexico city,146 +mexicomagico,1 +mexicotravel,1 +mexiquense,6 +mexiquense radio,6 +mezclas,1 +mezclas varios,1 +mezkla fm,1 +mezmur,2 +mg,1 +mg comunicación,1 +mg radio,3 +mgt,7 +mgt brasil hits mpb nacional,1 +mgt sertanejo clássicos,4 +mi,1 +miami,5 +miami rock classics,2 +michael,1 +michael buble,1 +michael jackson,3 +michael jackson),1 +michigan,3 +michigan state spartans sports,2 +michigan wolverines,1 +michigan wolverines sports,1 +michoacan,9 +michoacán,33 +mickten,2 +microtonal,1 +mid 1990s,1 +mid-rock,1 +mid-tempo,6 +mid-tempo electronica,1 +middle eastern music,1 +middle eastern pop and traditional,1 +middle of the road,6 +middleton,1 +middletown,2 +midnight oil,1 +midtempo,1 +midwest rap,1 +mikis theodorakis,2 +milano,21 +milazzo,3 +mildenau,1 +milenio radio,2 +miles davis,1 +miley cyrus,1 +militaire,1 +military,3 +millenium,2 +millersville,1 +milonga,1 +miltitz,2 +milwaukee,3 +mina,1 +minang,1 +minas,1 +minatitlán,1 +minden,1 +mindfulness,2 +minemusic,1 +minidisco,1 +minimal,61 +minimal electronica,1 +minimal tech house,5 +minimal techno,32 +minimal wave,6 +minimalism,1 +minimalsynth,2 +minneapolis,17 +minnesota,1 +minnesota public radio,9 +minority,11 +mint,1 +miramichi,2 +miraquill,1 +mirchi,3 +misc,272 +miscellaneous,63 +miscellenious,1 +mishary alafasy,2 +misic,1 +misiones,1 +mission,2 +missionary,1 +mississauga,1 +mississippi,1 +missoula,3 +mitte,2 +mittelalter,14 +mittelalter-rock,2 +mittelbach,1 +mitteldeutscher rundfunk,4 +mitteldeutschland,4 +mittweida,1 +mix,118 +mix - national,1 +mix alternative,2 +mix fm,22 +mix genres,1 +mixe,5 +mixed,40 +mixed music,4 +mixed programming,2 +mixes,6 +mixta,1 +mixtape,3 +mixtapes,8 +mj ♡,1 +mko,1 +mlb,1 +mlp,8 +mna,1 +moapa valley,1 +mobile,4 +mockau,2 +moda,2 +moda de viola,6 +modal jazz,1 +modao,3 +modena,2 +moderation,2 +modern,28 +modern adult contemporary,1 +modern big band,8 +modern classical,6 +modern composition,4 +modern electronic ambient,1 +modern hits,1 +modern jazz,1 +modern pop,4 +modern praise,1 +modern rock,28 +modern soul,3 +moderno,1 +modesto,2 +modica,1 +mods,1 +modular synthesis,2 +modão,1 +moe loogham,1 +mohammad refat,1 +mohan hits,1 +mohawk,1 +moi,1 +moi merino,700 +moin,1 +mojahed,1 +molauer land,4 +moldova,1 +molina comunicaciones,1 +molins de rei,1 +mollet del vallès,1 +mollywood,2 +molochio,1 +monas chinas,1 +monastery,1 +monastir,1 +monclova,3 +moncton,3 +monde,2 +mongo,1 +mongoradio,1 +monologues,1 +monos chinos,1 +mont belevieu,1 +montagne,1 +montana,1 +montaña,1 +montcarlo,1 +monte carlo,1 +montecalvo irpino,1 +montemorelos,2 +monterrey,41 +montgomery,1 +montreal,11 +montrose,1 +monts du lyonnais,1 +montserrat,1 +montsià,1 +montuno,1 +monusco,1 +mood,3 +mood musik,1 +moody,2 +moombahton,16 +moorehead,1 +moorhead,5 +moral outrage,9 +morcote,1 +more fm,1 +morelia,17 +morelos,15 +morgenleite,1 +morgenmagazin,1 +moringa,1 +mormon,3 +morning show,2 +moroccans,1 +morocco,1 +moroleon,1 +moroleón,1 +mortal,1 +moruya,1 +morón de la fra,1 +morón de la fra.,1 +morón de la frontera,2 +moscow,8 +moskva,2 +mosque,1 +motivational,5 +motivational talk,1 +motorhead,9 +motorsport,6 +motown,21 +motörhead,12 +mount cobb,1 +mount vernon,1 +mountain stage,1 +mountain top,2 +movie,26 +movie history,1 +movie music,4 +movie scores,2 +movie soundtracks,17 +movies,18 +movimento del '77,1 +mozart,3 +mp3,47 +mp3islam.com,1 +mpb,42 +mpbn,3 +mpp,1 +msv,1 +msviswanathan,1 +mt wellington,1 +mty,2 +mujer,2 +mulda,1 +multi,2 +multi genre,3 +multiculti,1 +multicultural,58 +multiculturalism,2 +multikulti,3 +multilingual,34 +multimedios,59 +multimedios radio,61 +multingual,1 +multiple genres,1 +mumbling,1 +mundart,4 +mundartradio - der mitmachsender von schwaben für alle,1 +mundo,1 +munich,5 +municipal,3 +murcia,2 +murfreesboro,2 +muscat,1 +music,3671 +music & music,2 +music & talk,2 +music .news,1 +music 70s 80s 90s,11 +music 80s 90s,1 +music adult contemporary,1 +music arabe,2 +music camp,1 +music contemporary,1 +music dj,1 +music drama,1 +music for life,1 +music for study,8 +music from the 1940s,3 +music from yesterday and today,1 +music hall,1 +music is life,1 +music journalistic topics,1 +music london,1 +music news,1 +music non stop,7 +music nonstop all day all night,1 +music only,10 +music pop,4 +music promotion,1 +music radio,7 +music rock,1 +music shows,1 +music trance,2 +music variety,5 +music videos,1 +music-hall,1 +musica,367 +musica alternativa,3 +musica andina,2 +musica anni 2000,2 +musica anni 70,3 +musica anni 80,6 +musica anni 90,2 +musica brasileira,3 +musica britanica,1 +musica catolica,4 +musica chilena,2 +musica christiana,1 +musica clasica en español,1 +musica clasica romantica,26 +musica classica,2 +musica cristiana,19 +musica de concierto,2 +musica del mundo,1 +musica ecuatoriana,1 +musica infantil,3 +musica italiana,64 +musica latina,158 +musica latinoamericana,113 +musica mexicana,141 +musica news,1 +musica pop,1 +musica popular,31 +musica portuguesa,2 +musica regional,157 +musica regional mexicana,170 +musica romantica,169 +musica romántica,3 +musica tradicional,5 +musica tradicional mexicana,139 +musica tropical,22 +musica variada,25 +musica variedades,1 +musical,23 +musical discovery,1 +musical theater,4 +musical theatre,3 +musicalmente instrumental,1 +musicals,15 +musicas,1 +musician,1 +musicmix,1 +music، talk,1 +musiek,1 +musik,22 +musik and news,1 +musik brunch,5 +musik der 80er,2 +musik der 90er,3 +musik entdecken,1 +musik gemischt,1 +musikentdecker,1 +musiki,1 +musiksender,5 +musique,16 +musique concrete,3 +musique contemporaine adulte,1 +musique cubaine,1 +musique de vieux,1 +musique du monde,2 +musique éclectique,1 +musiquera,1 +musiques des enfants,1 +muslim,3 +musuc,1 +musulmán,1 +muzic,2 +muzic info,4 +muzica,4 +muzica de poveste,2 +muzica inimii tale,1 +muzica populara,1 +muzică,1 +muzică de bun gust,1 +muzică de petrecere,14 +muzică populară,15 +muzică românească,4 +muziek zonder flauwekul,1 +muzik,2 +muzika za decu,1 +muzyka,2 +muzyka dla kazdego,1 +muzzanganda,1 +mv,1 +mvs,159 +mvs deportes,1 +mvs noticias,28 +mvs radio,164 +mvs radio poza rica,2 +mx,384 +my little pony,2 +my little pony: friendship is magic,7 +myoggradio,1 +myradio,2 +myradio 90.0,1 +mystery,9 +myxx,1 +myxx fm,1 +myxx-db,1 +myzic,2 +mágica,1 +más latina,1 +máxima,4 +mã¶nchpfiffel-nikolausrieth,2 +mã¼cheln (geiseltal),2 +märkischer kreis,1 +mérida,20 +méxico,988 +mía,1 +möckern,2 +mölkau,2 +mönchpfiffel-nikolausrieth,2 +músic,1 +música,827 +música actual,3 +música alternativa,1 +música andina,4 +música clásica,3 +música cristiana,8 +música cristiana pop,2 +música de concierto,1 +música del recuerdo,217 +música ecuatoriana,6 +música electrónica,5 +música en español,576 +música en español e inglés,230 +música en inglés,68 +música española,1 +música latina,19 +música latinoamericana,4 +música mexicana,112 +música original filipina,1 +música para infancias,1 +música para niños,2 +música pop,395 +música popular,1 +música popular brasileira,11 +música popular mexicana,292 +música regional,293 +música regional mexicana,119 +música religiosa,11 +música romántica,38 +música tradicional mexicana,79 +música tropical,34 +música urbana,74 +música uruguaya,2 +música variada,300 +música y mucho mas,1 +música y noticias,159 +música éxitos,1 +músicas atuais,1 +mücheln (geiseltal),2 +mügeln,1 +mühlenkreis,1 +münchen,5 +münchen 89,2 +münster,2 +müzik,2 +n-tv,2 +n24,3 +nablus,1 +nacajuca,1 +nachrichten,30 +nachrichten für den norden,1 +nachrichtenradio,1 +nachrichtensender,3 +naci alex,2 +naci alex naci alex naci alex naci alex naci alex,1 +nacionais,3 +nacional,15 +nacional cumbia 70 80 varios,1 +nacional varios,1 +nagercoil,2 +nagoya kei,1 +naija,1 +najvise muzike,1 +nana,1 +nanaimo,1 +nantes,1 +napierville,1 +napoli,13 +narodna,22 +narodna - etno,1 +narodna - folk,7 +narodna - folk - izvorna - krajiška starogradska,1 +narodna - folk - vlaška - kola,1 +narodna - izvorna - starogradska,1 +narodna - krajiška,2 +narodna - krajiška - uživo,1 +narodna - mix - rap - trap,1 +narodne,1 +narodni,2 +narodni radio,1 +narsdorf,2 +nashville,19 +nashville sound,1 +nat king cole,1 +natal,1 +natalizia,1 +national,31 +national affairs,1 +national music,1 +national news,5 +national press review,1 +nations unis,1 +nativa,4 +native,1 +native american,5 +nativista,2 +nature,18 +nature sounds,1 +naumburg,1 +naumburg (saale),4 +naunhof,2 +navidad,6 +navojoa,7 +naxi,9 +nayarit,25 +nazi,9 +naziversifft,1 +nba,1 +nbc,1 +nbc milano,1 +nbc sports radio,2 +nc central university,1 +ncaa football,9 +nccu,1 +ncs,3 +ndh,2 +ndombolo,2 +ndr,11 +ndr 1,1 +ndr 1 niedersachsen,1 +ndr 1 ol,1 +ndr 1 oldenburg,1 +ndr fernsehen,4 +ndr hd,4 +ndr1,1 +ndr1 nidersachsen,1 +ndr1 ol,1 +ndr1 oldenburg,1 +ndr2,1 +ndw,11 +nebelschütz,2 +nebeski radio,1 +nebra (unstrut),4 +necip fazıl,1 +nederlands,2 +nederlandstalig,14 +nederlandstalige muziek,1 +nederpop,3 +nederweert,1 +neenah-menasha,2 +negros,1 +neil diamond,1 +neil young,1 +neiva,1 +nelson,1 +nemsdorf-gã¶hrendorf,2 +nemsdorf-göhrendorf,2 +neo,1 +neo folk,1 +neo psychedelia,1 +neo soul,6 +neo soul… 100%vocal,1 +neo-exotica,2 +neo-progressive rock,4 +neo-soul,5 +neoclassical,5 +neofolk,7 +neomedieval,2 +nepal,2 +nepali,1 +nepali evergreen,1 +nervige moderatoren,1 +neschwitz,2 +netaudio,1 +nethel,1 +netherlands,6 +netlabel,1 +network,4 +netzkultur,1 +netzwerk,1 +neu,1 +neubrandenburg,2 +neudeutsch,1 +neue bundesländer,2 +neue detusche härte,1 +neue deutsche härte,2 +neuengã¶nna,2 +neuengönna,2 +neuer stream niedrige qualität,1 +neukieritzsch,2 +neukirch/lausitz,2 +neukirchen,1 +neurodivergent,9 +neurofunk,8 +neurospicy,1 +neustadt,6 +neustadt an der orla,4 +neustadt in sachsen,2 +nevada,4 +new,16 +new acoustic,1 +new age,51 +new age piano,6 +new beat,1 +new beat.,1 +new bedford,1 +new branding of previous entry,1 +new classic,1 +new classics,23 +new classics 80s,1 +new classics 80´s,8 +new country,24 +new format,3 +new format new branding,1 +new format rebranding,19 +new friends,1 +new generation,1 +new glasgow,2 +new hits,3 +new italo disco,3 +new jack swing,1 +new jersey,6 +new london,7 +new mexico,1 +new music,30 +new orleans,12 +new port richey,1 +new rave,3 +new releases,5 +new rock,9 +new romantic,1 +new songs,1 +new soul,1 +new sport,1 +new traditionalist,1 +new url march 2021,1 +new vibe,1 +new wave,115 +new wave/,1 +new weird america,1 +new westminster,2 +new wilmington,1 +new york,8 +new york city,31 +new york house,3 +new york public radio,5 +new zealand,1 +new-wave,4 +newage,1 +newark,3 +newcastle,2 +newcastle hardcore,1 +newcomer,6 +newfoundland,2 +newport,1 +newport news,1 +news,2686 +news and current affairs,1 +news and talk,3 +news clips,1 +news free,1 +news music,2 +news radio,5 +news talk,297 +news talk music,49 +news talk music entertainment,3 +news talk show,1 +news talk sports,1 +news united kingdon,1 +news variety,1 +newscasts,1 +newspapers,2 +newsradio,1 +newstalk,3 +news؛talk,1 +news؛talk؛culture؛kurdish music,1 +news🇺🇸🇺🇸🇺🇸,1 +next fm,1 +nexus,1 +nezahualcóyotl,1 +nfl,2 +ng,1 +nhl,1 +niagara,2 +niagara falls,1 +nichelino,1 +nicholas parsons,1 +nicht-kommerziell,1 +nicht-kommerzielles lokalradio,22 +nichtkommerzielles lokalradio,4 +nicki minaj,1 +niederdorf,1 +niederpoyritz,2 +niederrhein,1 +niedersachsen,2 +niederschlag,1 +niederschöna,1 +niederstriegis,1 +nienburg (saale),4 +nietleben,4 +nieuws,3 +nigeria,1 +night club,1 +nightcore,1 +nightlife,2 +ninetie,1 +nineties,4 +nintendo,2 +nirvana,10 +nitra,1 +nièvre,1 +niñas,2 +niñes,2 +niños,1 +nkl,15 +no,1 +no ads,54 +no agenda,2 +no alternative,1 +no depression,1 +no playlist,1 +no testosterock,1 +no.1 for dance music in leeds,1 +noaa,1 +noad,1 +noagenda,1 +nobeat,1 +nocopyright,1 +noel-music,2 +nogales,11 +noida,1 +noir,1 +noise,14 +noise rock,2 +non profit,1 +non stop,2 +non stop house sessions,2 +non stop music,4 +non-commercial,67 +non-profit,22 +non-stop,22 +non-stop music,30 +non-stop old skool & anthems,1 +non-underwritten,1 +noncomercial,11 +noncommercial,3 +noncommercial left liberal newyork eclectic,1 +nonprofit,2 +nonstop,1 +nonstop music,2 +nonstop-party-channel,2 +nontantola,1 +noongar,1 +noord,1 +norcal,2 +norddeutscher rundfunk,5 +norddeutschland,1 +nordestina,2 +nordharz,4 +nordic folk,1 +nordland,1 +nordmagazin,4 +nordrhein-westfalen,3 +norfolk,2 +norge,1 +norteamérica,752 +norteña,44 +norteño,93 +norteñísima,1 +north adams,1 +north american,1 +north beach,1 +north carolina,2 +north east somerset,1 +north newton,1 +northampton,1 +northern,2 +northern new jersey,1 +northern soul,11 +northern tasmania,2 +northfield,2 +norway,2 +norway house,1 +norwich,1 +nossa rádio,1 +nostalgia,36 +nostalgic,5 +nostalgico,3 +nostalgie,14 +nostalgie sarthe,1 +not-for-profit,3 +noticias,480 +noticias cortas,58 +noticias en español,93 +noticias locales,111 +noticias nacionales e internacionales,45 +noticias opinion,58 +noticias y comentarios,119 +noticias y deportes,12 +noticias y música,1 +noticieros,15 +noticioso,2 +notisistema,2 +notícias,17 +nouveau disco,2 +nouvelle vague,1 +nouvelle-aquitaine,1 +nouvelles,1 +nova,1 +nova friburgo,1 +nova news,1 +novedades,3 +novelty,1 +noventa padovana,1 +novo nordeste fm arapiraca,1 +novovolynsk,1 +now,3 +noyack,1 +noël,1 +npo,1 +npr,254 +npr for austin texas,1 +npr news now,1 +nq radio,1 +nrj,16 +nrm comunicaciones,8 +nrt méxico,1 +nrw,5 +nsfw,1 +nsv,1 +nsw,1 +ntr medios de comunicación,35 +ntv,3 +nu,1 +nu disco,28 +nu funk,2 +nu grooves,1 +nu jazz,6 +nu metal,15 +nu punk,13 +nu-disco,12 +nu-jazz,26 +nu-metal,1 +nudisco,2 +nuestras noticias,2 +nueva rosita,2 +nueva vida,1 +nuevo,1 +nuevo casas grandes,2 +nuevo laredo,7 +nuevo leon,16 +nuevo león,47 +nuevos,1 +nujazz,3 +numetal,1 +nummer-1-hits,1 +nuoro,2 +nur der rwe,1 +nutty-sound!,2 +nuuk,1 +nuus,1 +nwobhm,3 +ny,1 +nyoongar,1 +nyungar,1 +núcleo comunicación del sureste,3 +núcleo radio mina,1 +núcleo radio monterrey,2 +nürburgring,1 +nürnberg,12 +oak grove,1 +oak lawn,1 +oakdale,1 +oakland,1 +oasis,2 +oaxaca,16 +oaxaca city,3 +oberbayern,1 +obergurig,2 +oberkrain,2 +oberpfalz,1 +oberpoyritz,2 +obersdorf,4 +oberwiesenthal,1 +obhausen,4 +obituaries,1 +oboe,1 +obscure,7 +observation,1 +occasional folk classics,2 +occitan,4 +occitania,1 +occitanie,1 +ocean,3 +ocean city,3 +ocean drive,1 +ocean shores washington,1 +ocean shores washington news,1 +ocean shores washington weather,1 +ochenta,1 +ocracoke,1 +odessa,1 +odlie,1 +oechlitz,4 +oelsnitz,1 +oem,10 +off-broadway,3 +offener kanal,13 +office,1 +official,1 +official rock radio,1 +offshore,1 +offshore radio,4 +ogg,2 +ognjišče,1 +ohio,1 +ohio state,1 +ohope,1 +ohorn,2 +ohrwürmer,1 +oi,2 +oi!,4 +oi-punk,1 +oil city,1 +oir,2 +ojinaga,2 +ok fm,2 +ok fm df,1 +okarche,1 +oklahoma,1 +oklahoma city,8 +oklahoma state cowboys,2 +oktoberfest,1 +olaura,2 +olbernhau,1 +old age,9 +old country,2 +old hits,3 +old jazz,2 +old music,1 +old school,10 +old school dance,1 +old school hip hop,2 +old school hip-hop,4 +old school musical,1 +old school rap,2 +old school rave,1 +old skool,17 +old standards,2 +old time radio,67 +old time radio clips,1 +old time radio shows,1 +oldenburg,1 +oldie,7 +oldie based ac,1 +oldies,1122 +oldies & retro hits 70s 80s 90s 00s,1 +oldies 50's/60's,41 +oldies and retro hits 70´s 80´s 90´s 00´s,1 +oldies but goldies,2 +oldies der 60er,2 +oldies der 70er,2 +oldies der 80er,2 +oldies music radio weekeds,1 +oldschool,28 +oldschool electro house,1 +oldschool hardcore,1 +oldschool hiphop,2 +oldskool,5 +oldskool hardcore,4 +oldy,1 +olidies,1 +olimpica,1 +olimpica stereo,1 +olimpica stereo alicante,1 +oliva radio,3 +olivia newton-john,1 +olli schulz,1 +olot,1 +olympia,1 +olympics,1 +omaha,3 +omr group,2 +omroep brabant,1 +omulembe omutebi,1 +onda la superestacion,1 +one,1 +one hit wonders,1 +onethousend,3 +online,76 +online christian radio,8 +online only,68 +online radio,3 +onlineradio,5 +onlus,1 +only hits,1 +only music,17 +only the golden classics,11 +ontario,2 +op,1 +opb,1 +open air,12 +open source,3 +opening,1 +opensim,1 +opera,46 +opera metal,1 +opera mind,1 +operavore,1 +operetta,1 +opeth,1 +opinion,8 +opm,54 +opm hits,6 +opole dab,2 +opposition,1 +opus,15 +opus 94,2 +oranjestad,1 +orbassano,1 +orchestra,6 +orchestral,24 +oregon,2 +oreja fm,2 +oreja wfm,1 +orf,2 +organ,11 +organ house,1 +organic,3 +organic house,9 +organización radiofónica de acámbaro,3 +organización radiofónica de oaxaca,4 +organización radiofónica de tamaulipas,2 +orgel,1 +oriental,7 +oriental chillout,1 +oriental deep house,1 +orientale,1 +original,2 +original sound,1 +orizaba,9 +orlando,3 +ormond-by-the-sea,1 +oro,4 +oro valley,1 +orp,1 +orquesta,2 +orquesta tipica,1 +orquestrada,2 +orquestral,1 +ort,1 +orthodox,32 +orthodox christian,48 +orthodox church,8 +orthodoxy,19 +ortodox,3 +osendorf,4 +osgrid,1 +oshawa,2 +oshkosh,2 +osmanli,1 +osmanlı,1 +osmanlı edebiyatı,1 +ossetian music,1 +ost,7 +ost-musik,1 +ostdeutschland,2 +osten,1 +osterfeld,4 +ostpop,1 +ostrau,1 +ostrock,4 +ostrock radio,2 +ostrockradio,2 +ostsachsen,2 +ostvest,1 +ostwestfalen,1 +ostwestfalen-lippe,1 +oswego,3 +otago,2 +otaku,2 +otautahi,1 +other,26 +oto,1 +otr,54 +otros,3 +ottawa,9 +ottendorf-okrilla,2 +otterwisch,2 +ouro branco,1 +ouro verde,1 +ouro verde curitiba,1 +ouro verde fm,1 +ouro verde fm 105.5,1 +outlaw country,4 +outre-mer,25 +outrun,3 +outsiders,1 +overland park,1 +overlooked music,1 +overture,1 +overtures,1 +owen sound,3 +owensboro,1 +owl,1 +owosso,1 +oye,4 +oyerep fm,2 +oyerepa fm,2 +oyonnax,1 +oyun havası,1 +ozan,1 +ozguruz,1 +ozora,1 +p-funk,5 +p4,1 +pa,2 +pachuca,5 +pacific,1 +pacific cultures,1 +pacific grove,2 +pacific island music,2 +pacifica,14 +paderborn,1 +padova,5 +paekakariki,1 +pagan,2 +pagan black metal,1 +pagan metal,3 +pagodao,1 +pagode,12 +paisa estereo,1 +pakistan fm radio,1 +pakistani,4 +palamós,1 +palatine,1 +palawan,2 +palazzo,1 +palermo,6 +pallacanestro reggiana,1 +palm springs,4 +palma,1 +palmen,1 +pals,1 +pama media,4 +pampa,3 +panama city,1 +panda show,2 +panda zambrano,2 +panel discussions,1 +panel game,1 +pank,1 +panorama informativo,3 +panpalai,2 +panschwitz-kuckau,2 +paonia,1 +pappritz,2 +paradise,2 +paraense,1 +paranormal,12 +paraná,1 +parapolitics,1 +pardesi,1 +paris,5 +park city,1 +parlament,1 +parliament,3 +parody,1 +parole,1 +parral,7 +parrandeando,1 +parthenstein,2 +partially sighted,6 +partidos,1 +partinico,1 +party,110 +party hits,20 +partycharts,1 +partyhits,4 +partymix,11 +partymusik,4 +partyschlager,1 +partyviberadio,1 +pascal dumont,1 +pashto,1 +pasillos,2 +pasión fm,1 +paso doble,2 +passendorf,4 +passion,1 +passion fm,1 +pasto,1 +pasty,1 +patagonia,1 +paternò,1 +patrick county,1 +patriot,9 +patriotic,1 +patriots,2 +paul mc cartney,1 +paul simon,10 +paulista avenue,1 +paunsdorf,2 +pawcatuck,1 +pawleys island,1 +payphones,1 +países de língua portuguesa,1 +pc,1 +peace,5 +peace river,2 +pedara,3 +pegau,2 +pego,1 +peissen,4 +pendleton,1 +penig,1 +penn state,3 +pensacola,2 +pentecostal,3 +pentecostalism,4 +penticton,1 +penwith,1 +península studios,3 +people,1 +percussive house,1 +pereira,2 +periodístico,2 +perote,2 +pershian radio,1 +persian,7 +personal growth,1 +perth,3 +peru,2 +perugia,2 +peruvian cumbia,6 +peruvian music,1 +peruvian rock,2 +pesaro,1 +pescara,3 +pet shop boys,1 +petaluma,1 +peter bertelshofer,2 +peterborough,3 +petersberg,4 +petersburg,1 +petersham,1 +petrecere,25 +petrecere și manele,3 +petrignano,2 +petrolina,1 +petrolinafm,1 +petrozavodsk,1 +pfaffroda,1 +phil collins,1 +philadelphia,8 +philipsburg,11 +philly,1 +philly sound,1 +philo,1 +philosophy,5 +phish,1 +phoenix,13 +phoenix - „presseclub“,1 +phoenix news talk,1 +phoenix-sendung „im dialog“,1 +phone calls,1 +phonk,7 +phychedelic,1 +piacenza,1 +piano,67 +piano blues,2 +piano house,1 +piano jazz,19 +piano music,1 +picassent,1 +picoftheday,1 +pictou county,2 +piedras negras,14 +pieschen,2 +piha,1 +pillnitz,2 +pilot rock,1 +pilzköpfe,1 +pimba,1 +pineda de mar,1 +pinguin radio,1 +pinguinradio,2 +pink,1 +pink floyd,6 +pinkradio,1 +pino daniele,1 +pinoy,11 +pintoresco,1 +piotr wroński,1 +pipe organ,1 +piraat,1 +pirate,2 +pirate radio,4 +piraten,7 +piraten hits,2 +piraten muziek,3 +piratenhits,16 +piratenmuziek,4 +pirecuas,1 +pirineo,1 +pisadinha,3 +piseiro,1 +pistas,1 +pisticci,2 +piter,1 +pittsburgh,4 +placebo,1 +plagwitz,2 +plaisio stores,1 +planet,1 +planetshakers,1 +platdüütsch,1 +platja d'aro,1 +platteville,1 +platzhalter,1 +plauen,4 +plautdietsch,1 +plaußig,2 +play,1 +play what we want,1 +playa,3 +playa del carmen,4 +plays,1 +playstation,1 +plein air,4 +plein coueur vesoul,1 +plein-air,4 +plena,1 +plural,1 +plus,1 +plymouth,1 +pmoi,1 +po,1 +pocatello,1 +pockau-lengefeld,1 +podcast,27 +podcasts,13 +podfather,1 +poesia,2 +poesía,1 +poetry,13 +poezja,2 +poggibonsi,1 +pohoda,1 +poland,10 +polen,1 +police,4 +police scanner,8 +policy,1 +polish,19 +polish highlands,2 +polish jazz,1 +polish pop music,2 +polish rock,6 +politaktivismus,9 +politechnika gdanska,1 +politic,1 +politica,1 +politica internacional,1 +politica nacional,1 +political,13 +political folk,1 +political talk,45 +politics,105 +politics & current affairs,5 +politik,7 +politique,1 +politisch,1 +polka,10 +polska,4 +polski,1 +polski rock,2 +polskie radio,4 +polskieradio,14 +polvorines,1 +polyptych,1 +política,5 +pontarlier,1 +ponte exa,101 +ponte pilas,1 +pontevedra,2 +pontos,3 +ponts,1 +pool beach bar,7 +poole,3 +poolside,4 +poolside swing,1 +pop,4750 +pop & samba - antigas,1 +pop - 80s - 90s,1 +pop - folk,1 +pop - folk - ex-yu,1 +pop - folk - mix - mix - top 40 - hip-hop,1 +pop - house - dance,1 +pop - mix,1 +pop - narodna - folk- uživo - starogradska,1 +pop - rock - narodna - izvorna,1 +pop - zabavna,2 +pop - zabavna - mix,1 +pop - zabavna - rock,2 +pop - zabavna - rock - mix - ex-yu,1 +pop - zabavna - vesti,1 +pop adul kontemporer,1 +pop and electronic music from the late 70s and the 80s,1 +pop ballads,2 +pop classic,4 +pop clásico,52 +pop dance,147 +pop dance eurodance,1 +pop divas,8 +pop en español,97 +pop en español e inglés,195 +pop en ingles,26 +pop en inglés,74 +pop español,12 +pop fm,1 +pop folk,1 +pop gospel,1 +pop hits,1 +pop idol,2 +pop indie,3 +pop italian,2 +pop jazz,5 +pop latino,71 +pop m,2 +pop manado,1 +pop music,769 +pop music local news,1 +pop méxico,3 +pop müzik,1 +pop punk,3 +pop rap,1 +pop rnb,1 +pop rock,536 +pop rock hit news,3 +pop rock popular,4 +pop songs,1 +pop soul,2 +pop urbano,5 +pop-dance,1 +pop-folk,1 +pop-music,2 +pop-punk,1 +pop-rock,40 +pop-rock - 80s - dance,1 +pop.,1 +pop.mp3,1 +pop/rock,9 +pophit,15 +popjazz,1 +popkultur,1 +popmusik,2 +popolare,1 +popolare network,2 +poprock,2 +popschlager,3 +popsongs,1 +popstars,2 +popular,192 +popular culture,16 +popular music,26 +popular!,1 +populara,3 +populară,12 +por el placer de vivir,7 +pordeus,1 +pordeus.fm,1 +porlezza,1 +port clinton,1 +port elgin,1 +port huron,4 +port isabel,1 +port of spain,1 +port pirie,1 +port townsend,1 +portage,1 +portales,1 +portitz,2 +portland,11 +porto sant’elpidio,1 +portugal,12 +portuguese,8 +portuguese americans,1 +portuguese language,1 +portuguese music,3 +portuguese pop,3 +portuguese-speaking countries,1 +português,3 +positive,5 +positive energie,3 +positive news,1 +post britpop,1 +post dubstep,3 +post grunge,2 +post malone,1 +post punk,7 +post retro freeform,1 +post rock,3 +post-black metal,1 +post-bop,12 +post-grunge,1 +post-hardcore,7 +post-metal,1 +post-progressive,1 +post-punk,25 +post-rock,16 +postpunk,1 +posusje,1 +posušje,1 +potenza,2 +poughkeepsie,1 +pourlesport,1 +powell river,1 +power,2 +power 98,1 +power metal,14 +power pop,5 +power praise,2 +power rock,1 +powerbeats,1 +powerpop funk,1 +poza rica,5 +practice,1 +prague,18 +prairie grove,1 +praise,14 +praise & worship,1 +praise and worship,2 +praise jesus,1 +prank call,1 +prato,1 +pray,2 +prayer,3 +prayers,4 +preaching,3 +predicas,1 +predigten,2 +preguntale a gonzalo,1 +premià de mar,1 +prensa latina,1 +presidio,1 +press readings,1 +presseclub,1 +preußlitz,2 +preuãÿlitz,2 +prhs,1 +pri,62 +prießnitz,2 +prieãÿnitz,2 +primera 88.1,1 +prince,2 +princess anne,1 +principios,2 +priozersk,1 +prisa radio,6 +prishtine,2 +pristina,4 +private,1 +probstheida,2 +prog,14 +prog / symphonic rock,1 +prog / symphonic rock (e.g. yes),5 +prog metal,1 +prog rock,4 +prog-house,1 +prog-rock,1 +prog/symphonic rock,1 +programas,1 +progresive house,1 +progressive,69 +progressive artists,1 +progressive black metal,1 +progressive country,1 +progressive folk,1 +progressive house,35 +progressive metal,7 +progressive politics,2 +progressive pop,1 +progressive psytrance,7 +progressive rock,107 +progressive soul,1 +progressive talk,5 +progressive techno,1 +progressive trance,18 +progrock,1 +prohlis,2 +prokofiev,1 +promo,5 +promo éxitos,1 +promodj,7 +promomedios california,5 +promosat de méxico,1 +propaganda,6 +prophecy,2 +protest songs,1 +protestant,1 +proto-prog,1 +provence,2 +provence verte,1 +providence,1 +provincia,2 +provincia de buenos aires,3 +provincial,2 +prx,8 +pseudoscience,2 +psicodelia,1 +pskov,2 +psy,2 +psy-trance,1 +psybient,3 +psych rock,1 +psychedelia,13 +psychedelic,32 +psychedelic chillout grooves,1 +psychedelic folk,1 +psychedelic garage,1 +psychedelic pop,1 +psychedelic rock,20 +psychedelic soul,1 +psychedelic trance,1 +psychill,8 +psychobilly,2 +psychofolk,1 +psydub,1 +psyndora,1 +psytrance,36 +pub rock,6 +public,96 +public - npr,2 +public affairs,3 +public affairs programs,4 +public domain,1 +public npr,2 +public radio,971 +public rado,1 +public service,55 +publica,19 +publiic radio,1 +puccini,1 +puducherry,1 +puebla,37 +puebla city,15 +pueblo nuevo,1 +puerto escondido,1 +puerto rico,2 +puerto vallarta,16 +puig-reig,1 +pukhto,1 +pullman,1 +pulsar,1 +pulsnitz,2 +pump,3 +pump house,1 +pumping house,2 +pune,2 +pune am,1 +punjab,1 +punjabi,8 +punjabi radio australia,1 +punk,134 +punk anarcho-punk hardcore,4 +punk new wave,1 +punk oi ska,2 +punk or anything similar,1 +punk postpunk,1 +punk rock,44 +punk rock (e.g. sex pistols),2 +punk rock (e.g. the sex pistols),3 +punk wave,1 +punkowe radio,1 +punkrock,3 +punktum fernsehen,1 +punxsutawney,1 +pure rock and roll and trash; ska; trash; blue beat; r&b; blues; rock; garage; doo-wop,1 +purerock,1 +puros éxitos!!!,11 +puschwitz,2 +pã¶ãÿneck,2 +pößneck,2 +pública,60 +q102,1 +qatar,1 +quad ciities,1 +quality,1 +quarona,1 +quartier,1 +quartu sant'elena,1 +quattromiglia,1 +quebec,2 +quebec city,6 +queen,23 +queen's,1 +queens,1 +queensland,1 +queer,21 +queerbeet,1 +queretaro,2 +querfurt,6 +querétaro,15 +querétaro city,5 +quick,1 +quiet,1 +quiet storm,3 +quintana roo,14 +quinte,3 +quiroga,1 +quito,2 +quran,29 +quran radio,12 +quran station,1 +r,1 +r & b,2 +r /,1 +r and b,2 +r&b,124 +r&b soul,4 +r&b/urban,38 +r'n'b,13 +r'n'b funk,2 +r.c. sproul,1 +r.e.m.,1 +r.sa,22 +rabatz,1 +rabenstein,1 +raconteur,1 +radar,2 +radcap,90 +radeberg,2 +radewell,4 +radia,1 +radiante,1 +radibor,2 +radical,3 +radijas kelyje,1 +radijas kelyje lietuva,1 +radio,1184 +radio 4,1 +radio 40 plus,1 +radio 620,1 +radio 710,2 +radio 90.7,2 +radio activa,2 +radio alegría,1 +radio alma navidad cristiana,1 +radio amazigh,2 +radio anni 80,3 +radio argentina,3 +radio asamblea,1 +radio associative,9 +radio astral 102.9 fm,1 +radio atbir,1 +radio banana,2 +radio bauchi,1 +radio berbere,2 +radio blau,3 +radio bob,6 +radio bob!,10 +radio bremen,4 +radio bremen hd,1 +radio bremen tv,1 +radio buap,2 +radio by young people for young people,1 +radio c,1 +radio campus,2 +radio canada,1 +radio caprice,51 +radio caprice; radcap,12 +radio carnaval,1 +radio catolica,4 +radio cañón,10 +radio centro,10 +radio centro deportes,1 +radio chann pardesi,1 +radio chapultepec,1 +radio chetumal,1 +radio chiapas,1 +radio chilango,1 +radio cielo,1 +radio colima,1 +radio college,1 +radio communautaire,25 +radio communitaire,8 +radio comunicaciones de las altas montañas,1 +radio comunitaria,94 +radio cooperativa,1 +radio cristal,1 +radio cristiana,19 +radio de tlapa,1 +radio deinfm,1 +radio digitalia,1 +radio digitalia festival,1 +radio disney,12 +radio dla ludzi rozsądnych,1 +radio dominicana,1 +radio drama,10 +radio educación,7 +radio educativa de venezuela,2 +radio eins,1 +radio el mundo,1 +radio evangelica,2 +radio felicidad,5 +radio for the print handicapped,12 +radio france,51 +radio france concerts,1 +radio francia internacional,1 +radio fresh style - fresh,1 +radio fórmula,26 +radio fórmula primera cadena,19 +radio fórmula segunda cadena,3 +radio fórmula tercera cadena,1 +radio grupo garcía de león,2 +radio guerrero,1 +radio guyana inc,1 +radio hablada,314 +radio hbw,1 +radio honduras,1 +radio hz,1 +radio imer,2 +radio indigenista,2 +radio ipn,1 +radio kcht,1 +radio la guadalupana,1 +radio latina,2 +radio libre,8 +radio lobo,1 +radio lobo mx,1 +radio locale,4 +radio locale et associative à vierzon,1 +radio los compadres,1 +radio m,1 +radio maghreb,1 +radio maria,5 +radio maroc,2 +radio marocaine,1 +radio metrópoli,1 +radio mexicana,7 +radio mojahed,1 +radio monk,1 +radio moroleón,1 +radio municipal,1 +radio music rock pop,3 +radio muslim,1 +radio muzyka,1 +radio nacional,2 +radio nacional de españa,1 +radio net,1 +radio nigeria,1 +radio novelas,1 +radio nueva vida,1 +radio nuevo león,6 +radio núcleo,6 +radio núcleo comunicación oro,1 +radio omega,1 +radio onda,1 +radio one fm 91 gwadar,1 +radio one fm91,1 +radio online,70 +radio ostrock,2 +radio pak filmi,1 +radio plays,4 +radio plus agadir,2 +radio psr,17 +radio publica,25 +radio publique,20 +radio pública,71 +radio ranchito,5 +radio reading service,21 +radio red,2 +radio regenboog,1 +radio resultados,5 +radio roks,1 +radio roks moldova,1 +radio romania,2 +radio sa,3 +radio santa fe,1 +radio sarrebruck,1 +radio sensacion,1 +radio show,2 +radio shows,8 +radio sok,1 +radio sol,2 +radio stotis lietus,1 +radio sur,1 +radio swiss,3 +radio t,7 +radio tamazgha,1 +radio teddy,2 +radio televisión de veracruz,1 +radio teocelo,1 +radio trst a,1 +radio tunis chaine internationale,1 +radio turquesa,1 +radio tv méxico,2 +radio uadeo,1 +radio ujat,1 +radio unam,7 +radio underground,1 +radio united,1 +radio universal,10 +radio universidad,20 +radio universitaria,33 +radio upes,1 +radio variedades,1 +radio vida,1 +radio virtuale,1 +radio vital,1 +radio viva,1 +radio voltri uno,1 +radio vos,1 +radio voz,1 +radio wsw,1 +radio y televisión de aguascalientes,3 +radio y'omulembe omutebi,1 +radio yola,1 +radio zett,5 +radio zitouna,1 +radio zoque,1 +radio ública,1 +radio40plus,1 +radioactiva,1 +radiobob,4 +radiocharts,3 +radiocristiana,2 +radiodigitalia,1 +radiodrama,2 +radiodual,1 +radiogrupo,9 +radiomonster,2 +radiomás,1 +radion caprice,1 +radioparadise,4 +radioplay,1 +radiorama,96 +radiorama bajío,2 +radiorama de occidente,3 +radiorama durango,1 +radiorama mexicali,1 +radiorama morelos,4 +radiorama nayarit,7 +radiorama nuevo laredo,1 +radiorama parral,2 +radiorama piedras negras,1 +radiorama poza rica,2 +radiorama sinaloa,1 +radiorama sonora,3 +radiorama tamaulipas,2 +radiorecord,1 +radios comunitarias,18 +radios df,2 +radios ecuador online,1 +radios latinoamericanas,15 +radios pampeanas,14 +radiosender,1 +radioshow,1 +radiospektakl,1 +radioth comunicaciones,3 +radiotvmexico,4 +radiouas,1 +radioveten,1 +radiovisa,2 +radiovisión,1 +radiópolis,98 +radyo,2 +radyo can,1 +radyocan,2 +rafael delgado,3 +ragga,5 +ragga jungle,1 +raggae,6 +raggae london uk,1 +raggaeton,4 +raggamuffin,1 +ragtime,3 +rai,21 +rain,1 +rainorshine,13 +rajeev dixit,2 +rajshahi,1 +raleigh,1 +rammstein,4 +ran1,1 +ranchera,46 +rancheras,29 +random,2 +ranis,4 +rap,272 +rap classics,1 +rap fr,1 +rap francais,2 +rap hiphop rnb,51 +rap instrumentals,1 +rap metal,1 +rap music,1 +rap us,3 +rapcore,2 +raproyal,1 +rare 80s,2 +rare disco,1 +rare groove,14 +rare recordings,2 +rarities,1 +raschau-markersbach,1 +rasta,1 +raum und zeit,1 +rave,15 +raw,1 +raw hardstyle,1 +raw house,1 +raw uncut funk,1 +raßnitz,2 +raãÿnitz,2 +rbb,13 +rbb24,2 +rbn,1 +rbw,1 +rcg,9 +rclasicos,1 +rcs network,3 +rdj,1 +rdp,9 +rdp africa,1 +rdp açores,1 +rdp madeira,1 +re,1 +re-branding new format,4 +reactor,2 +readers,1 +reading,6 +readings,5 +reagge,1 +real classic rock,3 +real college radio,1 +realworld,1 +rebel fm,2 +rebel media,2 +rebranded new format,2 +rebranding,6 +recco,1 +rechtsextremismus,9 +recife,2 +reconexión,1 +recordações,1 +recordings,1 +recuerdo,8 +recuerdos,14 +red deer,4 +red dirt,4 +red hot chili peppers,2 +red lion,2 +red pill,1 +redbullet28,1 +redding,1 +redif,1 +redlands,1 +redovas,1 +redpres.com,1 +redwood,1 +reel big fish,1 +reflector,1 +reformat,1 +reformed,3 +refugio,1 +regaeton,4 +regenboog,1 +regensburg,1 +reggae,196 +reggae gospel,1 +reggae pop,2 +reggaeton,207 +reggaetón,20 +regge,1 +reggea,1 +reggeton,2 +reggio calabria,2 +reggio emilia,1 +regina,4 +regio,1 +regiocast,5 +region 10,1 +region 15,7 +region 29,2 +region 35,1 +region 37,1 +region 47,15 +region 53,1 +region 60,2 +region 76,4 +regionaal,4 +regional,334 +regional blues,4 +regional folk,1 +regional mexican,375 +regional mexicana,293 +regional mexicano,4 +regional music,184 +regional news,2 +regional programs,3 +regional radio,285 +regional reporting,3 +regional service,1 +regional service (german,1 +regionalfernsehen,5 +regionalfernsehen harz,1 +regionalmagazin,8 +regionalna muzyka,1 +regionalprogramm,6 +regionalsender,9 +regis-breitingen,2 +regrexa,2 +regueton,4 +reguetón,6 +rehoboth beach,1 +reichenbach/o.l.,2 +reichenbrand,1 +reichstag,1 +reick,2 +reideburg,4 +reinsdorf,5 +relationship,2 +relax,189 +relax associative,1 +relax music,5 +relax.,1 +relaxation,46 +relaxation music,18 +relaxing,83 +religi,2 +religia,3 +religion,187 +religiones,1 +religios,2 +religiosa,10 +religioso,4 +religious,293 +religious music,1 +religião,2 +religión,11 +religous,4 +remagen,4 +remake,2 +remember,3 +remember music information,1 +remix,25 +remixed,10 +remixes,37 +renaissance,2 +rende,1 +renewing your mind,1 +reno,2 +reno bike project,1 +reo speedwagon,2 +repeater network,1 +replay,2 +reports,2 +republica dominicana,1 +republica srbska,1 +republican,1 +request line,1 +requests,1 +rescaldina,1 +resi,1 +residance,2 +resistencia chaco,1 +respuesta radiofónica,2 +retro,255 +retro computing,2 +retro electronica,5 +retro fm,5 +retro games,10 +retro hits,9 +retro music,8 +retro r&b,2 +retro radio,1 +retro vinyl,1 +retrowave,8 +retrô,1 +reudnitz,2 +reußen,2 +reuãÿen,2 +reviews,1 +revista chilango,1 +revolucion,1 +revuelta de octubre,1 +revuelta social,1 +rey,4 +reyfm,13 +reykjavik,5 +reynosa,5 +rfe,1 +rfe-rl,19 +rfi,2 +rgv,1 +rhb,1 +rheda,1 +rheinland-pfalz,1 +rheinland-pfälz,2 +rhema,1 +rhinelander,1 +rhythm,3 +rhythm & blues,3 +rhythm and blues,29 +rhythm and blues (1960s randb (eg. the rolling stones)),5 +rhythm and blues (e.g. rolling stones),1 +rhythm and blues*,1 +rhythmic adult contemporary,3 +rhythmic contemporary hits,6 +rhythmic hits,1 +rhythmic top 40,3 +riba-roja de túria,1 +ribagorza,1 +riccardocioni,1 +riccione,1 +richmond,8 +richmond hill,1 +riddim,2 +ridge,1 +ridge radio,1 +ridgewood,4 +riegel und oederan,2 +rietberg,1 +riffs,1 +right wing,1 +right winger,1 +rightwing,1 +rihanna,2 +rijeka,1 +rijnmond,1 +rincon,1 +rincon de mielberg,1 +ringo starr,1 +rio de janeiro,3 +rio grande do sul,1 +rio grande valley,1 +rippach,4 +rire,3 +rire et chansons,2 +riscado,2 +rit,1 +ritual,2 +rivas radio,1 +riverhead,2 +riverside,3 +rm.fm,1 +rmf,1 +rmf francaics,1 +rmf hop bęc,1 +rmf lagodne przeboje,1 +rmf piosenka filmowa,1 +rmf polski hip hop,1 +rmf top 30 dance,1 +rmf top 30 pl,1 +rmf top 30 pop,1 +rmf łagodne przeboje,1 +rmffm,1 +rmln,5 +rnb,174 +rne,5 +rne 1,1 +rnh,1 +road,2 +roadtrip,2 +robbie williams,1 +robin schulz,2 +rochester,8 +rochlitz,1 +rochwitz,2 +rock,2547 +rock & roll,17 +rock 'n roll,1 +rock 'n' roll,19 +rock - pop-rock,2 +rock /,1 +rock alternatif,1 +rock alternativo,1 +rock and pop,2 +rock and roll,12 +rock and roll en español,2 +rock argentino,2 +rock ballads,9 +rock classics,24 +rock clásico,1 +rock en español,49 +rock en ingles,17 +rock en inglés,8 +rock fra dit liv!,1 +rock garage,1 +rock hits,3 +rock international,2 +rock italiano,1 +rock klassiker,1 +rock latino,6 +rock metal,1 +rock mexicano,3 +rock music,21 +rock n roll,28 +rock n' exa,2 +rock nacional,6 +rock n‘ roll,2 +rock perlen,1 +rock peruano,3 +rock pop,3 +rock pop dance,3 +rock pop soul 80s 70s oldies 60s,1 +rock pop trinidad 80s 70s 90s,2 +rock radio,1 +rock romântico,1 +rock sinfónico,1 +rock ua,1 +rock ‘n’ roll,1 +rock&blues,1 +rock'n'roll,30 +rock+pop,1 +rock-classic,1 +rock-pop,1 +rock/hardrock,1 +rockabilly,20 +rockballaden,1 +rockford,5 +rockhampton,1 +rockmusik,1 +rocknroll,3 +rockola,1 +rockolero,1 +rockparty,1 +rockstar,1 +rocksteady,13 +rockton,1 +rockzirkus,1 +rock’n’roll,1 +rocola,1 +rod stewart,1 +roda de fogo,1 +rodau,4 +rodeo,1 +rodolfo walsh,1 +roger,1 +rogers media,19 +rok,1 +rollers,2 +rolling stones,17 +roma,26 +roman catholic church,3 +romance,39 +romania,6 +romanian,26 +romanian disco,1 +romanian folk,11 +romanian goldies,1 +romanian hip hop,2 +romanian hip-hop,4 +romanian music,11 +romanian pop,19 +romanian rap,4 +romanian retro,1 +romanian rock,1 +romanian trap,2 +romantic,106 +romantic music,51 +romantic period,1 +romantic pop,1 +romantic songs,1 +romantica,43 +romanticas,3 +romantico,1 +romantik,9 +romantique,1 +romántica,62 +romántica y femenina,6 +románticas,62 +romântica etc...,1 +românticas,1 +romântico,1 +ronda policial,1 +ronnyantenne,2 +room,1 +roots,20 +roots music,3 +roots reggae,14 +roots rock,4 +roquetes,1 +rosario,9 +rosarito,1 +rosenheim,1 +rosenthal-bielatal,2 +rossau,1 +rostock,1 +rot weiss,1 +rotterdam,3 +rottluff,1 +roundup,1 +rovaniemi,1 +rovereto,2 +rovigo,1 +royal,2 +royal concertgebouw orchestra,1 +royalradio.ru,2 +royaltyfree,1 +roßbach,2 +roßwein,1 +roãÿbach,2 +rpsfm,1 +rsl,13 +rt de,1 +rt deutsch,1 +rtbf,19 +rtci,1 +rte,1 +rtl,2 +rtp,8 +rtv radio televisión de veracruz,1 +rtva,1 +rtvs,11 +rtvslo,7 +ru,1 +rubí,1 +ruiz,1 +ruiz de montoya,1 +rumba,8 +rund um die uhr für euch spielen wir für euch ein mix dance,2 +rundfunk berlin brandenburg,2 +rundfunk-kombinat,9 +rundfunk-kombinat sachsen,2 +rundfunkbeitrag,1 +rundfunkkombinat,1 +running,4 +rupertiwinkel,1 +rupestre,1 +rural,3 +rush limbaugh,2 +rusiński,1 +rusrek,1 +russell springs,1 +russia,12 +russia today,1 +russian,51 +russian abstract hiphop,1 +russian chanson,4 +russian dance electronic pop hits radio,1 +russian hardbass,1 +russian hip hop,1 +russian hits,40 +russian music,13 +russian pop,17 +russian programming,9 +russian punk,1 +russian rap,2 +russian rock,22 +russian shanson,1 +russian war-ship go fиck-yo-self,1 +russisches staatsfernsehen,1 +rusyn,2 +rutigliano,1 +rv1,1 +rwe,1 +r´n´b,1 +rádio,1 +rádio 1900 - ponte preta,1 +rádio africana,1 +rádio do sertão,1 +rádio espírita,1 +rás 2,1 +räcknitz,2 +régionale,1 +río grande,4 +röhrsdorf,1 +rötha,2 +rückmarsdorf,2 +s.matze,1 +sa:mp,1 +saad alghamdi,1 +saale,1 +saalekreis,4 +saalfeld/saale,4 +saarbrücken,2 +saarland,5 +saarländischer rundfunk,1 +sabadell,1 +sabambú fm,1 +sabrosita,1 +sachsen,22 +sachsen-anhalt,3 +sachsen-anhalt heute,6 +sachsenradio,1 +sachsensongs,2 +sachsenspiegel,6 +sacramento,8 +sacramento news,1 +sacred,4 +sad songs,1 +sadcore,2 +sageville,1 +saguenay,1 +saint germain,1 +saint john,3 +saint vincent,1 +saint-etienne,3 +saint-gabriel-de-brandon,1 +saint-hillarion,1 +sainte-foy,1 +sainté,2 +salaf,2 +salafi,2 +salafy,9 +sale,1 +salem,1 +salina cruz,3 +salinas,3 +salisbury,1 +salon,1 +salonmusik,1 +salsa,215 +salsa colombiana,9 +salsa dura,1 +salsa méxico,1 +salsa tecno 70 80 baladas bachata nacional varios,1 +salsa tecno 70 80 baladas varios,1 +salsas,12 +salsoul,1 +salta,2 +saltillo,6 +salto uruguay,1 +salud,4 +salzatal,4 +salzmã¼nde,2 +salzmünde,2 +sam divine,1 +samba,21 +sambass,1 +san andres,1 +san andrés tuxtla,2 +san antonio,4 +san antonio la isla,1 +san benedetto del tronto,1 +san bernadino,1 +san cristóbal,1 +san diego,26 +san donà di piave,1 +san francisco,7 +san gabriel,1 +san javier,1 +san jose,2 +san jose earthquakes,1 +san jose sharks,1 +san josé,5 +san josé del cabo,1 +san josé iturbide,2 +san juan,2 +san juan de los lagos,1 +san juan del río,1 +san juan fm,1 +san luis obispo,1 +san luis potosi,7 +san luis potosí,20 +san luis potosí city,6 +san martín texmelucan,1 +san mateo,1 +san michele,1 +san miguel,1 +san pedro sula,5 +san pedro tlaquepaque,1 +san quintín,2 +san rafael,5 +san salvador,1 +san salvo,1 +san zenone degli ezzelini,1 +sana doctrina,1 +sanat,1 +sanat müziği,1 +sangeet bhojpuri,1 +sangeet radio fm 95.1 am 1460 houston,1 +sangerhausen,4 +sanjuanitos,2 +sankt petersburg,2 +sanremo,2 +sanremo festival,1 +sans chaine,1 +sanspub,3 +sant carles de la ràpita,1 +sant celoni,1 +sant cugat del vallès,1 +sant'agata di militello,1 +santa bàrbara,1 +santa cristina d'aro,1 +santa cruz,5 +santa fe,2 +santa maría ocotán,1 +santa misa santo rosario católica,2 +santa monica,1 +santa monica college,1 +santa rosa,6 +santana,1 +santiago,4 +santiago ixcuintla,2 +santo domingo,2 +santurce,1 +sarajevo,2 +sarasota,1 +sarlle,1 +sarllé,1 +sarmiento,1 +sarnia,5 +saronno,1 +saskatchewan,1 +saskatoon,7 +sassofono,2 +satellite,1 +satire,2 +saturday night fever,1 +saturdayselection,1 +saud al-shuraim,1 +saudades,1 +saudi,8 +saugatuck,1 +sausalito,1 +savannah,4 +savigliano,1 +savignano sul rubicone,1 +sax,2 +saxony,10 +saya,1 +sayda,1 +sayre,1 +saúde integral,1 +scanner,4 +scarborough,1 +schalgers,1 +schallenberg,1 +scheibe,1 +scheibenberg,1 +schenectady,1 +schirgiswalde-kirschau,2 +schkeuditz,6 +schkopau,4 +schkã¶len,2 +schkölen,2 +schlager,205 +schlager-pop,3 +schlagerhits,9 +schlageroldies,2 +schlagerrallye,1 +schlagerrallye 2018,1 +schlagers,1 +schlagerselection,2 +schleife,2 +schleswig holstein magazin,4 +schlettau,1 +schleußig,2 +schloß holte,1 +schloßchemnitz,1 +schneeberg,1 +school,1 +schranz,2 +schraplau,4 +schubert,2 +schule,1 +schullwitz,2 +schwany,9 +schwarzenberg,1 +schweiz,3 +schã¶ndorf,2 +schönau,1 +schöndorf,2 +schönefeld,2 +schönfeld,2 +sci-fi,8 +science,13 +science fiction,10 +scifi,4 +scooter,1 +score,3 +scotland,3 +scranton,6 +screamo,3 +scripture,1 +scripture reading,2 +sea sounds,2 +sean hannity,2 +seaquarium beach,1 +seashellradio,5 +seaside,1 +season,6 +seasonal,12 +seasonal/holiday,6 +seattle,5 +seattle sound,1 +sebastian demrey,1 +sebnitz,2 +secondlife,3 +secondradio,5 +secretaría de cultura,4 +seeben,4 +seegebiet mansfelder land,4 +seehausen,2 +seelitz,1 +sega,1 +segrate,1 +seidnitz,2 +seiffen,1 +selbitz,4 +selbstverwaltet,1 +select uk,1 +selection,1 +selective exposure,9 +sellerhausen,2 +semana santa,1 +semba,2 +semerkand,1 +sendedienst,1 +senegal,2 +seniors,1 +sensación,1 +sensual,5 +sensuale,1 +sentmenat,1 +seo,1 +serbia,1 +serbian,4 +serbian folk,2 +serbian music,2 +serbian pop,3 +serbski,1 +serie,1 +series tv,1 +sermons,3 +sertaneja,12 +sertaneja raiz,1 +sertanejo,44 +sertanejo raiz,21 +server,1 +services,1 +serviço integrado de saúde,1 +sessions,2 +sevdah,2 +sevdah 24 sata,2 +seventies,3 +sevilla,4 +sevilla fc,1 +sevilla futbol club,1 +sevillanas,1 +sex pistols,11 +sfax,1 +sfcr,1 +shabads,1 +shack e,1 +shag,1 +shallmar,1 +shalom132,1 +shania twain,1 +shanson,6 +shanties,1 +sharon,2 +shediac,1 +sheffield,1 +shenandoah,1 +shenandoaha valley,1 +shensi,4 +shetland islands broadcasting company,1 +shia,3 +shibuya kei,1 +shilat,1 +shoegaze,11 +shopping,3 +shoreditch,1 +short stories,1 +short story,1 +short wave,1 +shortwave,1 +show,2 +show musical,1 +show tunes,2 +showbiz,1 +shows,1 +showtunes,14 +shqip,5 +shreveport,5 +shuswap,1 +sieglitz,4 +siegmar,1 +siempre,2 +siena,1 +sierreño,10 +sigle tv,1 +siglufjordu,1 +sigma radio,2 +sigthwave,1 +siir,1 +sikh,1 +silao,3 +silberhã¶he,2 +silberhöhe,2 +silence,1 +silent,1 +silesia,3 +simon and garfunkel,1 +simple plan,1 +simply beautiful,1 +simply different,2 +simulator,2 +sinaloa,77 +sinatra,3 +sindicalismo,1 +singer songwriter,1 +singer-songwriter,48 +singer/,1 +singer/songwriter,5 +singer/songwriter (e.g. paul simon,6 +single,1 +singlecharts,4 +sinn,1 +sinnlos-telefon,1 +sioux center,1 +sioux falls,6 +sir.robo media,1 +siracusa,2 +siradio,1 +siradio.fm,1 +sistema de radio y televisión de nuevo león,6 +sistema de radiodifusoras culturales indígenas,8 +sistema público de radiodifusión,2 +sistema quintanarroense de comunicación social,3 +sistema radio lobo,1 +sitcom,1 +sitio das palmeiras,1 +sitka,1 +situationist,1 +sixties,8 +sj,1 +sk,1 +ska,43 +ska punk,3 +ska-punk,1 +sketches,1 +ski mountain,1 +skin,2 +skinhead,3 +skovoroda,1 +sky news,1 +skyline,1 +slap house,2 +slask,1 +slavonija,1 +slayer,1 +sleaze metal,1 +sleaze-/glamrock,1 +sleep,30 +sleep relaxation,2 +sleepingpill,3 +slide guitar,2 +sligo,1 +slipkinot,1 +slipknot,1 +slm,15 +slm alibisender,1 +slm sachsen,1 +slm-füllprogramm,1 +slovenia,1 +slovenian music,2 +slovenija,2 +slow,28 +slow jams,1 +slow jamz,2 +slow radio,4 +slow rock,19 +slow trance,1 +slowcore,1 +slowenien,1 +slowly musics,1 +slp,12 +sludge,1 +smash palace,1 +smith college,1 +smooth,73 +smooth country,1 +smooth jazz,210 +smooth lounge,24 +smooth reggae,3 +smooth rock,1 +smooth soul,2 +smoothest country,1 +smoothest instrumentals,1 +smoothest songs,10 +smoothjazz,6 +smothjazz,1 +smouth jazz,1 +snap!,1 +sneh bhakti,1 +sneh bhojpuri,1 +snow,2 +so,1 +soacha,2 +sobradinho,1 +sobro,1 +soca,20 +soccer,17 +soccer news,2 +social,66 +social activist programming,2 +social change,2 +socialismo,1 +sociedad,7 +society,7 +sofia,1 +soft,28 +soft adult contemporary,43 +soft beats,1 +soft blues,1 +soft jazz,4 +soft jazz – 100% vocal,1 +soft music,33 +soft pop,26 +soft rock,130 +soft sound,1 +sohbet,1 +sohbetleri,1 +sohland an der spree,2 +sol 89.1,1 +sol fm,1 +sol stereo,2 +solea,1 +soleil,1 +solidarität,1 +solo le migliori,1 +solo música en español,1 +somafm,4 +somali,3 +somerset,1 +somma vesuviana,1 +sommer,3 +sommerhits,2 +sommerradio,1 +somos vida radio,1 +son,1 +son huasteco,1 +son jarocho,1 +son montuno,1 +sonderkanal,1 +sondersendung,4 +sondersendungen,2 +sones,1 +song,1 +song hits,1 +song premieres,1 +song requests,4 +songs,8 +songs written by bob dylan,1 +sonidera,2 +sonido estrella,2 +sonne,1 +sonnenberg,1 +sonoma county,3 +sonora,53 +sonora grupera,1 +sophisti-pop,3 +sophisticated,6 +sophisticated underground music,1 +sora,1 +sosnoviy bor,1 +sosnovy bor,2 +soukous,3 +soul,345 +soul & great rock n' roll,1 +soul & great rock n' roll from the 60s & 70s,1 +soul funk,1 +soul jazz,1 +soul london music,2 +soul music,3 +soulful,11 +soulful house,31 +sound,1 +sound art,3 +sound of ocean,1 +soundart,2 +sounds,2 +sounds of nature,10 +sounds of the wild,1 +soundscape,4 +soundtrack,47 +soundtrack für die besten abende des jahres,1 +soundtrack/scores,2 +soundtracks,46 +south africa,1 +south asian,5 +south asian music,3 +south coast,1 +south india,9 +south indian,2 +south radio,1 +south wales,2 +south west london,1 +southbridge,1 +southeast ohio,1 +southeastern oklahoma radio,1 +southern gospel,8 +southern gothic,1 +southern hip-hop,1 +southern oregon university,1 +southern pines,1 +southern rap,1 +southern rock,15 +southern soul,2 +southernrock,1 +southland,1 +southwest wisconsin,1 +soverato,1 +sovereign grace music,1 +soviet,8 +soy fm,3 +sozial,1 +spa,1 +space,18 +space disco,1 +space music,4 +space program,1 +space rock,5 +spacemusic,1 +spacesynth,8 +spain,10 +spanglish,1 +spanish,35 +spanish adult contemporary,11 +spanish adult hits,42 +spanish broadcasting system,4 +spanish christian,6 +spanish contemporary hits,43 +spanish hits,4 +spanish oldies,17 +spanish pop,19 +spanish rock,1 +spanish talk,2 +sparkle,1 +sparse info,1 +spartenprogramm,1 +special,1 +special broadcast,2 +speech,22 +speed metal,1 +speedcore,1 +speeding songs,1 +spiritual,25 +spiritual balance,1 +spiritual discourses,1 +spirituality,3 +spitiritist,1 +splash,8 +split,3 +split infinity radio,1 +spokane,9 +spoken,3 +spoken art,3 +spoken word,34 +spoleto,1 +sport,188 +sport new,1 +sportfreunde stiller,1 +sporting events,1 +sports,287 +sports club radio,1 +sports commentary,7 +sports news,90 +sports radio,4 +sports talk,52 +sportsnet,1 +sports⚽⚽⚽🏈🏈🏀🏀,1 +spr,2 +spreetal,2 +springfield,7 +springsteen,1 +spugedelic trance,2 +sputnik mailbox,1 +squamish,1 +sr,13 +sr fernsehen,1 +sr info,1 +sr1,2 +srf,2 +srg,1 +srg ssr,37 +sri lanaka radio,1 +sri lanka,2 +srilanka radio,1 +st esprit,1 +st etienne,1 +st paul,1 +st-etienne,1 +st. augustine,1 +st. catharines,2 +st. charles,1 +st. helena,1 +st. john's,1 +st. louis,2 +st. mary's,1 +st. paul,9 +st. thomas,1 +staatsfernsehen,1 +staatsfunk,1 +stad en ommeland,1 +stadtradio,1 +stage,5 +stage and screen,3 +stahmeln,2 +stamps,1 +standalone,1 +standards,4 +standing rock,1 +standup,2 +stanford,2 +star,1 +star group,5 +star radio,21 +star trek,2 +stara zagora,1 +starmix,1 +stars on 45,1 +state,4 +state college,11 +statesville,1 +station,1 +stax,1 +steampunk,1 +steel guitar,1 +steely dan,1 +steffen lukas,4 +steigra,4 +steina,2 +steinfurt,1 +steinhagen,1 +steinigtwolmsdorf,2 +stelzendorf,1 +stendal,1 +stephanoise,1 +stephenville,2 +stereo 100,2 +stereo 91,1 +stereo cien,1 +stereo cristal,1 +stereo saltillo,1 +stereo sol,1 +stereo uno,9 +stereo vida,3 +stereorey,5 +sterpeto,1 +stetienne,1 +stettler,1 +stevens point,1 +stevie wonder,1 +stillwater,1 +sting,1 +stingray network,14 +stiri,2 +stockton,4 +stollberg,1 +stolpen,2 +stoner,6 +stoner rock,10 +stonerrock,1 +stonington,1 +stony brook,2 +stories,5 +storm lake,1 +story,1 +storyteller,1 +storytelling,35 +strahwalde,2 +straight from israel,1 +straight-ahead,8 +strandbar,1 +strauss,2 +stream,10 +stream restore,1 +streaming,2 +streektaal,1 +streema,1 +street,2 +street punk,1 +streisand,1 +stride,3 +striegistal,1 +striesen,2 +strings,13 +strohmann-argument,9 +străină și românească,1 +stuart,1 +student,21 +student radio,72 +student run,3 +student-managed,6 +students,3 +studio,3 +studio 54,2 +studio exa,3 +stukenbrock,1 +sturgeon bay,1 +stylish,1 +stã¶ãÿen,2 +stötteritz,2 +stößen,2 +stünz,2 +suave,5 +subasio,1 +subgenius,1 +subiaco,1 +subjektiv,1 +subway to sally,1 +sucessos,1 +sud,2 +sudamerica,1 +sudamericana,2 +sudbury,3 +suffolk,1 +sufi,3 +suisse,1 +suleymanpasa,1 +sulęcin,1 +sulęciński ośrodek kultury,1 +sumer,1 +sumerfm,1 +summer,12 +summer hits,3 +summer lounge,3 +summer radio,3 +summer tunes,6 +summerside,1 +summerville,1 +sumter,1 +sun,4 +sunda pop indonesia,3 +sundowner,3 +sunnah,11 +sunni,2 +sunny side of life,1 +suno fm,1 +sunrise avenue,1 +sunriver,1 +sunset,4 +sunshine,5 +sunshine pop,1 +suomi,2 +suomisaundi,3 +super,7 +super estelar,1 +super hits,3 +super rtl,2 +super stereo,1 +super stereo 96,1 +super stereo miled,3 +superhits,1 +superior,3 +supermix,1 +supernetwork,1 +support,1 +supremo,1 +sur,1 +suramérica,12 +sureste,77 +surf,20 +surf music,14 +surf rock,15 +surfrock,6 +surfside beach,3 +surrey,2 +survivalism,5 +suspense,4 +susquehanna valley,1 +sussex,2 +suva,2 +svate pismo,1 +svensk,1 +svensk folkmusik,1 +sveriges radio,28 +svoboda,1 +swamp rock,1 +swan river,1 +swedish,1 +sweet sour,1 +sweet'n'sour radio,1 +swing,62 +swiss,2 +swiss made,1 +switch,1 +swr,1 +swr fersehen,1 +sydney,11 +sylda,4 +sylvania,2 +symphonic,13 +symphonic death metal,2 +symphonic metal,11 +symphonic power metal,1 +symphonic rock,18 +symphonic-metal,1 +symphony,12 +synth,10 +synth pop,1 +synth-pop,11 +synthcore,2 +synthesizer,6 +synthie,1 +synthpop,44 +synthwave,22 +syracuse,3 +system of a down,12 +szanty,2 +szczecin,2 +szlagry,3 +sámi,1 +são tomé e príncipe,1 +são tomé island,1 +sã¼dharz,2 +sã¼dstadt,2 +sächsisch,1 +sächsische landesanstalt für privaten rundfunk und neue medien,5 +só se quiseres!,1 +só toca,1 +sólo música romántica,22 +söbrigen,2 +südharz,2 +südrang,1 +südstadt,2 +südvorstadt,2 +süleymanpaşa,1 +słubice,1 +t-rex,10 +tab,4 +tabasco,20 +tabatinga,1 +tabernáculo,1 +tachira,1 +tag,1 +tag by redbullet28,1 +tagalog,11 +tagesschau,2 +tagesschau24,7 +tagoresiedlung,1 +tags,1 +tahir buyukkorukcu,1 +tahir büyükkörükçü,4 +tahir büyükkörükçü radyo,1 +tahir büyükkörükçü sohbetleri,2 +taiwanese pop,1 +takaka,1 +take that,1 +talent,2 +tales,1 +talk,1404 +talk & speech,233 +talk back,1 +talk news,5 +talk radio,129 +talk show,50 +talk show radio,8 +talk shows,4 +talk variety,2 +talk-news,1 +talk. music,1 +talkback,6 +talke,1 +talking,3 +talking heads,10 +talkradio,2 +talks,9 +talkshow,12 +talksport,2 +tallahassee,3 +tallinn,1 +tamaki,1 +tamaulipas,38 +tamazight,3 +tamazunchale,2 +tamba,1 +tamil,29 +tamil christian radio,1 +tamil cinema music,18 +tamil fm,12 +tamil hits,6 +tamil melodies,1 +tamil melody songs,4 +tamil music,18 +tamil nadu,2 +tamil radio,5 +tamil songs,10 +tampa,3 +tampa bay,2 +tampa bay lightning,2 +tampico,15 +tandil,1 +tango,21 +tango nuevo,1 +tangos,1 +tangos y algo más,1 +tanm,1 +tantoyuca,1 +tantra,1 +tanz-,1 +taoism,1 +tapachula,3 +taquirari,1 +tarab,1 +taranaki,1 +tarandacuao,2 +taranto,1 +targoviste,1 +tarih,1 +tarling,1 +tarot,1 +tarragona,1 +tasavvf,1 +tasavvuf,1 +tasavvufi,1 +tataouine,1 +tatar,8 +tatarstan,4 +tauchritz,2 +taura,1 +tavern,1 +tawa,1 +taxco,2 +taylor swift,1 +tb,1 +tb-acc-hd,1 +tb-acc-low,1 +tbm,1 +tchaikovsky,1 +te conecta,29 +teaditional,1 +team,1 +teaneck,2 +teatru,1 +tec,1 +tecate,3 +tech,2 +tech house,64 +tech minimal,2 +tech news,1 +tech pop,2 +tech step,1 +techhouse,7 +technews,1 +technical death metal,1 +technik,1 +technikradio,1 +techno,305 +techno classics,1 +techno funk,1 +techno; indie; electro; alternative,1 +technohard,2 +technology,14 +technoypi,1 +techstep,4 +tecno,6 +tecno country 70 80 varios,1 +tecno pop,1 +tecnocumbia,1 +tecnomerengue,2 +tecomán,2 +tecuala,2 +teen idols,1 +teen pop,12 +teenies,1 +teens,7 +tegucigalpa,10 +tehuacán,2 +tejano,1 +tek house mix,1 +tekirdag,1 +tekirdağ,1 +tekk,1 +tekkno,1 +tekno,2 +tel aviv,1 +telefórmula,3 +televisa radio,2 +television,1 +television audio,1 +telugu,1 +tempe,1 +tempoal,1 +temuka,1 +tennessee vols,1 +tenosique,3 +teocelo,1 +teología,1 +tepic,14 +tequisquiapan,1 +teramo,1 +teresópolis,1 +terni,1 +terrace,1 +terre haute,4 +tertulia,1 +tertulias,4 +test,3 +teuchern,4 +teutschenthal,4 +texarkana,2 +texas,37 +texas country,4 +teykovo,1 +thai looktung,1 +thai pop,1 +thailanna,1 +thale,4 +thalebra,4 +thalheim,1 +thallwitz,2 +thalwinkel,4 +the,1 +the '70s,2 +the 70s,1 +the beach boys,1 +the beatles,2 +the bee gees,1 +the best dance music mix,4 +the best decade 80s radio around.,3 +the best of 80's,37 +the best of 90's,1 +the black eyed peas,1 +the bosshoss,1 +the bulls,1 +the byrds,1 +the doors,1 +the eagles,1 +the germans‘ favourite decade – and here you can hear clearly why. an extra dosis 80s rock with ac/dc,1 +the greaseman,2 +the holland project,1 +the home of real soul music,1 +the internet's #1 hit station,1 +the kinks,1 +the latest news from the national and international music scene,1 +the leite show,1 +the lower clarence valley,1 +the monkees,1 +the portugal corner,1 +the promise,1 +the rhythm of the people,1 +the ridge,1 +the rolling stones,3 +the seven best,2 +the sex pistols,1 +the sound of philadelphia,4 +the sound of silence,1 +the source of wisdom,2 +the spice girls,1 +the strongest,1 +the tourists,1 +the who,1 +the world´s greatest!,1 +the ’60s,1 +theater,14 +theatre,14 +thees uhlmann,1 +theißen,2 +theiãÿen,2 +thekla,3 +thessaloniki,1 +thetford mines,1 +thief river falls,1 +thimbleradio,5 +thom hartmann,2 +thonberg,2 +thousand palms,1 +thousend hits,3 +thrash,6 +thrash metal,14 +three decades of country,1 +thrissur,1 +throwback,1 +throwbacks,11 +thum,1 +thunder bay,3 +thüringen,1 +thüringen journal,6 +ticul,1 +tierra amarilla,1 +tierra blanca,2 +tigray,1 +tigrigna,2 +tijuana,23 +tik tok songs,1 +tikeur,1 +tikhvin,5 +tiki,1 +tiktok,4 +tilde,1 +tildeverse,1 +tim mcgraw,1 +timaru,1 +timba,4 +timeless,3 +timeless music,3 +timeless principles,1 +timișoara,1 +timmins,1 +tina turner,1 +tingle,1 +tipparade,2 +tirol,1 +tirunelveli,1 +tisbury,1 +tixtla,1 +tiziano ferro,1 +tizimín,3 +tizta,1 +tlalmanalco,5 +tlaxcala,7 +tlaxcala city,1 +tlc fm,1 +tm soundarajan,1 +tms,1 +toay,1 +today,5 +today's hits,4 +todays hits,5 +tofino,2 +toggo,1 +toggo radio,2 +tokyo,2 +tolaga bay,1 +toledo,7 +tolkewitz,2 +tolmezzo,3 +toluca,11 +tom jones,1 +tom petty,1 +tomares,1 +tonal,1 +tongan,1 +tonopah,1 +tony bennett,1 +too deep,1 +tool,1 +toolroom radio,1 +toowoomba,1 +top,105 +top 10,3 +top 100,106 +top 1000,1 +top 20,3 +top 200,1 +top 30,2 +top 40,820 +top 40 hits,82 +top 40 pop,2 +top 40/pop,2 +top 50,3 +top charts,127 +top hits,222 +top punjabi radio,1 +top-40,3 +top10,7 +top100,31 +top200,1 +top30,1 +top40,303 +topeka,5 +topical,3 +torah,1 +torino,8 +torna,2 +tornamexa,4 +toronto,30 +torreón,14 +torrões,1 +tosno,1 +totalitarismus,9 +toten hosen,1 +touhou,5 +toulouse,2 +tour vibration,1 +tourism,15 +tourist info,1 +town info,1 +towson,1 +tr,1 +trachau,2 +tracked,1 +tracy,1 +trad,1 +trade union,1 +tradicional,1 +tradition,1 +traditional,55 +traditional christian,4 +traditional country,4 +traditional folk,3 +traditional irish,1 +traditional mexican music,218 +traditional music,1 +traditional opera,6 +traditional pop,4 +traditional radio 95.0,1 +traditional rock and roll,3 +traffic,125 +traffic information,2 +traffic news,2 +traffic radio broadcast,61 +trafic...,13 +trafik,1 +training,2 +trance,229 +trance house,1 +trance und pop,1 +trance uplifting,3 +trancebase,1 +trancebase.fm,1 +trancepulse,1 +trancetechnic,1 +trancetechnic radio,1 +trans,3 +trans non-binary,9 +transgender,1 +translation,1 +trap,65 +trap & trill,1 +trash,2 +trash metal,2 +trash talk,2 +trashpop,2 +trasimeno,1 +traumfabrik,2 +traunstein,1 +traurig,1 +travel,12 +traveler,1 +traverse city,1 +travis county,1 +trebendorf,2 +trebnitz,4 +trebsen/mulde,2 +treffpunkt,5 +trenitalia,1 +trento,1 +trenton,4 +tri-cities,1 +tribal,1 +tribal house,3 +tribuna comunicación,4 +tribute radio,1 +trichy,1 +trieste,6 +trillwave,1 +trip hop,14 +trip-hop,19 +triphop,10 +tripoli,1 +trissino,1 +trión,5 +trión nights,2 +trión weekend,2 +trockenborn-wolfersdorf,4 +tropial,1 +tropical,208 +tropical house,5 +tropical lattina,1 +tropical music,41 +tropical; cross over,1 +tropicalia,1 +trotha,4 +trova,1 +trova yucateca,1 +troy,2 +troyes,1 +truck-driving country,2 +truckerhits,1 +trumpeth,2 +truth,1 +truth about the world. pravda o svete.,1 +truth for life,1 +tschechien,1 +tschechow,4 +tsm,1 +tsn,2 +tu estación,1 +tu música hoy,1 +tu radio diocesana,1 +tuba stereo,1 +tube tamil,1 +tubes actuels,13 +tucano fm,1 +tucson,6 +tuga,1 +tula,1 +tulancingo,1 +tulle,1 +tulsa,1 +tultitlán,1 +tulum,1 +tunisia,1 +tunja,1 +turbo,1 +turbo folk,1 +turismo,1 +turismo mexico,1 +turk,4 +turkey,6 +turkish music,16 +turkish pop,9 +turkiye,2 +turkısh rap,1 +turlock,3 +turnup is real,1 +tus favoritas,1 +tuxpan,3 +tuxtepec,3 +tuxtla,3 +tuxtla gutiérrez,8 +tuzla,1 +tv,91 +tv audio,1 +tv halle,1 +tvr comunicaciones,1 +twang,3 +tweens,2 +twente,1 +twist,9 +twitter,2 +tx,2 +tàrrega,1 +témoris,1 +tónleikur,2 +türk dili,1 +türk edebiyatı,1 +türk halk müziği,2 +türk müziği,1 +türk pop,1 +türk pop müziği,1 +türk sanat,1 +türkçe,4 +türkçe rap,2 +türkü,5 +türkü radyo,2 +türküler,3 +u.k. comedy,2 +u2,3 +ua,1 +uab artvydas,1 +uab aukštaitijos radijas,1 +uab centro medija,1 +uab geruda,1 +uab interbanga,1 +uab labas,1 +uab laisvoji banga,1 +uab m-1,4 +uab proarsa,1 +uab radijas kelyje,1 +uab radijo stotis laluna,2 +uab radiola,2 +uady,1 +uae,1 +uas,1 +ubc,1 +uc berkeley,1 +uc davis,1 +ucd,1 +udo lindenberg,1 +ufa,2 +ufo,3 +uganda,1 +uildm,1 +uk,8 +uk bass,3 +uk dub,1 +uk funky,2 +uk garage,12 +uk grime,2 +uk hardcore,1 +uk hip-hop,1 +uk house,1 +uk rap,1 +uk rock,1 +uk synthpop,5 +ukedm,1 +ukg,1 +ukraine,3 +ukrainian,14 +ukrainian rock,3 +ukulele,1 +ukw,9 +ulldecona,1 +ulm,2 +ulrichshalden,4 +ultimate variety,2 +ultimative charthits,1 +ultimative chartshits,1 +ultra,4 +ultra gym & fitness,1 +ultra stacion,1 +umbra,1 +umbria,2 +un,1 +un plugged,1 +unabhängige medien,1 +unam,7 +uncensored,4 +unconventional,1 +undefined,15 +undeground,2 +underground,40 +underground music,3 +underwater,1 +ungarn,1 +uniradio,10 +unitarian universalist church,1 +united arab emirates,1 +united kingdom,2 +united labour party,1 +united nations,1 +universal,14 +universidad,20 +universidad autónoma de aguascalientes,1 +universidad autónoma de durango,1 +universidad autónoma de méxico,6 +universidad autónoma de occidente,1 +universidad autónoma de san luis potosí,1 +universidad autónoma de sinaloa,1 +universidad autónoma de tamaulipas,1 +universidad de guadalajara,1 +universidad de la plata,1 +universidad de monterrey,1 +universidad de sonora,1 +universidad nacional de misiones,1 +universidad stereo,1 +universidad tecnologica nacional,2 +universitaire,4 +universitaria,19 +university,42 +university heights,2 +university of california,1 +university of minnesota,1 +university of nebraska,1 +university of richmond,1 +university radio,214 +universitário,1 +unlp,2 +unplugged,6 +unq,1 +unsigned,3 +unsterbliche klassiker,1 +unstruttal,4 +unterhaltung für den norden,1 +unvierzitet,1 +unwimd,1 +unwind,12 +up,2 +up tempo house,2 +up to date,2 +upbeat,3 +upes,1 +uplifting,8 +uplifting music,1 +uplifting trance,11 +uplink network gmbh,5 +upper montclair,1 +uptempo,1 +uptempo hardcore,2 +urak tanah ulu,1 +urban,127 +urban adult contemporary,37 +urban alternative,1 +urban contemporary,67 +urban cowboy,1 +urban gospel,2 +urban indigenous,1 +urban jazz & neo-soul,1 +urban music,7 +urban oldies,1 +urbana,15 +urbanism,1 +urbano,74 +urbano 106,1 +urdu,3 +urlaubsradio,1 +urrao,1 +uruapan,3 +uruguay,5 +us,3 +us-rap,1 +usa,57 +usaku,1 +usaku fm,1 +usaku radio,1 +usher,1 +usmate velate,1 +usoara,1 +usrap,1 +ussr,2 +utah,1 +utica,2 +utilidade,1 +utility,1 +utn,3 +uturn,2 +uva 90.5,1 +v-disc,4 +v4v,1 +vaikhari,4 +vail,1 +valanci media group,4 +valle,1 +valle de bravo,1 +valle de méxico,139 +valle di cadore,1 +vallenar,7 +vallenata,2 +vallenato,103 +valls,1 +van alles,1 +van halen,3 +vancouver,10 +vaporware,1 +vaporwave,16 +vapourwave,1 +vaquejada,1 +var,2 +variada,27 +variado,41 +variados,12 +varied,17 +variedad,36 +variedad- espiritual,1 +variedad-espiritual,1 +variedades,7 +varieties,3 +variety,486 +variety hits,57 +varios,30 +various,136 +various music,4 +various pop hits oldies easylistening nederlands,1 +variété,3 +vastelaovend,1 +vaudeville,1 +vbr,1 +vbs,1 +vcienseña,1 +vedic,2 +vegas lounge music,1 +venezia,3 +venezuela,5 +vengaboys,1 +veracruz,79 +veracruz city,14 +vercelli,1 +verdinha,1 +verkehr,1 +verl,1 +vermillion,2 +vernon,1 +veroia,1 +verona,2 +versmold,1 +vesterålen,1 +vesti,1 +vetenfm,1 +vhsa,3 +viata satului,1 +vibe fm,4 +vibes,6 +vibo valentia,1 +vibra fm,2 +vibra radio,2 +vibration,1 +vibrez,1 +vibrons,1 +vic,1 +vicenza,1 +vichy,1 +victoria,5 +vida,5 +vida romántica,3 +video game,13 +video game music,19 +videogame music,17 +videogames,7 +vidéo,1 +viejitas,3 +vielfalt,1 +vigie,1 +viking,2 +viking metal,1 +vilablareix,1 +vilanova del camí,1 +vilanova i la geltrú,1 +villa,2 +villa hidalgo,1 +villafalletto,1 +villaflores,1 +villafranca de los barros,1 +villahermosa,20 +villavicencio,1 +vinilo fm,1 +vino,1 +vintage,20 +vintage cassettes,1 +vintage comedy,4 +vintage country,2 +vintage music,9 +vintage radio,3 +vintage records,1 +vinyl,12 +viola,2 +violin,7 +virden,1 +viroqua,1 +visitmexico,1 +visual kei,1 +vivaldi,2 +vive fm,8 +vividh bharti pune,1 +vividhbharti,1 +vixa,2 +vladikavkaz,8 +vlaska uzivo,1 +voa,1 +vocal,29 +vocal deep,8 +vocal house,9 +vocal jazz,33 +vocal lounge,4 +vocal pop,5 +vocal trance,11 +vocaloid,8 +voces,3 +vocm network,4 +vocoder,1 +vogtlandradio,2 +voice,12 +voice of greece,1 +voice of the hyaks,1 +voiceofgreece,1 +voices,2 +voix des sans voix,1 +volk,1 +volkmarsdorf,2 +volksmusik,52 +volksmusik instrumental,1 +volksmusik oldies,1 +volksmuziek,1 +volkstümlich,8 +volkstümlicher schlager,3 +volunteer,6 +vos,1 +vox,9 +vox fm,6 +vox love station,1 +vox radio hits,4 +voxpop,1 +voyage,1 +voyageurs,1 +vpr,4 +vrishti,1 +vrt,15 +vtuber,1 +vyborg,4 +vzfdm,1 +všį šou imperija,1 +w deportes,2 +w radio,23 +w0kie,1 +wabush valley,1 +wachwitz,2 +wacken,15 +wacken open air,12 +wacken radio,10 +wagner,1 +wahren,2 +wainwright,2 +wairoa,1 +waldheim,1 +walla walla,1 +walled lake,1 +wallhausen,4 +wallonia,2 +wallwitz,4 +walm,27 +walterboro,1 +wanderlust,1 +wapakoneta,1 +wapmahka,5 +warehouse house,1 +wartime music,5 +wasaga beach,2 +washington dc,5 +waskom,1 +waterbury,1 +waterloo,3 +wave,7 +wavefarm,2 +wavemix,1 +waves radio,1 +waving,1 +wawa,1 +wayne,2 +waynesboro,75 +wced,1 +wcpa,1 +wdr,7 +wdr2,1 +wdr2 schlagerrallye,1 +wdsn,1 +we play again,1 +weather,43 +weather radio,7 +web,3 +webcantares,1 +webeo,1 +webradio,23 +webradio cantares,1 +webradiocantares,1 +webradios,1 +website down - twitter page only,1 +webster,1 +wechselburg,1 +weed,1 +weert,1 +weihnachen,1 +weihnachten,29 +weihnachtslieder,3 +weihnachtsmusik,2 +weihnachtsradio,2 +weihnachtsrock,3 +weihnachtsstream,1 +weißenberg,2 +weißenborn,1 +weißenfels,2 +weißer hirsch,2 +weißwasser,2 +weiãÿenfels,2 +welbsleben,4 +welcome to generation yes,1 +wellbeing tips,1 +wellington,1 +wellness,2 +welsh language,2 +welt,4 +welt tv,1 +wendezeit,2 +werave music,1 +werbefrei,18 +werther,1 +west coast,11 +west coast hip-hop,1 +west coast rap,1 +west end,9 +west hampton,1 +west indies,1 +west lafayette,2 +west lothian,1 +west middlesex,1 +west point,1 +west virginia,3 +west-end,3 +westaskiwin,1 +westcoast,1 +westend,1 +westerly,1 +western,4 +western australia,1 +western kentucky university,1 +western swing,2 +westernarmenia,1 +westerns,1 +westewitz,1 +westlock,1 +wetaskiwin,1 +wethau,4 +wetter,1 +wetter und verkehr,1 +wetterzeube,4 +wettin,4 +weymouth,1 +wfmu,1 +whakatane,2 +wham,2 +whataboutism,9 +whatever,1 +whatsapp exa,1 +white machine,4 +whitecourt,1 +whitehorse,1 +whitesnake,1 +whiteville,1 +whitney houston,1 +wi,2 +wichita,1 +wichita falls,4 +wickenburg,1 +widerhall,1 +wieczorna audycja z komentarzem,1 +wiedenbrück,1 +wiederitzsch,2 +wiednitz,2 +wiehe,4 +wiesn,2 +wiht,2 +wild west,1 +wilde mischung diverse metal-stilrichtungen inkl. ruhigerer songs im wechsel,1 +wilkes-barre,3 +willemstad,3 +williamsport,1 +willow spings,1 +wilmington,5 +wilsdruff,2 +wilthen,2 +wimberley valley radio,1 +windsor,5 +wingham,3 +winnebago,1 +winnipeg,8 +winona,1 +winter,4 +winter harbor,2 +winters,1 +wioq,1 +wioq-fm,1 +wippra,4 +wisconsin,1 +wisconsin badgers,2 +wissen,4 +wissenschaft,4 +witch house,6 +witchhouse,2 +without computer generated music rotation,1 +wittgensdorf,1 +wittichenau,2 +wivelliscombe,1 +wixl,1 +wixl-lp,1 +wkql,1 +wku,1 +wladimir putin,1 +wmkx,1 +wncu,1 +woa,12 +woken,1 +wokeness,9 +wolf pack radio,1 +wolferstedt,4 +wolkenstein,1 +woman,1 +women,7 +women's history,1 +women's issues,2 +woodstock,2 +woozy electronica,1 +worcester,6 +worcestershire,1 +word,1 +word of god,1 +work,2 +working,6 +workout,20 +worktime music,1 +worl-music,1 +world,76 +world beats,2 +world ethnic,1 +world folk,2 +world fusion,1 +world hits,3 +world hits musics,3 +world middle east,38 +world music,232 +world news,56 +world news channels,1 +world of blaze,1 +world radio,3 +world wide ethno music & reports,1 +world-music,1 +worldbeat,15 +worlds best classic hits,1 +worlds finest music,1 +worship,34 +worship through song,1 +wow,1 +wpac,1 +wrfl,1 +wurzen,2 +wvgm,1 +ww ii,1 +ww2,1 +wwch,1 +wwu,1 +www.80smusic.digital,1 +wxpn,1 +wzrk,1 +wã¶rmlitz,2 +wã¼lknitz,2 +wã¼nschendorf/elster,2 +wörmlitz,2 +wülknitz,2 +wünschendorf/elster,2 +włocławek,1 +x-mas,4 +xalapa,7 +xbox,1 +xeu,5 +xeva,1 +xh medios,1 +xiomara cumple,1 +xmas,17 +xmas rock,3 +xn radio,3 +xnavidad,2 +xponential radio,2 +xy la rompebocinas,1 +xyrem,1 +y fm,1 +y2k,1 +ya! fm,5 +yacht rock,7 +yakima,1 +yamba,1 +yaroslavl,4 +yasser aldosari,1 +yautepec,1 +yearmix,1 +yes fm,1 +yeshua,1 +yesudas,1 +yesudoss,1 +yfm,1 +yiddish,1 +yle radio suomi,1 +yle sápmi,1 +ynop,1 +yo punjabi,1 +yo soy,1 +yoga,13 +yogyakarta,1 +yordi rosado,8 +york,1 +york region,1 +yorkton,2 +you with,1 +young,1 +young adults,6 +youngstown,8 +your multi,1 +your number one radio,1 +youradio,2 +yourclassical,1 +youth,74 +youth programming,3 +youth programs,3 +ypsilanti,1 +yu pop rock,1 +yucatan,15 +yucatec,2 +yucatec maya,2 +yucatán,26 +yuma,2 +yuriria,1 +z88,1 +zabavna,9 +zabavna - 90s,1 +zabavna - folk,1 +zabavna - lagana,1 +zabavna - narodna,9 +zabavna - narodna - folk,1 +zabavna - narodna - mix - ex-yu,1 +zabavna - pop - rock,3 +zabavna - pop-rock,1 +zabavna - rock,1 +zabavne,1 +zacapu,1 +zacatecas,27 +zacatecas city,2 +zagreb,3 +zakopane,1 +zambales,2 +zambian music,1 +zamora,3 +zanesville,1 +zappendorf,4 +zaracay,1 +zarate,1 +zarzuela,1 +zdf,7 +zdf hd,1 +zdf info,1 +zdf-“maybrit illner“,1 +zdfneo,1 +zdftivi,1 +zeckenfunk,9 +zeitgeschehen,3 +zeitz,4 +zempeikiko,1 +zen,13 +zen ambient,3 +zensurfreier raum,1 +zet hit,1 +zet impreza,1 +zet latino,1 +zet na swieta,1 +zet po polsku,1 +zet pop rock,1 +zettlitz,1 +zeuhl,3 +zihuatanejo,2 +zina,1 +zinios,1 +zinios is lietuvos,1 +ziniu radijas,1 +ziniu radijas lietuva,1 +ziniu radijas vilnius,1 +zion multimedia,1 +zitate,1 +zitouna,1 +zittau,5 +zitácuaro,1 +zivilgesellschaft,1 +zona tres,1 +zongolica,1 +zouk,12 +zschertnitz,2 +zschopau,1 +zumpango,2 +zweites deutsches fernsehen,3 +zwenkau,2 +zydeco,3 +zz top,2 +zz top and many more will blow your mullets away!,1 +à l'ancienne,1 +áfrica,1 +água grande,1 +álamo,3 +álamo temapache,2 +álvaro obregón,1 +ã¶ffentlich-rechtlich,2 +ä,1 +ärzte,1 +åland islands,1 +çocuk,2 +çocuk radyosu,2 +çocuk şarkıları,2 +éclectique,1 +électro,1 +électronic,1 +éxitos,26 +éxtasis digital,6 +ópera,2 +öffentlich-rechtlich,17 +östergötland,1 +última hora,1 +últimas noticias,1 +český rozhlas,15 +čítanie,1 +łemko,2 +łużyce,1 +łužica,1 +łužyca,1 +śląsk,1 +şairler,1 +şeyh galip,1 +şiir,1 +šibenik,1 +știri,1 +δημοτικά,1 +εκκλησία,14 +ελλάδα,1 +ερα,1 +ερτ,1 +ερτ 3,1 +ιερά μητρόπολις,10 +ιερά μονή,3 +ιερός ναός,1 +λαϊκά,1 +νησιώτικα,1 +ορθοδοξία,19 +παλιά λαϊκά,1 +παραδοσιακά,1 +ρεμπέτικο,1 +ροκ - adult rock,1 +χριστιανισμός,4 +авторская песня,3 +алтай,1 +альтернатив,1 +анекдот,1 +аниме,1 +аудиокниги,6 +бард,2 +бард-рок,2 +барнаул,2 +без,1 +биробиджан,1 +блюз,7 +вести,3 +военно-патриотическая,4 +воронеж,1 +вяра,2 +гачи,2 +гачи ремикс,1 +гачимучи,1 +говорит,1 +голос,1 +голос свободи,1 +гоп,1 +гопфм,1 +городское радио,1 +гтрк,1 +ддт,2 +детское,4 +детям,1 +джаз,24 +дип хаус,6 +диско,2 +дисскусии,1 +драм-н-бэйс,2 +евангельские христиане баптисты,2 +европа,1 +ехб,1 +знакомства,1 +игровое,1 +инди,1 +инди-рок,1 +информационное,8 +информационный,7 +каверы,3 +казан,1 +казань,1 +кино,2 +классика,1 +классическая музыка,10 +клуб,2 +клубная,9 +лëгкая,1 +лëгкая музыка,29 +ламбада,1 +лаундж,1 +липецк,1 +литература,1 +львівська хвиля,1 +любэ,3 +магнитогорск,1 +мандрівне,1 +маруся,1 +мв,1 +мдс,1 +местные новости,2 +металл,5 +металлика,1 +миронов,1 +модель для сборки,1 +модное,1 +музыка,13 +народне,2 +наука,1 +научно-познавательное,1 +національне,1 +нація,1 +новосибирск,1 +новости,9 +обьявления,1 +онлайн-радіо,1 +панк,1 +партия,1 +плюс,1 +политика,3 +поп,9 +поп-музыка,210 +популярная музыка,2 +популярное!,1 +православие,1 +проповеди,1 +радио в стиле фреш - свежее,1 +радиосити,1 +радиосититюмень,1 +разговор,1 +разговорное,16 +разговорный,15 +разная музыка,11 +разное,4 +рассказы,2 +расслабляющая,3 +реггей,1 +регги,1 +релакс,1 +ретро,49 +рок,52 +рок-н-ролл,1 +романс,1 +романтическая,2 +россия,3 +ростовская область,1 +русская популярная музыка,8 +русский,3 +русский рок,5 +русский шансон,2 +русское,1 +рэп,7 +свобода,1 +сердце,1 +сковорода,1 +смешанный,5 +современная музыка,2 +соул,2 +спорт,1 +справедливая,1 +танцевальная,53 +танцевальная музыка,6 +танцевальный,1 +татар,1 +татарстан,1 +татары,1 +техно,1 +томск,2 +транс,3 +трк вс рф,2 +трк вс рф военно-патриотическая,2 +трэп,1 +тюмень,1 +україна,1 +україномовне,3 +урюпинск,1 +уфа,2 +фанк,2 +фантастика,2 +фолк,20 +фолк-рок,1 +фронтовое радио,1 +хип-хоп,11 +хит fm,10 +хиты,2 +хиты 80-х,3 +хиты 90-х,3 +хиты прошлых лет,3 +христианская радиостанция,1 +християнство,2 +цензуры,1 +цой,2 +чилаут,17 +шансон,17 +электроника,1 +электронная,34 +электронная музыка,8 +эмигрантское,1 +этника,1 +этно,4 +юмор,10 +דתי,8 +ישראל,1 +מזרחי,1 +أخبار الآن,1 +أغاني و معزوفات موسيقية من روائع الموسيقى العربية,1 +إيناس,6 +اسلام,1 +اسلامي,1 +اغاني كلاسيكيه,1 +الآن,1 +الأمارات,1 +الإذاعة الامازيغية,1 +الإذاعة الامازيغية | snrt,1 +الإذاعة الوطنية المغربية,1 +البلاد,1 +الجزيرة,1 +السديس,1 +السعودية,2 +القرآن,12 +القرآن الكريم,13 +الموسيقى العربية,2 +بودكاست,1 +دبي,1 +دين,1 +راديو الآن,1 +راديو القرآن,11 +رادیو,1 +رادیو مجاهد,1 +رواي,2 +رواية ورش عن نافع,1 +زيارات,1 +زيارت,1 +سازمان مجاهدین خلق ایران,1 +سعود الشريم,1 +سورة البقرة,2 +طرب,1 +عبدالباسط,1 +عبدالرحمن,1 +عبدالصمد,1 +علي الحذيفي,1 +فارسی,1 +قرآن,1 +قراء,1 +قران كريم,1 +قناة الجزيرة,1 +كتاب صوتي,5 +كتاب صوتيview-source:https://de1.api.radio-browser.info:443/json/add?name=radio enas,1 +كلاسيك,1 +ماهر المعيقلي,3 +محمد رفعت,1 +مسلم,1 +مشاري العفاسي,2 +مصر,2 +ياسر الدوسري,1 +คลื่นขาว จาก ชาวพุทธ,1 +คลื่นฮิตอันดับ 1 ของชาวขอนแก่น,1 +ซันไชน์ เรดิโอ พัทยา,1 +มูลนิธิศึกษาและเผยแพร่พระพุทธศาสนา,1 +ลูกทุ่ง ทั้งวัน ทังคืน,1 +ลูกทุ่ง เพื่อชีวิต,1 +วัดป่าไทรงาม กำแพงเพชร (fm 98.25 mhz),1 +วิทยุออนไลน์ วัดป่าไทรงาม (fm 98.25 mhz),1 +สังฆทานธรรม คลื่นขาว จาก ชาวพุทธ,1 +อสมท อุดรธานี,1 +เพลงลูกทุ่ง,1 +„landmarken“,1 +„pariser platz“,1 +„tonart.e“,1 +„zeit-forum der wissenschaft“,1 +☕,1 +⚽🏀🏈🏀🏑🏸🎾🥅🎾,1 +⚽🏀🏈🏑🌏🏴󠁧󠁢󠁥󠁮󠁧󠁿,1 +❤️,2 +いわき市,1 +さいたま市,1 +コミュニティfm,1 +乡村音乐,1 +二戸,1 +亚洲音乐96.5,1 +交通,2 +交通广播,1 +京丹後市,1 +京都市,1 +京都府,1 +仙台,2 +俄罗斯动漫广播电台,1 +八戸市,1 +八王子,1 +前橋,1 +加古川市,1 +北九州市,1 +华人,1 +古典音乐,1 +名取,1 +名護市,1 +哈尔滨,3 +哈尔滨电视台,3 +国家台,7 +地方台,11 +塩竈,1 +声音客栈,1 +多古町,1 +大島郡,1 +大连少儿广播,1 +天主,1 +奄美市,1 +娱乐,1 +宜野湾市,2 +宮古,1 +岡崎,1 +川口,1 +川崎,1 +延岡市,1 +延边,1 +影视,1 +徳島市,1 +快乐卷卷猫,1 +怀旧,1 +慈溪经典车电台,1 +成都,1 +手段はラジオ,1 +播客,1 +教育,1 +敦賀市,1 +文艺,1 +新闻,11 +日立,1 +朝霞,1 +本地,2 +本宮,1 +東近江市,1 +横手,1 +気仙沼,1 +水戸,1 +江東,1 +汽车音乐898,1 +沖縄市,1 +沧州音乐广播,1 +流行音乐,1 +海南旅游广播,1 +海老名,1 +深谷,1 +湯沢,1 +潮台969,1 +牛久,1 +生活,2 +电影,2 +电视,10 +电视伴音,11 +电视剧,1 +盛岡,1 +直方市,1 +石垣市,1 +石巻,1 +福岡市,1 +秋田,1 +立川,1 +米子市,1 +粤语,1 +紐約華語廣播,1 +经典946,1 +经典调频,1 +综合,9 +羊城交通广播,1 +聊天,1 +职场,1 +芒果时空音乐台,1 +調布,1 +读书,1 +谈话,1 +豊岡市,1 +豊見城市,1 +越谷,1 +辽宁广播电视台,2 +郡山,1 +都市,1 +野々市,1 +银色年华,1 +長岡京市,1 +音乐,7 +高萩,1 +鴻巣,1 +鹿角,1 +黒部市,1 +종합,1 +🇨🇮,1 +🇯🇲🇯🇲,1 +🇵🇰,2 +🌀,1 +🌍news🌏,1 +🌍🌍🌍,3 +🌍🌍🌍 uganda,1 +🌍🏰🌏,1 +🌎 world news,1 +🌎🌎🌎,2 +🌎🌎🌎🇶🇦,1 +🌎🏴󠁧󠁢󠁥󠁮󠁧󠁿🌎,1 +🌏🌍🌍,1 +🌏🌏🇺🇸🇺🇸,1 +🌏🌏🌍,1 +🌐🌐🇺🇸🇺🇸,1 +🌲🌲🌴🌲🌲🍇🍎🍓,1 +🌴🌴🌴,9 +🌿,1 +🍇🍇🍇,3 +🍇🍇🍇🍇,1 +🍇🍓🍇🍓🌴,1 +🍎🌴🍓🌴🍎🪷,1 +🍎🍎🍎🍎,1 +🍓🍎🍎,1 +🍓🍓🌴🍎🌺,1 +🍓💐🍇🌺🪷🌎🌲🌍🌷,1 +🎆🌲🌲🎆🌲,1 +🎆🎎,1 +🎶,1 +🏰,2 +🏴󠁧󠁢󠁥󠁮󠁧󠁿⚽⚽⚽⚽🏈🏀,1 +💐💐💐,6 +📡,1 +🔄,1 +🔧📻🌐,1 +🕺,1 +🗺️🌐🏴󠁧󠁢󠁥󠁮󠁧󠁿,1 +😃🎆😃🎆🌲🌷,1 +🪷🌷🌸🪷🌺💐,1 +🪷🪷🪷,1 diff --git a/tags.ods b/tags.ods new file mode 100644 index 0000000..b5c04ed Binary files /dev/null and b/tags.ods differ diff --git a/tuner.mm b/tuner.mm index 3f59806..5080712 100644 --- a/tuner.mm +++ b/tuner.mm @@ -1,8 +1,8 @@ - + - + @@ -71,52 +71,58 @@ - + - + - + - - + + + - - - - - + + + - + - + + - - - - - - - + + + + + + + + + + + + + - + - + - - - - - + + + + + @@ -125,10 +131,10 @@ - + - - + + @@ -142,15 +148,15 @@ - - + + - + - - + + @@ -171,20 +177,28 @@ - - - + + + + + + + + + + + - + - - + + - + @@ -194,11 +208,11 @@ - - + + - - + + @@ -209,17 +223,61 @@ - - - - - - + + + + + + + + + +

+ Kicks off the application and return the running instance +

+ +
+
+ + + + + + + + +

+ Configures the application, +

+

+ Constructs loads saved settings, sets the cache and data directoriesnew PlayerController +

+

+ Activates new Window, DBus
+

+ +
+ + + + + + + + +

+ Play (stream & audio) controller +

+ +
+
+ + + + + - - -