Tous les appareils partageaient le jeton du serveur : perdre un téléphone obligeait à changer ce jeton, donc à réappairer tout le monde. Chaque appairage émet désormais le sien. Révoquer devient une opération locale, vérifiée en appairant deux appareils puis en coupant le premier : 401 pour lui, 200 pour l'autre, jeton maître intact. L'interface du serveur les liste avec leur modèle, leur date d'appairage et leur dernière synchronisation — sans jamais afficher les jetons eux-mêmes, ce qui reviendrait à les distribuer. La table s'ajoute sans toucher aux données existantes et le jeton maître reste accepté : rien ne casse pour un appareil appairé avant cette version.
319 lines
14 KiB
Markdown
319 lines
14 KiB
Markdown
# 🎧 Noxely
|
||
|
||
> Un serveur cherche, guette les radios et enregistre. Le téléphone ne fait que récupérer les
|
||
> fichiers prêts — et garde sa batterie.
|
||
|
||
[](https://developer.android.com)
|
||
[](https://kotlinlang.org)
|
||
[](https://developer.android.com/jetpack/compose)
|
||
[](https://developer.android.com/media/media3)
|
||
[](VERSION)
|
||
[](LICENSE)
|
||
[](#-comment-ça-marche)
|
||
|
||
Noxely est un lecteur audio et vidéo pensé pour les trajets, les tunnels, les avions et les
|
||
forfaits épuisés. On choisit ce qu'on emporte, l'application le télécharge, et tout reste lisible
|
||
sans la moindre connexion.
|
||
|
||
---
|
||
|
||
## 📸 Aperçu
|
||
|
||
| Bibliothèque | Lecture | Hors ligne | Catalogues libres |
|
||
|:---:|:---:|:---:|:---:|
|
||
|  |  |  |  |
|
||
|
||
---
|
||
|
||
## 🏗️ Architecture
|
||
|
||
```
|
||
SERVEUR (Python / FastAPI, sur votre machine)
|
||
├── cherche dans les catalogues libres
|
||
├── guette les morceaux sur les radios
|
||
├── enregistre les directs, titrage ICY compris
|
||
└── tient les fichiers prêts
|
||
│ HTTP + jeton
|
||
▼
|
||
MOBILE (Android / Compose)
|
||
└── demande ce qui est neuf, télécharge, lit
|
||
Wi-Fi non facturé · batterie suffisante · toutes les 6 h
|
||
```
|
||
|
||
Tout ce qui écoutait en continu a migré sur le serveur. Le téléphone dort entre deux
|
||
synchronisations : c'est là qu'est l'économie.
|
||
|
||
## ✨ Ce que fait le serveur
|
||
|
||
| | |
|
||
|---|---|
|
||
| 🔎 **Recherche** | Internet Archive, Underground (netlabels + Free Music Archive), LibriVox, Wikimedia Commons |
|
||
| 🧾 **Choix piste par piste** | Un album affiche ses pistes avec leur taille avant d'être mis en file |
|
||
| 📡 **Guet multi-stations** | Nommez un morceau : les stations les plus écoutées sont balayées jusqu'à le trouver |
|
||
| 📻 **Enregistrement** | Capture du direct, nommée d'après le titre annoncé par la station |
|
||
| ⏳ **File de téléchargement** | Reprise après échec, état visible |
|
||
| 🖥️ **Interface web** | Tout se pilote depuis un navigateur, sans toucher au téléphone |
|
||
|
||
## 📱 Ce que fait le mobile
|
||
|
||
| | |
|
||
|---|---|
|
||
| 🔄 **Synchronisation sobre** | Toutes les 6 h, en Wi-Fi non facturé et batterie suffisante |
|
||
| 📱 **Appairage** | Un bouton côté serveur, un bouton côté téléphone : aucun jeton à recopier |
|
||
| 🔑 **Un jeton par appareil** | Chacun le sien, révocable seul depuis l'onglet Appareils |
|
||
| 🎵 **Lecture** | Audio et vidéo, arrière-plan, casque, écran verrouillé, ±15 s, stop |
|
||
| 📚 **Bibliothèque** | Médias reçus du serveur et contenu déjà présent sur l'appareil |
|
||
| 📋 **Playlists** | Listes nommées, appui long pour y ajouter un titre |
|
||
| 🖼️ **Pochettes** | Cherchées automatiquement pour les titres qui n'en ont pas |
|
||
| ℹ️ **À propos et mises à jour** | Version, garanties, journal, et suivi des releases du dépôt |
|
||
|
||
---
|
||
|
||
## 📦 Sources interrogées par le serveur
|
||
|
||
| Source | État | Détail |
|
||
|---|:---:|---|
|
||
| 📱 Appareil | ✅ | Musique et vidéos déjà sur le téléphone (côté mobile) |
|
||
| 🔗 Lien direct | ✅ | URL `http(s)` vers un fichier audio ou vidéo |
|
||
| 📻 Podcast RSS | ✅ | Flux standard avec `<enclosure>` |
|
||
| 🏛️ Internet Archive | ✅ | Domaine public / Creative Commons |
|
||
| 📚 LibriVox | ✅ | Livres audio du domaine public, filtrables par langue |
|
||
| 🎼 Wikimedia Commons | ✅ | Enregistrements classiques et archives sonores libres |
|
||
| 📻 Radio | ✅ | Annuaire Radio Browser, direct et enregistrement (flux HLS exclus) |
|
||
| 🎧 Underground | ✅ | Netlabels et Free Music Archive : ~94 000 morceaux d'artistes indépendants |
|
||
|
||
### 🔎 Catalogues évalués puis écartés
|
||
|
||
| Candidat | Pourquoi non |
|
||
|---|---|
|
||
| **Openverse** | A cessé de servir l'accès anonyme : **401 sur huit requêtes sur huit**, et jusqu'à 21 s de latence quand il répondait encore. Exige désormais une inscription. |
|
||
| **ccMixter** | La recherche fonctionne, mais les fichiers ne sont servis qu'avec un en-tête `Referer` — que Media3 ne permet pas de poser par téléchargement. |
|
||
| **Radio Browser** | Flux radio continus : rien à mettre en cache. |
|
||
|
||
### ⚖️ Ce que Noxely ne fait pas — et pourquoi
|
||
|
||
| Service | Pourquoi |
|
||
|---|---|
|
||
| 🎵 **Spotify** | Le SDK ne donne aucun accès au flux audio. Le hors-ligne n'existe que dans l'application Spotify. Un podcast également diffusé en RSS public reste ajoutable. |
|
||
| ▶️ **YouTube** | Les conditions d'utilisation interdisent d'extraire ou de stocker les flux. Le hors-ligne passe par YouTube Premium. |
|
||
| ☁️ **Google Drive / Photos** | Techniquement faisable pour vos propres fichiers, mais cela exige un identifiant OAuth Google lié à votre compte développeur, volontairement non embarqué ici. |
|
||
|
||
C'est un choix assumé : l'application ne contourne rien, elle travaille avec les sources qui
|
||
autorisent la copie locale.
|
||
|
||
---
|
||
|
||
## 🧭 Comment ça marche
|
||
|
||
```
|
||
Source ──► Bibliothèque ──► [ Garder ] ──► Cache Media3 ──► Lecture hors ligne
|
||
│
|
||
reprise auto (WorkManager)
|
||
```
|
||
|
||
Trois propriétaires de données, aucun doublon :
|
||
|
||
- **MediaStore** possède le contenu local — on le relit, on ne le recopie pas ;
|
||
- **`imported.json`** possède les seules références distantes ajoutées à la main ;
|
||
- **l'index Media3** possède l'état du cache — taille, progression, complétude.
|
||
|
||
Le cache utilise un `NoOpCacheEvictor` : il contient **exactement** ce que vous avez demandé à
|
||
garder, et rien n'en est effacé sans votre geste. La lecture lit ce cache mais n'y écrit jamais,
|
||
pour qu'une écoute en ligne ne le fasse pas gonfler à votre insu.
|
||
|
||
---
|
||
|
||
## 🚀 Démarrage
|
||
|
||
### Prérequis
|
||
|
||
- ☕ JDK 17
|
||
- 🤖 Android SDK avec la plateforme **36** et les build-tools 36
|
||
- 📱 Un appareil ou un émulateur en **Android 8.0 (API 26)** ou plus
|
||
|
||
### Construire
|
||
|
||
```bash
|
||
make build # APK de debug
|
||
make install # installe sur l'appareil connecté
|
||
make run # installe et lance
|
||
make lint # analyse Android Lint
|
||
make release # APK de release (signé si keystore.properties existe)
|
||
make publish # release Gitea : tag + notes + APK signé
|
||
make publish-dry # simulation, sans rien envoyer
|
||
make deploy # release puis publish
|
||
```
|
||
|
||
`make help` liste toutes les cibles, groupées par catégorie.
|
||
|
||
Sans `make`, les commandes Gradle équivalentes fonctionnent :
|
||
|
||
```bash
|
||
./gradlew assembleDebug
|
||
./gradlew installDebug
|
||
```
|
||
|
||
L'APK se trouve dans `app/build/outputs/apk/debug/app-debug.apk`.
|
||
|
||
### Signer une version de publication
|
||
|
||
Créez un `keystore.properties` à la racine — il est ignoré par git :
|
||
|
||
```properties
|
||
storeFile=cachette.jks
|
||
storePassword=...
|
||
keyAlias=cachette
|
||
keyPassword=...
|
||
```
|
||
|
||
### 📤 Publier une version
|
||
|
||
`make publish` crée le tag, la release Gitea et y attache l'APK **signé**. La cible est
|
||
idempotente : relancée sur une version déjà publiée, elle réutilise la release et remplace l'asset.
|
||
`make publish-dry` répète l'opération à blanc.
|
||
|
||
La publication délègue à `gitea_release.sh` de **BorisSkill**, qui déduit l'hôte, le propriétaire
|
||
et le dépôt du remote `origin` — il n'y a donc aucun nom de dépôt à configurer ici. Le chemin du
|
||
script est surchargeable : `make publish GITEA_RELEASE=/chemin/gitea_release.sh`.
|
||
|
||
Prérequis :
|
||
|
||
| Élément | Rôle |
|
||
|---|---|
|
||
| `VERSION` | Source unique de la version — `versionCode` en est déduit (`1.0.0` → `10000`) |
|
||
| `CHANGELOG.md` | La section `## [x.y.z]` devient le corps des notes de release |
|
||
| `keystore.properties` | Signature — sans lui l'APK est `-unsigned` et refusé à la publication |
|
||
| Dépôt git | Un `origin` configuré : le tag `vx.y.z` y est poussé |
|
||
|
||
Le jeton Gitea est résolu dans l'ordre : `$GITEA_TOKEN`, `.gitea-token` à la racine du dépôt,
|
||
`~/.config/gitea/token`, puis repli sur `~/.git-credentials`. Aucun secret n'est stocké dans ce
|
||
dépôt.
|
||
|
||
Pour monter de version, éditez `VERSION` et ajoutez la section correspondante au `CHANGELOG.md` :
|
||
Gradle, le tag git et les notes de release suivront seuls.
|
||
|
||
---
|
||
|
||
## 🔐 Permissions et vie privée
|
||
|
||
| Permission | Pourquoi |
|
||
|---|---|
|
||
| `READ_MEDIA_AUDIO` / `READ_MEDIA_VIDEO` | Lister la musique et les vidéos de l'appareil |
|
||
| `INTERNET` | Télécharger ce que vous avez choisi de garder |
|
||
| `POST_NOTIFICATIONS` | Commandes de lecture et progression des téléchargements |
|
||
| `FOREGROUND_SERVICE_*` | Continuer la lecture et les téléchargements écran éteint |
|
||
| `REQUEST_INSTALL_PACKAGES` | Proposer l'installation d'une mise à jour téléchargée |
|
||
|
||
Aucune donnée ne quitte l'appareil : pas de compte, pas de télémétrie, pas de sauvegarde cloud
|
||
(`dataExtractionRules` exclut explicitement le contenu mis en cache). Les seules connexions
|
||
sortantes sont celles des URL que vous ajoutez et des catalogues que vous interrogez.
|
||
|
||
> ⚠️ `usesCleartextTraffic` est activé pour permettre l'usage d'un serveur média auto-hébergé en
|
||
> `http://` sur le réseau local. C'est un compromis assumé ; préférez `https` dès que possible.
|
||
|
||
---
|
||
|
||
> ⚠️ **Changement de nom en 1.6.0.** L'application s'appelait Cachette et son identifiant était
|
||
> `fr.tuxgyver.cachette`. Android traite le nouvel identifiant `fr.tuxgyver.noxely` comme une
|
||
> application distincte : une installation en 1.5.0 ne se met pas à jour toute seule, il faut la
|
||
> désinstaller puis installer Noxely. Les enregistrements déjà rangés dans `Music/Cachette`
|
||
> survivent — ils sont dans le stockage partagé — et restent lisibles par tout lecteur.
|
||
|
||
---
|
||
|
||
## 📼 Où vont les enregistrements
|
||
|
||
À partir d'Android 10, dans `Music/Noxely/`, déclarés dans MediaStore : ils sont donc visibles
|
||
de tous les lecteurs de l'appareil et survivent à la désinstallation. En deçà, ils restent dans le
|
||
dossier privé de l'application, MediaStore y réclamant une permission de stockage dont Noxely n'a
|
||
pas besoin par ailleurs.
|
||
|
||
Le nom du fichier porte le titre du morceau — et pas seulement la colonne `TITLE` : en publiant le
|
||
fichier, MediaStore le relit et réécrit ce champ à partir du nom. Le scan local, lui, ignore
|
||
`Music/Noxely` pour ne pas afficher deux fois ce que la bibliothèque connaît déjà.
|
||
|
||
Chaque enregistrement se supprime individuellement depuis l'écran **Hors ligne** : fichier, entrée
|
||
MediaStore et référence en bibliothèque partent ensemble. « Tout libérer » ne les touche pas.
|
||
|
||
---
|
||
|
||
## 🆕 Mises à jour
|
||
|
||
Noxely étant distribuée hors magasin, elle surveille elle-même les releases du dépôt.
|
||
|
||
```
|
||
Release publiée ──► vérification quotidienne (WorkManager, Wi-Fi)
|
||
│
|
||
├─ version plus récente ? ──► téléchargement de l'APK
|
||
│ │
|
||
└─ sinon : rien ▼
|
||
notification « prête à installer »
|
||
│
|
||
installation = geste utilisateur
|
||
```
|
||
|
||
L'installation reste un geste explicite : Android l'impose à toute application hors magasin.
|
||
L'écran **Sources** porte la même vérification à la demande, et affiche la version installée.
|
||
|
||
Le dépôt étant privé, la lecture des releases demande un jeton. Il est lu depuis
|
||
`update-token.properties` (ignoré par git) et injecté dans `BuildConfig` à la compilation :
|
||
|
||
```properties
|
||
giteaUpdateToken=…
|
||
```
|
||
|
||
> ⚠️ Un jeton embarqué dans un APK est extractible. Celui-ci est volontairement limité à la portée
|
||
> `read:repository` et révocable depuis Gitea. Il donne un accès en lecture aux dépôts du compte —
|
||
> ne pas lui accorder davantage.
|
||
|
||
---
|
||
|
||
## 🗂️ Structure
|
||
|
||
```
|
||
app/src/main/java/fr/tuxgyver/cachette/
|
||
├── NoxelyApp.kt Application + conteneur de dépendances (assemblé à la main)
|
||
├── MainActivity.kt
|
||
├── model/ MediaEntry, MediaKind, SourceKind
|
||
├── library/ MediaStore, persistance des imports, bibliothèque unifiée
|
||
├── sources/ Import URL et RSS, catalogues Internet Archive et LibriVox
|
||
├── offline/ Cache Media3, gestionnaire et service de téléchargement
|
||
├── playback/ MediaSessionService, ExoPlayer, contrôleur d'interface
|
||
├── radio/ Annuaire des stations, service d'enregistrement du direct
|
||
├── update/ Vérification des releases, téléchargement, notification
|
||
└── ui/ Compose — thème, bibliothèque, hors ligne, playlists, sources,
|
||
parcours des catalogues, choix des pistes, lecteur
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 État de vérification
|
||
|
||
Validé sur émulateur Pixel (Android 17, API 37) :
|
||
|
||
- ✅ Scan MediaStore audio et vidéo, vignettes comprises
|
||
- ✅ Lecture avec session média système et position qui progresse
|
||
- ✅ Import d'un flux RSS et de ses épisodes
|
||
- ✅ Téléchargement d'un épisode jusqu'à l'état « disponible hors connexion »
|
||
- ✅ Recherche Internet Archive, résolution du fichier et mise en cache
|
||
- ✅ Android Lint sans erreur
|
||
|
||
---
|
||
|
||
## 📄 Licence
|
||
|
||
**GNU General Public License v3.0** — le texte intégral est dans [LICENSE](LICENSE).
|
||
|
||
Copyright © 2026 Johnny (tuxgyver)
|
||
|
||
Noxely est un logiciel libre : vous pouvez le redistribuer et le modifier selon les termes de la
|
||
GPL v3, telle que publiée par la Free Software Foundation, au choix dans sa version 3 ou dans une
|
||
version ultérieure.
|
||
|
||
Il est distribué dans l'espoir d'être utile, mais **sans aucune garantie**, sans même la garantie
|
||
implicite de qualité marchande ou d'adéquation à un usage particulier. Voir la licence pour le
|
||
détail.
|
||
|
||
Ce choix engage aussi celui qui redistribue : toute version modifiée diffusée doit l'être sous la
|
||
même licence, code source compris.
|