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.
14 KiB
🎧 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.
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.jsonpossè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
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 :
./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 :
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.
⚠️
usesCleartextTrafficest activé pour permettre l'usage d'un serveur média auto-hébergé enhttp://sur le réseau local. C'est un compromis assumé ; préférezhttpsdè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 identifiantfr.tuxgyver.noxelycomme 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 dansMusic/Cachettesurvivent — 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 :
giteaUpdateToken=…
⚠️ Un jeton embarqué dans un APK est extractible. Celui-ci est volontairement limité à la portée
read:repositoryet 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.
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.



