mirror of
https://github.com/talwat/lowfi
synced 2025-12-31 19:13:21 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9acfcdcf9d | ||
|
|
cd002ef9ab | ||
|
|
6b65b7d952 | ||
|
|
6802db1a1e | ||
|
|
fa236439e3 | ||
|
|
0dc3eddab7 |
29
CHILLHOP.md
29
CHILLHOP.md
@ -1,29 +0,0 @@
|
|||||||
# Using the chillhop list
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> As of lowfi 1.7.0, the chillhop list is included by default. For a more
|
|
||||||
> detailed explanation, see [MUSIC.md](MUSIC.md). This document is included
|
|
||||||
> to preserve any old links or references. The instructions are still valid.
|
|
||||||
|
|
||||||
## Linux
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir -p ~/.local/share/lowfi
|
|
||||||
curl https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillhop.txt -O --output-dir ~/.local/share/lowfi
|
|
||||||
```
|
|
||||||
|
|
||||||
## MacOS
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir -p "$HOME/Library/Application Support/lowfi"
|
|
||||||
curl https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillhop.txt -O --output-dir "$HOME/Library/Application Support/lowfi"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Windows
|
|
||||||
|
|
||||||
Go to `%appdata%` in Explorer, then `Roaming`, and make a folder called `lowfi`.
|
|
||||||
Then just put [this file](https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillhop.txt) in there.
|
|
||||||
|
|
||||||
## Launching lowfi
|
|
||||||
|
|
||||||
Once the list has been added, just launch `lowfi` with `-t chillhop`.
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
# Contributing to lowfi
|
# Contributing to lowfi
|
||||||
|
|
||||||
|
[[Version française](./docs/fr/CONTRIBUER.md)]
|
||||||
|
|
||||||
There are a few guidelines outlined here that will make it more likely for your PR to be accepted.
|
There are a few guidelines outlined here that will make it more likely for your PR to be accepted.
|
||||||
Only ones that are less obvious are going to be listed. If you need to ask, it's probably a no.
|
Only ones that are less obvious are going to be listed. If you need to ask, it's probably a no.
|
||||||
|
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1311,7 +1311,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lowfi"
|
name = "lowfi"
|
||||||
version = "2.0.1-dev"
|
version = "2.0.2-dev"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lowfi"
|
name = "lowfi"
|
||||||
version = "2.0.1-dev"
|
version = "2.0.2-dev"
|
||||||
rust-version = "1.83.0"
|
rust-version = "1.83.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "An extremely simple lofi player."
|
description = "An extremely simple lofi player."
|
||||||
@ -18,7 +18,7 @@ homepage = "https://github.com/talwat/lowfi"
|
|||||||
repository = "https://github.com/talwat/lowfi"
|
repository = "https://github.com/talwat/lowfi"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
mpris = ["dep:mpris-server"]
|
mpris = ["dep:mpris-server", "dep:arc-swap"]
|
||||||
extra-audio-formats = ["rodio/default"]
|
extra-audio-formats = ["rodio/default"]
|
||||||
scrape = [
|
scrape = [
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
@ -38,7 +38,7 @@ thiserror = "2.0.12"
|
|||||||
# Async
|
# Async
|
||||||
tokio = { version = "1.41.1", features = ["macros", "rt", "fs", "io-util", "sync", "time"], default-features = false }
|
tokio = { version = "1.41.1", features = ["macros", "rt", "fs", "io-util", "sync", "time"], default-features = false }
|
||||||
futures-util = { version = "0.3.31", default-features = false }
|
futures-util = { version = "0.3.31", default-features = false }
|
||||||
arc-swap = "1.7.1"
|
arc-swap = { version = "1.7.1", optional = true }
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
reqwest = { version = "0.12.9", features = ["stream", "http2", "default-tls"], default-features = false }
|
reqwest = { version = "0.12.9", features = ["stream", "http2", "default-tls"], default-features = false }
|
||||||
|
|||||||
10
README.md
10
README.md
@ -1,15 +1,17 @@
|
|||||||
# lowfi
|
# lowfi
|
||||||
|
|
||||||
|
[[Version française](./docs/fr/README.md)]
|
||||||
|
|
||||||
lowfi is a tiny rust app that serves a single purpose: play lofi.
|
lowfi is a tiny rust app that serves a single purpose: play lofi.
|
||||||
It'll do this as simply as it can: no albums, no ads, just lofi.
|
It'll do this as simply as it can: no albums, no ads, just lofi.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
As of the 1.7.0 version of lowfi, **all** of the audio files embedded
|
As of the 1.7.0 version of lowfi, **all** of the audio files embedded
|
||||||
by default are from [chillhop](https://chillhop.com/). Read
|
by default are from [chillhop](https://chillhop.com/). Read
|
||||||
[MUSIC.md](MUSIC.md) for more information.
|
[MUSIC](./docs/MUSIC.md) for more information.
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
|
|
||||||
@ -145,7 +147,7 @@ and as such are also stored in the same directory.
|
|||||||
### Extra Flags
|
### Extra Flags
|
||||||
|
|
||||||
If you have something you'd like to tweak about lowfi, you use additional flags which
|
If you have something you'd like to tweak about lowfi, you use additional flags which
|
||||||
slightly tweak the UI or behaviour of the menu. The flags can be viewed with `lowfi --help`.
|
slightly tweak the UI or behavior of the menu. The flags can be viewed with `lowfi --help`.
|
||||||
|
|
||||||
| Flag | Function |
|
| Flag | Function |
|
||||||
| ----------------------------------- | --------------------------------------------------- |
|
| ----------------------------------- | --------------------------------------------------- |
|
||||||
@ -160,6 +162,8 @@ slightly tweak the UI or behaviour of the menu. The flags can be viewed with `lo
|
|||||||
| `-t`, `--track-list <TRACK_LIST>` | Use a [custom track list](#custom-track-lists) |
|
| `-t`, `--track-list <TRACK_LIST>` | Use a [custom track list](#custom-track-lists) |
|
||||||
| `-s`, `--buffer-size <BUFFER_SIZE>` | Internal song buffer size [default: 5] |
|
| `-s`, `--buffer-size <BUFFER_SIZE>` | Internal song buffer size [default: 5] |
|
||||||
|
|
||||||
|
If you need something even more specific, see [ENVIRONMENT_VARS](./docs/ENVIRONMENT_VARS.md).
|
||||||
|
|
||||||
### Extra Features
|
### Extra Features
|
||||||
|
|
||||||
lowfi uses cargo/rust's "feature" system to make certain parts of the program optional,
|
lowfi uses cargo/rust's "feature" system to make certain parts of the program optional,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
# Environment Variables
|
# Environment Variables
|
||||||
|
|
||||||
Lowfi has some more specific options, usually as a result of minor feature requests, which are only documented here.
|
[[Version française](./fr/ENVIRONMENT_VARS.md)]
|
||||||
|
|
||||||
|
lowfi has some more specific options, usually as a result of minor feature requests, which are only documented here.
|
||||||
If you have some behavior you'd like to change, which is quite specific, then see if one of these options suits you.
|
If you have some behavior you'd like to change, which is quite specific, then see if one of these options suits you.
|
||||||
|
|
||||||
* `LOWFI_FIXED_MPRIS_NAME` - Limits the number of lowfi instances to one, but ensures the player name is always `lowfi`.
|
* `LOWFI_FIXED_MPRIS_NAME` - Limits the number of lowfi instances to one, but ensures the player name is always `lowfi`.
|
||||||
* `LOWFI_DISABLE_UI` - Disables the UI.
|
* `LOWFI_DISABLE_UI` - Disables the UI. This requires MPRIS, so that you can still actually control lowfi.
|
||||||
@ -1,4 +1,6 @@
|
|||||||
# The State of Lowfi's Music
|
# The State of lowfi's Music
|
||||||
|
|
||||||
|
[[Version française](./fr/MUSIQUE.md)]
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This document will be a bit long and has almost nothing to do with the actual
|
> This document will be a bit long and has almost nothing to do with the actual
|
||||||
@ -12,7 +14,7 @@ I always hated apps that required extensive configuration just to be usable.
|
|||||||
Sometimes it's justified, but often, it's just pointless when most will end up
|
Sometimes it's justified, but often, it's just pointless when most will end up
|
||||||
with the same set of "defaults" that aren't really defaults.
|
with the same set of "defaults" that aren't really defaults.
|
||||||
|
|
||||||
Lowfi is so nice and simple because of the "plug and play" aspect,
|
lowfi is so nice and simple because of the "plug and play" aspect,
|
||||||
but it's become a lot harder to continue it as of late.
|
but it's become a lot harder to continue it as of late.
|
||||||
|
|
||||||
## The Lofi Girl List
|
## The Lofi Girl List
|
||||||
23
docs/fr/CONTRIBUER.md
Normal file
23
docs/fr/CONTRIBUER.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Contribuer à lowfi
|
||||||
|
|
||||||
|
Il y a quelque directives listées ici qui vont augmenter les chances pour votre PR d'être acceptée.
|
||||||
|
Seules les moins évidentes seront listées, si vous avez besoin de demander la réponse est probablement non.
|
||||||
|
|
||||||
|
## 1. IA
|
||||||
|
|
||||||
|
Vous pouvez utiliser l'IA pour chercher, ou s'il y a quelque chose de mineur et fastidieux(eg. des tests) que vous préférez éviter de faire manuellement.
|
||||||
|
|
||||||
|
Cela dit, si l'usage d'IA est visible, c'est déjà trop.
|
||||||
|
Les PR générées par IA n'aident pas les développeurs, elles sont juste embêtantes et leur font perdre leur temps.
|
||||||
|
|
||||||
|
## 2. Petit = mieux
|
||||||
|
|
||||||
|
Faites en sorte que chaque PR ne contienne qu'une fonctionnalité distincte. Ajouter plusieurs fonctionnalités dans une seule PR est généralement une mauvaise idée.
|
||||||
|
Cela permet aussi que des fonctionnalités spécifiques soit approuvées ou refusées au cas par cas, plutot qu'un seul bloc de code important.
|
||||||
|
|
||||||
|
## 3. Keep lowfi simple
|
||||||
|
|
||||||
|
lowfi est censé être un programme simple. Pour l'instant aucune modification de l'interface initiale ne sera acceptée.
|
||||||
|
L'interface de lowfi pendant la lecture est restée la même depuis les premières versions, la compliquer irait à l'encontre de son but initial.
|
||||||
|
|
||||||
|
Des fonctionnalités plus complexes, comme des couleurs fantaisistes ou des pochettes d'albums, ne seront jamais acceptées. L'implémentation de fonctionnalitées acceptables doit être simple et discrète, si une fonctionnalité est simple mais que sa mise en oeuvre est très complexe elle ne sera pas acceptée.
|
||||||
7
docs/fr/ENVIRONMENT_VARS.md
Normal file
7
docs/fr/ENVIRONMENT_VARS.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Variables d'Environment
|
||||||
|
|
||||||
|
lowfi a quelques options précises, généralement dûe à des demandes de fonctionnalité mineures, qui sont uniquement documentées ici.
|
||||||
|
S'il y a quelque chose de spécifique que vous souhaitez changer, voyez si l'une des ces options vous va.
|
||||||
|
|
||||||
|
* `LOWFI_FIXED_MPRIS_NAME` - Limite le nombre d'instances de lowfi à 1, mais force le nom du lecteur à toujours être `lowfi`.
|
||||||
|
* `LOWFI_DISABLE_UI` - Désactive l'interface utilisateur.
|
||||||
48
docs/fr/MUSIQUE.md
Normal file
48
docs/fr/MUSIQUE.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# La Musique de lowfi
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Ce document sera un peu long et n'aura presque rien à voir avec l'utilisation pratique de lowfi, juste avec la musique intégrée par défaut.
|
||||||
|
|
||||||
|
Mais avant cela, un peu de contexte. lowfi comprend une longue liste de musiques intégrés au programme, vous pouvez donc l'installer et l'utiliser directement.
|
||||||
|
|
||||||
|
J'ai toujours détesté les applications qui nécessitent une configuration complexe juste pour pouvoir les utiliser. Occasionnellement, cela se justifie, mais souvent, cela n'a aucun sens, car la plupart des utilisateurs finissent par utiliser les mêmes paramètres, qui ne sont même pas vraiment les mêmes.
|
||||||
|
|
||||||
|
lowfi est super et simple grâce à son aspect « plug and play », mais c'est devenu beaucoup plus difficile de continuer comme ça les derniers temps.
|
||||||
|
|
||||||
|
## La Liste Lofi Girl
|
||||||
|
|
||||||
|
À l'origine, il était prévu que lowfi utilise la musique récupérée sur le site web de Lofi Girl. Croyez-le ou non, le scraper est en fait apparu avant le reste du programme.
|
||||||
|
|
||||||
|
Cependant, après une longue période d'indisponibilité, le site web de Lofi Girl a été refait sans les fichiers mp3. Ceux-ci sont désormais pratiquement inaccessibles, sauf en achetant chaque album individuellement sur Bandcamp, ce qui revient très vite très cher.
|
||||||
|
|
||||||
|
*Scraper* n'a jamais été interdit, mais c'est désormais tout simplement impossible. La question était donc : que faire après avoir perdu la principale source de musique de lowfi ?
|
||||||
|
|
||||||
|
## Listes de Morceaux
|
||||||
|
|
||||||
|
Au départ, j'étais contre l'idée de créer des listes de morceaux personnalisées, car j'avais une vision presque puriste d'un lowfi 100 % sans configuration. J'ai cependant finis par céder, ce qui s'est avéré être une très bonne décision. Maintenant peu importe le choix que je fais par rapport à la musique qui est intégrée, tout le monde peut ne pas utiliser celle-ci et choisir ce qu'il veut.
|
||||||
|
|
||||||
|
Cela a aboutit à quelque *templates*, stockés dans le dossier [data](../../data), et en particulier la liste chillhop par [danielwerg](https://github.com/danielwerg).
|
||||||
|
|
||||||
|
## Le Changement
|
||||||
|
|
||||||
|
Après que `lofigirl.com` deviennent inaccessible, j'ai réfléchi un coup puis ai finalement décidé de serrer les dents et passer à la liste chillhop. Et ce malgré que chillhop bannis tous les lecteurs tiers dans leur CGU. Ils interdisent aussi les *scrappers*, ce que j'ai appris seulement après en avoir écrit un.
|
||||||
|
|
||||||
|
Bon, est-ce que lowfi va vraiment devoir violer les CGU de son fournisseur de musique ?
|
||||||
|
Eh bien oui. J'y ai réfléchi et je suis arrivé à la conclusion que lowfi n'est probablement pas une grande menace pour plusieurs raisons.
|
||||||
|
|
||||||
|
Premièrement, il émule exactement le lecteur "radio" de chillhop. La seule différence étant que l'un force l'utilisation d'un navigateur web, et l'autre celle d'une beau terminal.
|
||||||
|
|
||||||
|
Ensuite j'ai réalisé que lowfi est juste un petit programme utilisé par peu.
|
||||||
|
Je ne gagne pas d'argent avec, et je pense que dégrader l'expérience de mes cher nerds qui veulent juste écouter de la lofi sans toute la merde ne vaut pas le coup.
|
||||||
|
|
||||||
|
Au final, lowfi a un `UserAgent` unique, si chillhop a un jour un problème avec, le bannir est extrêmement simple. Je ne souhaite pas que cela arrive, mais je comprendrais.
|
||||||
|
|
||||||
|
## Well, *je* Deteste la Musique Chillhop
|
||||||
|
|
||||||
|
Ce n'est pas aussi « lofi ». C'est presque un compromis, ça je ne peux même pas prétendre le nier. J'utilise le bouton « skip » presque trois fois plus souvent avec chillhop.
|
||||||
|
|
||||||
|
Si vous n'êtes pas assez découragé par les CGU pour avoir lu jusqu'ici, vous pouvez utiliser la liste [archive.txt](../../data/archive.txt) dans le dossier [data](../../data). Cette liste est le fruit de mon inquiétude quant à la possibilité que les morceaux sur `lofigirl.com` aient pu être perdus d'une manière ou d'une autre, en raison de la fermeture du site web.
|
||||||
|
|
||||||
|
Elle est hébergée sur `archive.org` et pourrait être supprimée à tout moment pour n'importe quelle raison.
|
||||||
|
Provenant de mes propres archives locales, elle contient environ 2 700 des 3 700 morceaux.
|
||||||
|
Elle n'est pas parfaite, son organisation est également *mauvaise*, mais elle existe.
|
||||||
243
docs/fr/README.md
Normal file
243
docs/fr/README.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# lowfi
|
||||||
|
|
||||||
|
lowfi est une petite application écrite en Rust qui sert un objectif unique : écouter de la lofi.
|
||||||
|
Elle le fait de la manière la plus simple possible : pas d’albums, pas de pubs, juste de la lofi.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Attention
|
||||||
|
|
||||||
|
À partir de la version 1.7.0 de lowfi, **tous** les fichiers audio intégrés par défaut proviennent de [chillhop](https://chillhop.com/).
|
||||||
|
Consultez [MUSIQUE](./MUSIQUE.md) pour plus d’informations.
|
||||||
|
|
||||||
|
## Pourquoi ?
|
||||||
|
|
||||||
|
Je déteste les plateformes de musique modernes, et je voulais une application, petite et simple, qui mettrait simplement de la lofi aléatoire, sans vidéo ni autres fioritures.
|
||||||
|
|
||||||
|
Au-delà de ça, elle a aussi été conçue pour être assez résistante aux connections instables, et *cache* 5 morceaux entiers à la fois.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Si vous êtes intéressé par la maintenance d’un paquet pour `lowfi` sur des gestionnaires de paquets comme Homebrew ou autres, ouvrez une issue.
|
||||||
|
|
||||||
|
### Dépendances
|
||||||
|
|
||||||
|
Sur toutes les plateformes : Rust 1.83.0+.
|
||||||
|
|
||||||
|
Sur macOS et Windows, aucune dépendance supplémentaire n’est nécessaire.
|
||||||
|
|
||||||
|
Sur Linux, vous aurez aussi besoin d’openssl et d’alsa.
|
||||||
|
|
||||||
|
* `alsa-lib` sur Arch, `libasound2-dev` sur Ubuntu, `alsa-lib-devel` sur Fedora.
|
||||||
|
* `openssl` sur Arch, `libssl-dev` sur Ubuntu, `openssl-devel` sur Fedora.
|
||||||
|
|
||||||
|
Si vous utilisez PulseAudio vous aurez aussi besoin d’installer `pulseaudio-alsa`.
|
||||||
|
|
||||||
|
### Cargo
|
||||||
|
|
||||||
|
La méthode d’installation recommandée est cargo :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install lowfi
|
||||||
|
|
||||||
|
# Si vous voulez utiliser le protocole MPRIS.
|
||||||
|
cargo install lowfi --features mpris
|
||||||
|
```
|
||||||
|
|
||||||
|
Assurez-vous que `$HOME/.cargo/bin` est ajouté à votre `$PATH`.
|
||||||
|
Voir également [Fonctionnalités supplémentaires](#fonctionnalités-supplémentaires) pour des fonctionnalités étendues.
|
||||||
|
|
||||||
|
### Packets précompilés
|
||||||
|
|
||||||
|
Si vous rencontrez des difficultés ou ne souhaitez pas utiliser cargo, vous pouvez simplement télécharger les exécutables précompilés depuis la [dernière release](https://github.com/talwat/lowfi/releases/latest).
|
||||||
|
|
||||||
|
### AUR
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yay -S lowfi
|
||||||
|
```
|
||||||
|
|
||||||
|
### openSUSE
|
||||||
|
|
||||||
|
```sh
|
||||||
|
zypper install lowfi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debian
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Ce packet est sur un dépôt non officiel maintenu par [Dario Griffo](https://github.com/dariogriffo).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -sS https://debian.griffo.io/3B9335DF576D3D58059C6AA50B56A1A69762E9FF.asc | gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/debian.griffo.io.gpg
|
||||||
|
echo "deb https://debian.griffo.io/apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.list
|
||||||
|
sudo apt install -y lowfi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fedora (COPR)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Ce packet utilise un dépôt COPR non officiel par [FurqanHun](https://github.com/FurqanHun).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo dnf copr enable furqanhun/lowfi
|
||||||
|
sudo dnf install lowfi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuel
|
||||||
|
|
||||||
|
Utile pour le débogage.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/talwat/lowfi
|
||||||
|
cd lowfi
|
||||||
|
|
||||||
|
# Si vous voulez un exécutable
|
||||||
|
cargo build --release --all-features
|
||||||
|
./target/release/lowfi
|
||||||
|
|
||||||
|
# Si vous voulez juste tester
|
||||||
|
cargo run --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
`lowfi`
|
||||||
|
|
||||||
|
Oui, c’est tout.
|
||||||
|
|
||||||
|
### Contrôles
|
||||||
|
|
||||||
|
| Touche | Fonction |
|
||||||
|
| ------------------ | ------------------- |
|
||||||
|
| `s`, `n`, `l` | Passer le morceau |
|
||||||
|
| `p`, Espace | Lecture / Pause |
|
||||||
|
| `+`, `=`, `k`, `↑` | Volume +10 % |
|
||||||
|
| `→` | Volume +1 % |
|
||||||
|
| `-`, `_`, `j`, `↓` | Volume -10 % |
|
||||||
|
| `←` | Volume -1 % |
|
||||||
|
| `q`, CTRL+C | Quitter |
|
||||||
|
| `b` | Ajouter aux Favoris |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> En plus de ces contrôles habituels, lowfi est compatible avec les touches multimédia de votre machine ainsi qu'avec le standard [MPRIS](https://wiki.archlinux.org/title/MPRIS) (avec des outils comme `playerctl`).
|
||||||
|
>
|
||||||
|
> MPRIS est actuellement une [fonctionnalité optionnelle](#fonctionnalités-supplémentaires) dans Cargo (activée avec `--features mpris`) car elle est uniquement destinée à Linux, et parce que le but principal de lowfi est son interface unique et minimaliste.
|
||||||
|
|
||||||
|
### Favoris
|
||||||
|
|
||||||
|
Les favoris sont la réponse extrêmement simple de lowfi à la question « et si je voulais garder un morceau ? ».
|
||||||
|
Vous pouvez ajouter ou retirer des morceaux des favoris avec `b`, et les lire avec `lowfi -t bookmarks`.
|
||||||
|
|
||||||
|
D’un point de vue technique, vos favoris ne sont pas différents de n’importe quelle autre liste de morceaux, et sont donc stockés dans le même répertoire.
|
||||||
|
|
||||||
|
### Options supplémentaires
|
||||||
|
|
||||||
|
Si vous avez quelque chose que vous souhaitez ajuster dans lowfi, vous pouvez utiliser des options supplémentaires qui modifient légèrement l’interface ou le comportement du menu.
|
||||||
|
Les options peuvent être consultées avec `lowfi --help`.
|
||||||
|
|
||||||
|
| Option | Fonction |
|
||||||
|
| ----------------------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| `-a`, `--alternate` | Utiliser un écran de terminal alternatif |
|
||||||
|
| `-m`, `--minimalist` | Masquer la barre de contrôle inférieure |
|
||||||
|
| `-b`, `--borderless` | Exclure les bordures de l’interface |
|
||||||
|
| `-p`, `--paused` | Lancer lowfi en pause, |
|
||||||
|
| `-f`, `--fps` | FPS de l’interface [défaut : 12] |
|
||||||
|
| `--timeout` | Délai d’attente en secondes pour les téléchargements |
|
||||||
|
| `-d`, `--debug` | Inclure les logs ALSA et autres |
|
||||||
|
| `-w`, `--width <WIDTH>` | Largeur du lecteur, de 0 à 32 [défaut : 3] |
|
||||||
|
| `-t`, `--track-list <TRACK_LIST>` | Utiliser une [liste de pistes personnalisée](#listes-de-pistes-personnalisées) |
|
||||||
|
| `-s`, `--buffer-size <BUFFER_SIZE>` | Nombre de morceaux ajoutés au cache en avance [défaut : 5] |
|
||||||
|
|
||||||
|
### Fonctionnalités supplémentaires
|
||||||
|
|
||||||
|
lowfi utilise le système de « features » de cargo/rust pour rendre certaines parties du programme optionnelles, notamment celles qui ne sont censées être utilisées que par une minorité d’utilisateurs.
|
||||||
|
|
||||||
|
#### `scrape` - Scraping
|
||||||
|
|
||||||
|
Cette fonctionnalité fournit la commande `scrape`.
|
||||||
|
Elle n’est généralement pas très utile, mais est incluse par souci de transparence.
|
||||||
|
|
||||||
|
Plus d’informations sont disponibles en exécutant `lowfi help scrape`.
|
||||||
|
|
||||||
|
#### `mpris` - MPRIS
|
||||||
|
|
||||||
|
Active MPRIS.
|
||||||
|
|
||||||
|
#### `extra-audio-formats` - Formats audio supplémentaires
|
||||||
|
|
||||||
|
Ceci est uniquement pertinent pour les utilisateurs de listes de pistes personnalisées ; dans ce cas, cela permet plus de formats que le simple MP3, à savoir FLAC, Vorbis et WAV.
|
||||||
|
|
||||||
|
Ces formats devraient couvrir environ 99 % des fichiers audio que les gens souhaitent lire. Si vous faites partie du 1 % utilisant un autre format audio, et présent dans [cette liste](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#codecs-decoders), ouvrez une issue.
|
||||||
|
|
||||||
|
### Listes de pistes personnalisées
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Certains gentils utilisateurs, en particulier [danielwerg](https://github.com/danielwerg), ont déjà créé des listes alternatives situées dans le dossier [data](https://github.com/talwat/lowfi/blob/main/data/) de ce dépôt. Vous pouvez les utiliser avec lowfi en utilisant l’option `--track-list`.
|
||||||
|
>
|
||||||
|
> N’hésitez pas à proposer votre propre liste via une pull request.
|
||||||
|
|
||||||
|
lowfi prend également en charge les listes de pistes personnalisées, bien que celle par défaut de chillhop soit intégrée directement dans l'exécutable.
|
||||||
|
|
||||||
|
Pour utiliser une liste personnalisée, utilisez l’option `--track-list`. Cela peut être soit un chemin vers un fichier, soit le nom d’un fichier (sans l’extension `.txt`) présent dans le dossier données.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Répertoires de données par plateforme :
|
||||||
|
>
|
||||||
|
> * Linux - `~/.local/share/lowfi`
|
||||||
|
> * macOS - `~/Library/Application Support/lowfi`
|
||||||
|
> * Windows - `%appdata%\Roaming\lowfi`
|
||||||
|
|
||||||
|
Par exemple, `lowfi --track-list minipop` chargera `~/.local/share/lowfi/minipop.txt`.
|
||||||
|
Tandis que `lowfi --track-list ~/Music/minipop.txt` chargera depuis le répertoire spécifié.
|
||||||
|
|
||||||
|
Tous les morceaux doivent être au format MP3, sauf si lowfi a été compilé avec la fonctionnalité `extra-audio-formats`, qui ajoute la prise en charge de certains autres formats.
|
||||||
|
|
||||||
|
#### Le format
|
||||||
|
|
||||||
|
Dans les listes, la première ligne est appelée l’en-tête, suivie du reste des pistes.
|
||||||
|
Chaque piste sera d’abord concaténée à l’en-tête, puis l’ensemble sera utilisé pour télécharger le morceau.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> lowfi *n’ajoutera pas* de `/` entre la base et la piste pour plus de flexibilité ;
|
||||||
|
> dans la plupart des cas, vous devriez donc avoir un `/` final dans votre en-tête.
|
||||||
|
|
||||||
|
L’exception à cette règle est lorsque le nom de la piste commence par un protocole tel que `https://`, auquel cas la base ne sera pas préfixée. Si toutes vos pistes sont de ce type, vous pouvez mettre `noheader` comme première ligne et ne pas avoir d’en-tête du tout.
|
||||||
|
|
||||||
|
Par exemple, dans cette liste :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://lofigirl.com/wp-content/uploads/
|
||||||
|
2023/06/Foudroie-Finding-The-Edge-V2.mp3
|
||||||
|
2023/04/2-In-Front-Of-Me.mp3
|
||||||
|
https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
lowfi téléchargerait ces trois URL :
|
||||||
|
|
||||||
|
* `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3`
|
||||||
|
* `https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3`
|
||||||
|
* `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3`
|
||||||
|
|
||||||
|
De plus, vous pouvez choisir un nom d’affichage personnalisé pour une piste,
|
||||||
|
indiqué par un `!`. Par exemple, avec une entrée comme celle-ci :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
2023/04/2-In-Front-Of-Me.mp3!nom personnalisé
|
||||||
|
```
|
||||||
|
|
||||||
|
lowfi téléchargera depuis la première partie et affichera la seconde comme nom du morceau.
|
||||||
|
|
||||||
|
`file://` peut être utilisé devant une piste ou un en-tête pour que lowfi le traite comme un fichier local.
|
||||||
|
C’est utile si vous souhaitez utiliser un fichier local comme URL de base, par exemple :
|
||||||
|
|
||||||
|
```txt
|
||||||
|
file:///home/utilisateur/Musique/
|
||||||
|
fichier.mp3
|
||||||
|
file:///home/utilisateur/Musique 2/deuxieme-fichier.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
D’autres exemples sont disponibles dans le dossier
|
||||||
|
[data](https://github.com/talwat/lowfi/tree/main/data).
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@ -3,57 +3,51 @@ use std::{sync::Arc, time::Duration};
|
|||||||
use rodio::Sink;
|
use rodio::Sink;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{mpsc, Notify},
|
sync::{mpsc, Notify},
|
||||||
task::{self, JoinHandle},
|
|
||||||
time,
|
time,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Background loop that waits for the sink to drain and then attempts
|
||||||
|
/// to send a `Message::Next` to the provided channel.
|
||||||
|
async fn waiter(
|
||||||
|
sink: Arc<Sink>,
|
||||||
|
tx: mpsc::Sender<crate::Message>,
|
||||||
|
notify: Arc<Notify>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
loop {
|
||||||
|
notify.notified().await;
|
||||||
|
|
||||||
|
while !sink.empty() {
|
||||||
|
time::sleep(Duration::from_millis(16)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx.try_send(crate::Message::Next).is_err() {
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Lightweight helper that waits for the current sink to drain and then
|
/// Lightweight helper that waits for the current sink to drain and then
|
||||||
/// notifies the player to advance to the next track.
|
/// notifies the player to advance to the next track.
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
/// Background task monitoring the sink.
|
|
||||||
task: JoinHandle<()>,
|
|
||||||
|
|
||||||
/// Notification primitive used to wake the waiter.
|
/// Notification primitive used to wake the waiter.
|
||||||
notify: Arc<Notify>,
|
notify: Arc<Notify>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Handle {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.task.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
/// Create a new `Handle` which watches the provided `sink` and sends
|
|
||||||
/// `Message::Next` down `tx` when the sink becomes empty.
|
|
||||||
pub fn new(sink: Arc<Sink>, tx: mpsc::Sender<crate::Message>) -> Self {
|
|
||||||
let notify = Arc::new(Notify::new());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
task: task::spawn(Self::waiter(sink, tx, Arc::clone(¬ify))),
|
|
||||||
notify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notify the waiter that playback state may have changed and it should
|
/// Notify the waiter that playback state may have changed and it should
|
||||||
/// re-check the sink emptiness condition.
|
/// re-check the sink emptiness condition.
|
||||||
pub fn notify(&self) {
|
pub fn notify(&self) {
|
||||||
self.notify.notify_one();
|
self.notify.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background loop that waits for the sink to drain and then attempts
|
|
||||||
/// to send a `Message::Next` to the provided channel.
|
|
||||||
async fn waiter(sink: Arc<Sink>, tx: mpsc::Sender<crate::Message>, notify: Arc<Notify>) {
|
|
||||||
loop {
|
|
||||||
notify.notified().await;
|
|
||||||
|
|
||||||
while !sink.empty() {
|
|
||||||
time::sleep(Duration::from_millis(8)).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tx.try_send(crate::Message::Next).is_err() {
|
impl crate::Tasks {
|
||||||
break;
|
/// Create a new `Handle` which watches the provided `sink` and sends
|
||||||
}
|
/// `Message::Next` down `tx` when the sink becomes empty.
|
||||||
}
|
pub fn waiter(&mut self, sink: Arc<Sink>) -> Handle {
|
||||||
|
let notify = Arc::new(Notify::new());
|
||||||
|
self.spawn(waiter(sink, self.tx(), notify.clone()));
|
||||||
|
|
||||||
|
Handle { notify }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use crate::tracks;
|
use crate::tracks;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
/// Flag indicating whether the downloader is actively fetching a track.
|
/// Flag indicating whether the downloader is actively fetching a track.
|
||||||
///
|
///
|
||||||
@ -28,14 +28,14 @@ pub struct Downloader {
|
|||||||
/// The track queue itself, which in this case is actually
|
/// The track queue itself, which in this case is actually
|
||||||
/// just an asynchronous sender.
|
/// just an asynchronous sender.
|
||||||
///
|
///
|
||||||
/// It is a [`Sender`] because the tracks will have to be
|
/// It is a [`mpsc::Sender`] because the tracks will have to be
|
||||||
/// received by a completely different thread, so this avoids
|
/// received by a completely different thread, so this avoids
|
||||||
/// the need to use an explicit [`tokio::sync::Mutex`].
|
/// the need to use an explicit [`tokio::sync::Mutex`].
|
||||||
queue: Sender<tracks::Queued>,
|
queue: mpsc::Sender<tracks::Queued>,
|
||||||
|
|
||||||
/// The [`Sender`] which is used to inform the
|
/// The [`mpsc::Sender`] which is used to inform the
|
||||||
/// [`crate::Player`] with [`crate::Message::Loaded`].
|
/// [`crate::Player`] with [`crate::Message::Loaded`].
|
||||||
tx: Sender<crate::Message>,
|
tx: mpsc::Sender<crate::Message>,
|
||||||
|
|
||||||
/// The list of tracks to download from.
|
/// The list of tracks to download from.
|
||||||
tracks: tracks::List,
|
tracks: tracks::List,
|
||||||
@ -48,39 +48,6 @@ pub struct Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Downloader {
|
impl Downloader {
|
||||||
/// Initializes the downloader with a track list.
|
|
||||||
///
|
|
||||||
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
|
||||||
pub fn init(
|
|
||||||
size: usize,
|
|
||||||
timeout: u64,
|
|
||||||
tracks: tracks::List,
|
|
||||||
tx: Sender<crate::Message>,
|
|
||||||
) -> crate::Result<Handle> {
|
|
||||||
let client = Client::builder()
|
|
||||||
.user_agent(concat!(
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
"/",
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
))
|
|
||||||
.timeout(Duration::from_secs(timeout))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let (qtx, qrx) = mpsc::channel(size - 1);
|
|
||||||
let downloader = Self {
|
|
||||||
queue: qtx,
|
|
||||||
tx,
|
|
||||||
tracks,
|
|
||||||
client,
|
|
||||||
rng: fastrand::Rng::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Handle {
|
|
||||||
queue: qrx,
|
|
||||||
task: crate::Tasks([tokio::spawn(downloader.run())]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Actually runs the downloader, consuming it and beginning
|
/// Actually runs the downloader, consuming it and beginning
|
||||||
/// the cycle of downloading tracks and reporting to the
|
/// the cycle of downloading tracks and reporting to the
|
||||||
/// rest of the program.
|
/// rest of the program.
|
||||||
@ -117,10 +84,7 @@ impl Downloader {
|
|||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
/// The queue receiver, which can be used to actually
|
/// The queue receiver, which can be used to actually
|
||||||
/// fetch a track from the queue.
|
/// fetch a track from the queue.
|
||||||
queue: Receiver<tracks::Queued>,
|
queue: mpsc::Receiver<tracks::Queued>,
|
||||||
|
|
||||||
/// The downloader task, which can be aborted.
|
|
||||||
task: crate::Tasks<crate::Error, 1>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The output when a track is requested from the downloader.
|
/// The output when a track is requested from the downloader.
|
||||||
@ -146,10 +110,37 @@ impl Handle {
|
|||||||
}, Output::Queued,
|
}, Output::Queued,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shuts down the downloader task, returning any errors.
|
impl crate::Tasks {
|
||||||
pub async fn close(self) -> crate::Result<()> {
|
/// Initializes the downloader with a track list.
|
||||||
let [result] = self.task.shutdown().await;
|
///
|
||||||
result
|
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
||||||
|
pub fn downloader(
|
||||||
|
&mut self,
|
||||||
|
size: usize,
|
||||||
|
timeout: u64,
|
||||||
|
tracks: tracks::List,
|
||||||
|
) -> crate::Result<Handle> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.user_agent(concat!(
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
"/",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
))
|
||||||
|
.timeout(Duration::from_secs(timeout))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let (qtx, qrx) = mpsc::channel(size - 1);
|
||||||
|
let downloader = Downloader {
|
||||||
|
queue: qtx,
|
||||||
|
tx: self.tx(),
|
||||||
|
tracks,
|
||||||
|
client,
|
||||||
|
rng: fastrand::Rng::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.spawn(downloader.run());
|
||||||
|
Ok(Handle { queue: qrx })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@ -89,11 +89,18 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shorthand to get a boolean environment variable.
|
||||||
|
#[inline]
|
||||||
|
pub fn env(name: &str) -> bool {
|
||||||
|
std::env::var(name).is_ok_and(|x| x == "1")
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the application data directory used for persistency.
|
/// Returns the application data directory used for persistency.
|
||||||
///
|
///
|
||||||
/// The function returns the platform-specific user data directory with
|
/// The function returns the platform-specific user data directory with
|
||||||
/// a `lowfi` subfolder. Callers may use this path to store config,
|
/// a `lowfi` subfolder. Callers may use this path to store config,
|
||||||
/// bookmarks, and other persistent files.
|
/// bookmarks, and other persistent files.
|
||||||
|
#[inline]
|
||||||
pub fn data_dir() -> crate::Result<PathBuf> {
|
pub fn data_dir() -> crate::Result<PathBuf> {
|
||||||
let dir = dirs::data_dir().unwrap().join("lowfi");
|
let dir = dirs::data_dir().unwrap().join("lowfi");
|
||||||
|
|
||||||
@ -121,12 +128,12 @@ async fn main() -> eyre::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let stream = audio::stream()?;
|
let stream = audio::stream()?;
|
||||||
let environment = ui::Environment::ready(args.alternate)?;
|
let environment = ui::Environment::ready(&args)?;
|
||||||
let mut player = Player::init(args, stream.mixer())
|
let (mut player, mut tasks) = Player::init(args, stream.mixer())
|
||||||
.await
|
.await
|
||||||
.inspect_err(|_| environment.cleanup(false).unwrap())?;
|
.inspect_err(|_| environment.cleanup(false).unwrap())?;
|
||||||
|
|
||||||
let result = player.run().await;
|
let result = tasks.wait(player.run()).await;
|
||||||
environment.cleanup(result.is_ok())?;
|
environment.cleanup(result.is_ok())?;
|
||||||
player.close().await?;
|
player.close().await?;
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::sync::{
|
use tokio::sync::mpsc::{self, Receiver};
|
||||||
broadcast,
|
|
||||||
mpsc::{self, Receiver},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
audio::waiter,
|
audio::waiter,
|
||||||
bookmark::Bookmarks,
|
bookmark::Bookmarks,
|
||||||
download::{self, Downloader},
|
download,
|
||||||
tracks::{self, List},
|
tracks::{self, List},
|
||||||
ui,
|
ui,
|
||||||
volume::PersistentVolume,
|
volume::PersistentVolume,
|
||||||
Message,
|
Message, Tasks,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
/// Represents the currently known playback state.
|
/// Represents the currently known playback state.
|
||||||
///
|
///
|
||||||
/// * [`Current::Loading`] indicates the player is waiting for data.
|
/// * [`Current::Loading`] indicates the player is waiting for data.
|
||||||
/// * [`Current::Track`] indicates the player has a decoded track available.
|
/// * [`Current::Track`] indicates the player has a decoded track available.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum Current {
|
pub enum Current {
|
||||||
/// Waiting for a track to arrive. The optional `Progress` is used to
|
/// Waiting for a track to arrive. The optional `Progress` is used to
|
||||||
/// indicate global download progress when present.
|
/// indicate global download progress when present.
|
||||||
@ -48,23 +45,20 @@ impl Current {
|
|||||||
/// `Player` composes the downloader, UI, audio sink and bookkeeping state.
|
/// `Player` composes the downloader, UI, audio sink and bookkeeping state.
|
||||||
/// It owns background `Handle`s and drives the main message loop in `run`.
|
/// It owns background `Handle`s and drives the main message loop in `run`.
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
/// Background downloader that fills the internal queue.
|
|
||||||
downloader: download::Handle,
|
|
||||||
|
|
||||||
/// Persistent bookmark storage used by the player.
|
/// Persistent bookmark storage used by the player.
|
||||||
bookmarks: Bookmarks,
|
bookmarks: Bookmarks,
|
||||||
|
|
||||||
/// Shared audio sink used for playback.
|
/// Current playback state (loading or track).
|
||||||
sink: Arc<rodio::Sink>,
|
current: Current,
|
||||||
|
|
||||||
|
/// Background downloader that fills the internal queue.
|
||||||
|
downloader: download::Handle,
|
||||||
|
|
||||||
/// Receiver for incoming `Message` commands.
|
/// Receiver for incoming `Message` commands.
|
||||||
rx: Receiver<crate::Message>,
|
rx: Receiver<crate::Message>,
|
||||||
|
|
||||||
/// Broadcast channel used to send UI updates.
|
/// Shared audio sink used for playback.
|
||||||
updater: broadcast::Sender<ui::Update>,
|
sink: Arc<rodio::Sink>,
|
||||||
|
|
||||||
/// Current playback state (loading or track).
|
|
||||||
current: Current,
|
|
||||||
|
|
||||||
/// UI handle for rendering and input.
|
/// UI handle for rendering and input.
|
||||||
ui: ui::Handle,
|
ui: ui::Handle,
|
||||||
@ -80,38 +74,34 @@ impl Player {
|
|||||||
/// based on persistent bookmarks.
|
/// based on persistent bookmarks.
|
||||||
pub fn set_current(&mut self, current: Current) -> crate::Result<()> {
|
pub fn set_current(&mut self, current: Current) -> crate::Result<()> {
|
||||||
self.current = current.clone();
|
self.current = current.clone();
|
||||||
self.update(ui::Update::Track(current))?;
|
self.ui.update(ui::Update::Track(current))?;
|
||||||
|
|
||||||
let Current::Track(track) = &self.current else {
|
let Current::Track(track) = &self.current else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let bookmarked = self.bookmarks.bookmarked(track);
|
let bookmarked = self.bookmarks.bookmarked(track);
|
||||||
self.update(ui::Update::Bookmarked(bookmarked))?;
|
self.ui.update(ui::Update::Bookmarked(bookmarked))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a `ui::Update` to the broadcast channel.
|
|
||||||
pub fn update(&mut self, update: ui::Update) -> crate::Result<()> {
|
|
||||||
self.updater.send(update)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize a `Player` with the provided CLI `args` and audio `mixer`.
|
/// Initialize a `Player` with the provided CLI `args` and audio `mixer`.
|
||||||
///
|
///
|
||||||
/// This sets up the audio sink, UI, downloader, bookmarks and persistent
|
/// This sets up the audio sink, UI, downloader, bookmarks and persistent
|
||||||
/// volume state. The function returns a fully constructed `Player` ready
|
/// volume state. The function returns a fully constructed `Player` ready
|
||||||
/// to be driven via `run`.
|
/// to be driven via `run`.
|
||||||
pub async fn init(args: crate::Args, mixer: &rodio::mixer::Mixer) -> crate::Result<Self> {
|
pub async fn init(
|
||||||
|
args: crate::Args,
|
||||||
|
mixer: &rodio::mixer::Mixer,
|
||||||
|
) -> crate::Result<(Self, crate::Tasks)> {
|
||||||
let (tx, rx) = mpsc::channel(8);
|
let (tx, rx) = mpsc::channel(8);
|
||||||
|
let mut tasks = Tasks::new(tx.clone());
|
||||||
if args.paused {
|
if args.paused {
|
||||||
tx.send(Message::Pause).await?;
|
tx.send(Message::Pause).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.send(Message::Init).await?;
|
tx.send(Message::Init).await?;
|
||||||
|
|
||||||
let (utx, urx) = broadcast::channel(8);
|
|
||||||
let list = List::load(args.track_list.as_ref()).await?;
|
let list = List::load(args.track_list.as_ref()).await?;
|
||||||
|
|
||||||
let sink = Arc::new(rodio::Sink::connect_new(mixer));
|
let sink = Arc::new(rodio::Sink::connect_new(mixer));
|
||||||
@ -120,40 +110,25 @@ impl Player {
|
|||||||
let volume = PersistentVolume::load().await?;
|
let volume = PersistentVolume::load().await?;
|
||||||
sink.set_volume(volume.float());
|
sink.set_volume(volume.float());
|
||||||
|
|
||||||
Ok(Self {
|
let player = Self {
|
||||||
ui: ui::Handle::init(tx.clone(), urx, state, &args).await?,
|
ui: tasks.ui(state, &args).await?,
|
||||||
downloader: Downloader::init(
|
downloader: tasks.downloader(args.buffer_size as usize, args.timeout, list)?,
|
||||||
args.buffer_size as usize,
|
waiter: tasks.waiter(Arc::clone(&sink)),
|
||||||
args.timeout,
|
|
||||||
list,
|
|
||||||
tx.clone(),
|
|
||||||
)?,
|
|
||||||
waiter: waiter::Handle::new(Arc::clone(&sink), tx),
|
|
||||||
bookmarks: Bookmarks::load().await?,
|
bookmarks: Bookmarks::load().await?,
|
||||||
current: Current::default(),
|
current: Current::default(),
|
||||||
updater: utx,
|
|
||||||
rx,
|
rx,
|
||||||
sink,
|
sink,
|
||||||
})
|
};
|
||||||
|
|
||||||
|
Ok((player, tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close any outlying processes, as well as persist state that
|
/// Close any outlying processes, as well as persist state that
|
||||||
/// should survive such as bookmarks and volume.
|
/// should survive such as bookmarks and volume.
|
||||||
pub async fn close(self) -> crate::Result<()> {
|
pub async fn close(self) -> crate::Result<()> {
|
||||||
// We should prioritize reporting UI/Downloader errors,
|
|
||||||
// but still save persistent state before so that if either one fails,
|
|
||||||
// state is saved.
|
|
||||||
let saves = (
|
|
||||||
self.bookmarks.save().await,
|
|
||||||
PersistentVolume::save(self.sink.volume()).await,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.ui.close().await?;
|
|
||||||
self.downloader.close().await?;
|
|
||||||
self.sink.stop();
|
self.sink.stop();
|
||||||
|
self.bookmarks.save().await?;
|
||||||
saves.0?;
|
PersistentVolume::save(self.sink.volume()).await?;
|
||||||
saves.1?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -205,11 +180,11 @@ impl Player {
|
|||||||
Message::ChangeVolume(change) => {
|
Message::ChangeVolume(change) => {
|
||||||
self.sink
|
self.sink
|
||||||
.set_volume((self.sink.volume() + change).clamp(0.0, 1.0));
|
.set_volume((self.sink.volume() + change).clamp(0.0, 1.0));
|
||||||
self.update(ui::Update::Volume)?;
|
self.ui.update(ui::Update::Volume)?;
|
||||||
}
|
}
|
||||||
Message::SetVolume(set) => {
|
Message::SetVolume(set) => {
|
||||||
self.sink.set_volume(set.clamp(0.0, 1.0));
|
self.sink.set_volume(set.clamp(0.0, 1.0));
|
||||||
self.update(ui::Update::Volume)?;
|
self.ui.update(ui::Update::Volume)?;
|
||||||
}
|
}
|
||||||
Message::Bookmark => {
|
Message::Bookmark => {
|
||||||
let Current::Track(current) = &self.current else {
|
let Current::Track(current) = &self.current else {
|
||||||
@ -217,7 +192,7 @@ impl Player {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let bookmarked = self.bookmarks.bookmark(current)?;
|
let bookmarked = self.bookmarks.bookmark(current)?;
|
||||||
self.update(ui::Update::Bookmarked(bookmarked))?;
|
self.ui.update(ui::Update::Bookmarked(bookmarked))?;
|
||||||
}
|
}
|
||||||
Message::Quit => break,
|
Message::Quit => break,
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/tasks.rs
89
src/tasks.rs
@ -1,47 +1,64 @@
|
|||||||
use std::{future::Future, mem::MaybeUninit};
|
//! Task management.
|
||||||
|
//!
|
||||||
|
//! This file aims to abstract a lot of annoying Rust async logic, which may be subject to change.
|
||||||
|
//! For those who are not intimately familiar with async rust, this will be very confusing.
|
||||||
|
|
||||||
trait AsyncArrayMap<T, const N: usize> {
|
use futures_util::TryFutureExt;
|
||||||
async fn async_map<U, F, Fut>(self, f: F) -> [U; N]
|
use std::future::Future;
|
||||||
where
|
use tokio::{select, sync::mpsc, task::JoinSet};
|
||||||
F: FnMut(T) -> Fut,
|
|
||||||
Fut: Future<Output = U>;
|
/// Handles all of the processes within lowfi.
|
||||||
|
/// This entails initializing/closing tasks, and handling any potential errors that arise.
|
||||||
|
pub struct Tasks {
|
||||||
|
/// The [`JoinSet`], which contains all of the task handles.
|
||||||
|
pub set: JoinSet<crate::Result<()>>,
|
||||||
|
|
||||||
|
/// A sender, which is kept for convenience to be used when
|
||||||
|
/// initializing various other tasks.
|
||||||
|
tx: mpsc::Sender<crate::Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, const N: usize> AsyncArrayMap<T, N> for [T; N] {
|
impl Tasks {
|
||||||
async fn async_map<U, F, Fut>(self, mut f: F) -> [U; N]
|
/// Creates a new task manager.
|
||||||
where
|
pub fn new(tx: mpsc::Sender<crate::Message>) -> Self {
|
||||||
F: FnMut(T) -> Fut,
|
Self {
|
||||||
Fut: Future<Output = U>,
|
tx,
|
||||||
{
|
set: JoinSet::new(),
|
||||||
let mut out: [MaybeUninit<U>; N] = unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
for (i, v) in self.into_iter().enumerate() {
|
|
||||||
out[i].write(f(v).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe { std::mem::transmute_copy(&out) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper around an array of JoinHandles to provide better error reporting & shutdown.
|
/// Processes a task, and adds it to the internal [`JoinSet`].
|
||||||
pub struct Tasks<E, const S: usize>(pub [tokio::task::JoinHandle<Result<(), E>>; S]);
|
pub fn spawn<E: Into<crate::Error> + Send + Sync + 'static>(
|
||||||
|
&mut self,
|
||||||
impl<T: Send + 'static + Into<crate::Error>, const S: usize> Tasks<T, S> {
|
future: impl Future<Output = Result<(), E>> + Send + 'static,
|
||||||
/// Abort tasks, and report either errors thrown from within each task
|
) {
|
||||||
/// or from tokio about joining the task.
|
self.set.spawn(future.map_err(Into::into));
|
||||||
pub async fn shutdown(self) -> [crate::Result<()>; S] {
|
|
||||||
self.0
|
|
||||||
.async_map(async |handle| {
|
|
||||||
if !handle.is_finished() {
|
|
||||||
handle.abort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match handle.await {
|
/// Gets a copy of the internal [`mpsc::Sender`].
|
||||||
Ok(Err(error)) => Err(error.into()),
|
pub fn tx(&self) -> mpsc::Sender<crate::Message> {
|
||||||
Err(error) if !error.is_cancelled() => Err(crate::Error::JoinError(error)),
|
self.tx.clone()
|
||||||
Ok(Ok(())) | Err(_) => Ok(()),
|
}
|
||||||
|
|
||||||
|
/// Actively polls all of the handles previously added.
|
||||||
|
///
|
||||||
|
/// An additional `runner` is for the main player future, which
|
||||||
|
/// can't be added as a "task" because it shares data with the
|
||||||
|
/// main thread.
|
||||||
|
///
|
||||||
|
/// This either returns when the runner completes, or if an error occurs
|
||||||
|
/// in any of the internally held tasks.
|
||||||
|
pub async fn wait(
|
||||||
|
&mut self,
|
||||||
|
runner: impl Future<Output = Result<(), crate::Error>> + std::marker::Send,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
select! {
|
||||||
|
result = runner => result,
|
||||||
|
Some(result) = self.set.join_next() => match result {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) if !e.is_cancelled() => Err(crate::Error::JoinError(e)),
|
||||||
|
Err(_) => Ok(()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/ui.rs
62
src/ui.rs
@ -1,12 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{player::Current, ui, Args};
|
use crate::player::Current;
|
||||||
use tokio::{
|
use tokio::{sync::broadcast, time::Instant};
|
||||||
sync::{broadcast, mpsc::Sender},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
|
pub mod init;
|
||||||
pub use environment::Environment;
|
pub use environment::Environment;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod interface;
|
pub mod interface;
|
||||||
@ -108,39 +106,21 @@ pub enum Update {
|
|||||||
/// The UI handle for controlling the state of the UI, as well as
|
/// The UI handle for controlling the state of the UI, as well as
|
||||||
/// updating MPRIS information and other small interfacing tasks.
|
/// updating MPRIS information and other small interfacing tasks.
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
|
/// Broadcast channel used to send UI updates.
|
||||||
|
updater: broadcast::Sender<Update>,
|
||||||
|
|
||||||
/// The MPRIS server, which is more or less a handle to the actual MPRIS thread.
|
/// The MPRIS server, which is more or less a handle to the actual MPRIS thread.
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
pub mpris: mpris::Server,
|
pub mpris: mpris::Server,
|
||||||
|
|
||||||
/// The UI's running tasks.
|
|
||||||
tasks: Option<crate::Tasks<ui::Error, 2>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
/// Actually takes care of spawning the tasks for the UI.
|
/// Sends a `ui::Update` to the broadcast channel.
|
||||||
fn spawn(
|
pub fn update(&mut self, update: Update) -> crate::Result<()> {
|
||||||
tx: Sender<crate::Message>,
|
self.updater.send(update)?;
|
||||||
updater: broadcast::Receiver<ui::Update>,
|
|
||||||
state: State,
|
|
||||||
params: interface::Params,
|
|
||||||
) -> crate::Tasks<Error, 2> {
|
|
||||||
crate::Tasks([
|
|
||||||
tokio::spawn(Handle::ui(updater, state, params)),
|
|
||||||
tokio::spawn(input::listen(tx)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shuts down the UI tasks, returning any encountered errors.
|
|
||||||
pub async fn close(self) -> crate::Result<()> {
|
|
||||||
let Some(tasks) = self.tasks else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
for result in tasks.shutdown().await {
|
|
||||||
result?
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The main UI process, which will both render the UI to the terminal
|
/// The main UI process, which will both render the UI to the terminal
|
||||||
/// and also update state.
|
/// and also update state.
|
||||||
@ -150,7 +130,7 @@ impl Handle {
|
|||||||
///
|
///
|
||||||
/// `rx` is the receiver for state updates, `state` the initial state,
|
/// `rx` is the receiver for state updates, `state` the initial state,
|
||||||
/// and `params` specifies aesthetic options that are specified by the user.
|
/// and `params` specifies aesthetic options that are specified by the user.
|
||||||
async fn ui(
|
pub async fn run(
|
||||||
mut updater: broadcast::Receiver<Update>,
|
mut updater: broadcast::Receiver<Update>,
|
||||||
mut state: State,
|
mut state: State,
|
||||||
params: interface::Params,
|
params: interface::Params,
|
||||||
@ -173,23 +153,3 @@ impl Handle {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the UI itself, along with all of the tasks that are related to it.
|
|
||||||
#[allow(clippy::unused_async)]
|
|
||||||
pub async fn init(
|
|
||||||
tx: Sender<crate::Message>,
|
|
||||||
updater: broadcast::Receiver<ui::Update>,
|
|
||||||
state: State,
|
|
||||||
args: &Args,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let params = interface::Params::try_from(args)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
#[cfg(feature = "mpris")]
|
|
||||||
mpris: mpris::Server::new(state.clone(), tx.clone(), updater.resubscribe()).await?,
|
|
||||||
tasks: params
|
|
||||||
.enabled
|
|
||||||
.then(|| Self::spawn(tx, updater, state, params)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -15,16 +15,29 @@ pub struct Environment {
|
|||||||
|
|
||||||
/// Whether the terminal is in an alternate screen or not.
|
/// Whether the terminal is in an alternate screen or not.
|
||||||
alternate: bool,
|
alternate: bool,
|
||||||
|
|
||||||
|
/// Whether the UI is actually enabled at all.
|
||||||
|
/// This will effectively make the environment just do nothing.
|
||||||
|
enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Environment {
|
impl Environment {
|
||||||
/// This prepares the terminal, returning an [Environment] helpful
|
/// This prepares the terminal, returning an [Environment] helpful
|
||||||
/// for cleaning up afterwards.
|
/// for cleaning up afterwards.
|
||||||
pub fn ready(alternate: bool) -> super::Result<Self> {
|
pub fn ready(args: &crate::Args) -> super::Result<Self> {
|
||||||
|
let enabled = !crate::env("LOWFI_DISABLE_UI");
|
||||||
|
if !enabled {
|
||||||
|
return Ok(Self {
|
||||||
|
enhancement: false,
|
||||||
|
alternate: args.alternate,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut lock = stdout().lock();
|
let mut lock = stdout().lock();
|
||||||
|
|
||||||
crossterm::execute!(lock, Hide)?;
|
crossterm::execute!(lock, Hide)?;
|
||||||
if alternate {
|
if args.alternate {
|
||||||
crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?;
|
crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,8 +52,9 @@ impl Environment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let environment = Self {
|
let environment = Self {
|
||||||
|
enabled,
|
||||||
enhancement,
|
enhancement,
|
||||||
alternate,
|
alternate: args.alternate,
|
||||||
};
|
};
|
||||||
|
|
||||||
panic::set_hook(Box::new(move |info| {
|
panic::set_hook(Box::new(move |info| {
|
||||||
@ -54,6 +68,10 @@ impl Environment {
|
|||||||
/// Uses the information collected from initialization to safely close down
|
/// Uses the information collected from initialization to safely close down
|
||||||
/// the terminal & restore it to it's previous state.
|
/// the terminal & restore it to it's previous state.
|
||||||
pub fn cleanup(&self, elegant: bool) -> super::Result<()> {
|
pub fn cleanup(&self, elegant: bool) -> super::Result<()> {
|
||||||
|
if !self.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let mut lock = stdout().lock();
|
let mut lock = stdout().lock();
|
||||||
|
|
||||||
if self.alternate {
|
if self.alternate {
|
||||||
|
|||||||
27
src/ui/init.rs
Normal file
27
src/ui/init.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//! Contains the code for initializing the UI and creating a [`ui::Handle`].
|
||||||
|
|
||||||
|
use crate::ui::{self, input, interface};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
impl crate::Tasks {
|
||||||
|
/// Initializes the UI itself, along with all of the tasks that are related to it.
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
pub async fn ui(&mut self, state: ui::State, args: &crate::Args) -> crate::Result<ui::Handle> {
|
||||||
|
let (utx, urx) = broadcast::channel(8);
|
||||||
|
|
||||||
|
#[cfg(feature = "mpris")]
|
||||||
|
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
||||||
|
|
||||||
|
let params = interface::Params::try_from(args)?;
|
||||||
|
if params.enabled {
|
||||||
|
self.spawn(ui::run(urx, state, params));
|
||||||
|
self.spawn(input::listen(self.tx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ui::Handle {
|
||||||
|
updater: utx,
|
||||||
|
#[cfg(feature = "mpris")]
|
||||||
|
mpris,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ use crate::{
|
|||||||
ui::{self, State},
|
ui::{self, State},
|
||||||
Args,
|
Args,
|
||||||
};
|
};
|
||||||
use std::{env, io::stdout, time::Duration};
|
use std::{io::stdout, time::Duration};
|
||||||
|
|
||||||
pub mod clock;
|
pub mod clock;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
@ -59,7 +59,7 @@ impl TryFrom<&Args> for Params {
|
|||||||
let delta = 1.0 / f32::from(args.fps);
|
let delta = 1.0 / f32::from(args.fps);
|
||||||
let delta = Duration::from_secs_f32(delta);
|
let delta = Duration::from_secs_f32(delta);
|
||||||
|
|
||||||
let disabled = env::var("LOWFI_DISABLE_UI").is_ok_and(|x| x == "1");
|
let disabled = crate::env("LOWFI_DISABLE_UI");
|
||||||
if disabled && !cfg!(feature = "mpris") {
|
if disabled && !cfg!(feature = "mpris") {
|
||||||
return Err(ui::Error::RejectedDisable);
|
return Err(ui::Error::RejectedDisable);
|
||||||
}
|
}
|
||||||
@ -141,7 +141,9 @@ impl Interface {
|
|||||||
/// Draws the terminal. This will also wait for the specified
|
/// Draws the terminal. This will also wait for the specified
|
||||||
/// delta to pass before completing.
|
/// delta to pass before completing.
|
||||||
pub async fn draw(&mut self, state: &State) -> super::Result<()> {
|
pub async fn draw(&mut self, state: &State) -> super::Result<()> {
|
||||||
self.clock.as_mut().map(|x| x.update(&mut self.window));
|
if let Some(x) = self.clock.as_mut() {
|
||||||
|
x.update(&mut self.window);
|
||||||
|
}
|
||||||
|
|
||||||
let menu = self.menu(state);
|
let menu = self.menu(state);
|
||||||
self.window.draw(stdout().lock(), menu)?;
|
self.window.draw(stdout().lock(), menu)?;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
//! Contains the code for the MPRIS server & other helper functions.
|
//! Contains the code for the MPRIS server & other helper functions.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
process,
|
process,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -328,7 +327,7 @@ impl Server {
|
|||||||
sender: mpsc::Sender<Message>,
|
sender: mpsc::Sender<Message>,
|
||||||
receiver: broadcast::Receiver<Update>,
|
receiver: broadcast::Receiver<Update>,
|
||||||
) -> ui::Result<Server> {
|
) -> ui::Result<Server> {
|
||||||
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
|
let suffix = if crate::env("LOWFI_FIXED_MPRIS_NAME") {
|
||||||
String::from("lowfi")
|
String::from("lowfi")
|
||||||
} else {
|
} else {
|
||||||
format!("lowfi.{}.instance{}", state.tracklist, process::id())
|
format!("lowfi.{}.instance{}", state.tracklist, process::id())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user