3 Commits
Author SHA1 Message Date
tuxgyver 53f5fc1148 Le serveur se présente, et range son jeton (2.3.0)
L'interface du serveur affichait un champ de jeton dans son en-tête, visible
en permanence alors qu'il ne sert qu'une fois : il descend dans l'onglet
Réglages, avec le nom du serveur, sa version et le nombre d'appareils
appairés.

Elle gagne surtout une page À propos sur le modèle des autres projets : cinq
sections, contenu séparé du rendu, version lue de /api/health et journal servi
depuis le CHANGELOG du dépôt — rien de recopié, donc rien qui puisse diverger.

Au passage, le serveur annonçait 1.0.0 depuis six publications ; il lit
maintenant le fichier VERSION comme le client.
2026-09-13 22:10:44 +02:00
tuxgyver 28cb545685 Un jeton par appareil, révocable seul (2.2.0)
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.
2026-09-13 21:23:19 +02:00
tuxgyver 51b60cda70 Appairage du téléphone, en-têtes de licence (2.1.0)
Recopier un jeton de trente-trois caractères sur un clavier tactile n'était pas
un réglage, c'était une corvée. On ouvre désormais une fenêtre depuis le
serveur, on appuie sur Appairer côté téléphone, et c'est fait.

Le jeton n'est pas pour autant distribué : ouvrir la fenêtre l'exige déjà,
elle dure deux minutes, se referme dès qu'un appareil s'est servi, et ne
répond qu'à une adresse privée. Le champ de saisie reste là pour qui préfère
le coller.

Les cinquante fichiers de code portent en outre leur notice de licence, comme
la GPL le recommande. Elle est en français, comme le reste ; le texte qui fait
foi demeure celui de LICENSE, non traduit.
2026-09-13 20:15:14 +02:00
54 changed files with 1101 additions and 20 deletions

No files matched your search

+68
View File
@@ -5,6 +5,74 @@ Toutes les évolutions notables de Noxely sont documentées ici.
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
et le versionnage [SemVer](https://semver.org/lang/fr/).
## [2.3.0] — 2026-09-13
### Ajouté
- **Page « À propos » du serveur.** Un onglet `ℹ️ À propos` avec en-tête d'identité et cinq
sections : ce que le serveur fait, ce qu'il garantit, avec quoi il est construit, ce qui a
changé, et qui en répond. Le contenu vit dans un module de constantes séparé du rendu.
- Le journal est **lu** et non recopié : une route `/api/changelog` sert le `CHANGELOG.md` du
dépôt, que la page interprète pour ce qu'il emploie réellement — titres, puces et gras.
### Modifié
- **Le champ « jeton » quitte l'en-tête du serveur**, où il n'avait rien à faire, pour l'onglet
`⚙️ Réglages`. Une carte y donne aussi le nom du serveur, sa version et le nombre d'appareils
appairés. Sans jeton connu, la pastille d'état indique où le saisir.
- **Le serveur annonce la version du dépôt.** Il lisait une constante figée à `1.0.0`, restée en
arrière de six publications ; il lit maintenant le fichier `VERSION`, comme le client.
### Corrigé
- `saveToken()` relançait l'amorçage du jeton au lieu de rafraîchir la page : un bloc y avait été
déplacé par erreur lors d'une édition antérieure.
- Une garantie annoncée à tort. La page affirmait que toute route hors `/api/health` exigeait un
jeton ; la page elle-même, ses ressources et le journal répondent sans. Les sept garanties ont
été confrontées au code une à une, celle-ci reformulée.
## [2.2.0] — 2026-09-13
### Ajouté
- **Un jeton par appareil.** Chaque appairage émet désormais sa propre valeur, au lieu de
distribuer le jeton maître. Révoquer un téléphone perdu n'atteint plus les autres : vérifié en
appairant deux appareils, en révoquant le premier, et en constatant que le second continue de
se synchroniser (401 pour l'un, 200 pour l'autre, jeton maître intact).
- **Onglet « Appareils »** dans l'interface du serveur : nom du modèle, date d'appairage,
dernière synchronisation — ou « jamais synchronisé » —, et révocation d'un clic.
- Le téléphone s'annonce par son modèle lors de l'appairage : un appareil qu'on ne sait pas
nommer ne se révoque pas.
### Sécurité
- Les jetons d'appareil ne sont **jamais renvoyés** par la route qui les liste : les afficher
reviendrait à les distribuer.
- La date de dernière utilisation est enregistrée à chaque appel authentifié, ce qui permet de
repérer un appareil oublié.
### Compatibilité
- La table s'ajoute sans toucher aux données existantes, et le jeton maître reste accepté : un
appareil appairé avant cette version garde l'accès. Le réappairer lui donnera un jeton propre,
et donc une révocation individuelle.
## [2.1.0] — 2026-09-13
### Ajouté
- **Appairage du téléphone.** Plus de jeton à recopier : on ouvre une fenêtre depuis l'interface
du serveur, on appuie sur « Appairer » dans Réglages ▸ Connexion, et l'application reçoit le
jeton. Le champ de saisie reste disponible pour qui préfère le coller à la main.
Le téléphone ne peut pas deviner un jeton — c'est ce qu'il protège — mais il peut le recevoir,
sous trois conditions cumulées : ouvrir la fenêtre exige déjà le jeton, elle expire au bout de
deux minutes, et elle se referme dès qu'un appareil s'est servi. La demande doit en outre venir
d'une adresse privée : un appairage n'est jamais joignable depuis Internet.
- En-têtes de licence GPL v3 dans les cinquante fichiers de code (Kotlin, Python, HTML), rédigés
en français comme le reste du projet, le texte faisant foi restant celui de `LICENSE`.
## [2.0.1] — 2026-09-13
### Ajouté
+3 -1
View File
@@ -7,7 +7,7 @@
[![Kotlin](https://img.shields.io/badge/Kotlin-2.1.0-7F52FF?logo=kotlin&logoColor=white)](https://kotlinlang.org)
[![Compose](https://img.shields.io/badge/Jetpack%20Compose-Material%203-4285F4?logo=jetpackcompose&logoColor=white)](https://developer.android.com/jetpack/compose)
[![Media3](https://img.shields.io/badge/Media3-1.11.1-FF6F00?logo=googleplay&logoColor=white)](https://developer.android.com/media/media3)
[![Version](https://img.shields.io/badge/Version-2.0.1-6B4EFF)](VERSION)
[![Version](https://img.shields.io/badge/Version-2.2.0-6B4EFF)](VERSION)
[![Licence](https://img.shields.io/badge/Licence-GPL%20v3-3FD8C0)](LICENSE)
[![Hors ligne](https://img.shields.io/badge/Hors%20ligne-100%25-3FD8C0)](#-comment-ça-marche)
@@ -59,6 +59,8 @@ synchronisations : c'est là qu'est l'économie.
| | |
|---|---|
| 🔄 **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 |
+1 -1
View File
@@ -1 +1 @@
2.0.1
2.3.0
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely
import android.Manifest
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely
import android.app.Application
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.library
import fr.tuxgyver.noxely.model.MediaEntry
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.library
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.library
import fr.tuxgyver.noxely.model.MediaEntry
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.library
import android.content.ContentUris
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.library
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.model
import kotlinx.serialization.Serializable
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.model
import kotlinx.serialization.Serializable
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.offline
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.offline
import android.app.Notification
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.offline
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.playback
import androidx.core.net.toUri
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.playback
import android.app.PendingIntent
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.playback
import android.content.ComponentName
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.radio
import android.content.ContentValues
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.sources
import java.net.HttpURLConnection
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.sync
import android.content.Context
@@ -1,6 +1,19 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.sync
import android.content.Context
import android.os.Build
import fr.tuxgyver.noxely.library.ImportedStore
import fr.tuxgyver.noxely.model.MediaEntry
import fr.tuxgyver.noxely.model.MediaKind
@@ -44,6 +57,44 @@ class SyncClient(
}
}
/**
* Récupère le jeton auprès du serveur, pendant sa fenêtre d'appairage.
*
* Le téléphone ne peut pas deviner un jeton — c'est ce qu'il protège. Il peut en revanche le
* recevoir, à condition que quelqu'un ait ouvert la fenêtre depuis l'interface du serveur, et
* que la demande vienne du réseau local.
*/
suspend fun pair(baseUrl: String): Result<String> = withContext(Dispatchers.IO) {
runCatching {
val base = baseUrl.trim().trimEnd('/')
val connection = openConnection("$base/api/pair", readTimeoutMs = 20_000).apply {
requestMethod = "POST"
doOutput = true
setRequestProperty("Content-Type", "application/json")
// Le nom du modèle rend l'appareil reconnaissable dans la liste du serveur.
val name = (Build.MANUFACTURER + " " + Build.MODEL).trim()
outputStream.use { it.write(("{" + "\"name\":\"" + name + "\"}").toByteArray()) }
}
val body = try {
when (connection.responseCode) {
403 -> throw IllegalStateException(
connection.errorStream?.bufferedReader()?.readText()
?.let { json.parseToJsonElement(it).jsonObject.text("detail") }
?: "Aucun appairage en cours sur le serveur"
)
in 200..299 -> connection.inputStream.bufferedReader().readText()
else -> throw IllegalStateException("Le serveur a répondu ${connection.responseCode}")
}
} finally {
connection.disconnect()
}
val payload = json.parseToJsonElement(body).jsonObject
val token = payload.text("token") ?: throw IllegalStateException("Réponse sans jeton")
settings.update { it.copy(baseUrl = base, token = token) }
"${payload.text("name")} ${payload.text("version")}"
}
}
suspend fun sync(): SyncResult = withContext(Dispatchers.IO) {
val config = settings.config.value
if (!config.isConfigured) return@withContext SyncResult(0, 0, "Serveur non configuré")
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.sync
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui
import androidx.activity.compose.BackHandler
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.about
import androidx.compose.foundation.Image
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.about
import fr.tuxgyver.noxely.BuildConfig
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.about
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.components
import android.graphics.Bitmap
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.components
import java.util.Locale
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.components
import androidx.compose.animation.animateColorAsState
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.library
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.offline
import androidx.compose.foundation.layout.Arrangement
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.player
import androidx.compose.foundation.basicMarquee
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.player
import androidx.annotation.OptIn
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.playlists
import androidx.compose.foundation.clickable
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.playlists
import androidx.compose.foundation.layout.Arrangement
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.playlists
import androidx.compose.foundation.clickable
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.server
import androidx.compose.foundation.layout.Arrangement
@@ -127,7 +139,7 @@ fun ServerScreen(
OutlinedTextField(
value = token,
onValueChange = { token = it; probe = null },
label = { Text("Jeton") },
label = { Text("Jeton (rempli par l'appairage)") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
@@ -135,6 +147,26 @@ fun ServerScreen(
Spacer(Modifier.size(14.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Button(
enabled = !busy && url.isNotBlank(),
onClick = {
busy = true
probe = null
scope.launch {
container.sync.pair(url).fold(
onSuccess = { name ->
token = container.serverSettings.config.value.token
probe = "Connecté à $name — jeton reçu"
},
onFailure = { probe = it.message ?: "Appairage impossible" },
)
busy = false
}
},
) { Text("Appairer") }
Spacer(Modifier.width(10.dp))
TextButton(
enabled = !busy && url.isNotBlank() && token.isNotBlank(),
onClick = {
busy = true
@@ -152,7 +184,7 @@ fun ServerScreen(
busy = false
}
},
) { Text("Tester et enregistrer") }
) { Text("Tester") }
Spacer(Modifier.width(10.dp))
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.sources
import androidx.compose.foundation.layout.Column
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.update
import android.content.Context
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.update
import android.Manifest
@@ -1,3 +1,15 @@
/*
* Noxely — collecte côté serveur, écoute hors connexion côté mobile.
* Copyright © 2026 Johnny (tuxgyver)
*
* Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
* selon les termes de la GNU General Public License, version 3 ou ultérieure,
* telle que publiée par la Free Software Foundation.
*
* Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
* Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
*/
package fr.tuxgyver.noxely.update
import android.content.Context
+10
View File
@@ -0,0 +1,10 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
+10
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Point d'entrée : `python -m noxely_server`."""
from __future__ import annotations
+107 -4
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""API HTTP du serveur Noxely.
Le client Android ne consomme que `/api/health`, `/api/library` et `/api/media/{id}/file` :
@@ -7,18 +17,25 @@ mobile doit pouvoir se contenter d'un sondage bref, c'est là qu'est le gain de
from __future__ import annotations
import asyncio
import ipaddress
import secrets
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, Response
from . import config as config_module
from .sources import catalogs, radio
from .storage import Storage
from .workers import Downloader, Watcher, safe_name
VERSION = "1.0.0"
# Client et serveur sortent du même dépôt, à la même version : on lit le fichier
# que la chaîne de publication incrémente, plutôt qu'un numéro à tenir à jour ici.
REPO_ROOT = Path(__file__).resolve().parents[2]
VERSION = (REPO_ROOT / "VERSION").read_text().strip() if (REPO_ROOT / "VERSION").exists() else "0.0.0"
PAIRING_WINDOW_SECONDS = 120
WEB_DIR = Path(__file__).parent / "web"
config = config_module.load()
@@ -28,9 +45,18 @@ watcher = Watcher(config, storage)
def authorised(x_noxely_token: str = Header(default="")) -> None:
"""Sans jeton valide, le serveur ne répond rien : il expose des fichiers."""
if x_noxely_token != config.token:
"""Sans jeton valide, le serveur ne répond rien : il expose des fichiers.
Deux formes valent : le jeton maître, qui pilote le serveur depuis le navigateur, et celui
d'un appareil appairé. Les distinguer est ce qui permet de révoquer un téléphone perdu sans
déranger les autres.
"""
if x_noxely_token and x_noxely_token == config.token:
return
device = storage.device_by_token(x_noxely_token) if x_noxely_token else None
if not device:
raise HTTPException(status_code=401, detail="Jeton invalide")
storage.touch_device(device["id"])
@asynccontextmanager
@@ -180,6 +206,68 @@ def queue_state() -> dict:
return {"items": [m.public() for m in pending if m.state != "ready"][:50]}
#: Fenêtre d'appairage : instant d'expiration, ou None si elle est fermée.
_pairing_until: float | None = None
def _is_private(host: str) -> bool:
"""Le réseau local, et lui seul."""
try:
address = ipaddress.ip_address(host)
return address.is_private or address.is_loopback
except ValueError:
return host in ("localhost", "")
@app.post("/api/pair/open", dependencies=[Depends(authorised)])
def open_pairing() -> dict:
"""Ouvre une fenêtre de deux minutes pendant laquelle un appareil du réseau local peut
réclamer le jeton. Fermée le reste du temps : sinon le jeton ne protégerait plus rien."""
global _pairing_until
_pairing_until = time.time() + PAIRING_WINDOW_SECONDS
return {"open": True, "seconds": PAIRING_WINDOW_SECONDS}
@app.get("/api/pair/state", dependencies=[Depends(authorised)])
def pairing_state() -> dict:
remaining = int(_pairing_until - time.time()) if _pairing_until else 0
return {"open": remaining > 0, "seconds": max(0, remaining)}
@app.get("/api/devices", dependencies=[Depends(authorised)])
def devices() -> dict:
return {"devices": storage.devices()}
@app.delete("/api/devices/{device_id}", dependencies=[Depends(authorised)])
def revoke_device(device_id: str) -> dict:
"""Révoque un appareil : son jeton cesse immédiatement d'être accepté."""
return {"revoked": storage.delete_device(device_id)}
@app.post("/api/pair")
def pair(request: Request, payload: dict = Body(default={})) -> dict:
"""Livre le jeton à un appareil du réseau local, pendant la fenêtre d'appairage.
La fenêtre se referme dès qu'un appareil s'est servi : une ouverture, un appairage.
"""
global _pairing_until
client = request.client.host if request.client else ""
if not _pairing_until or time.time() > _pairing_until:
raise HTTPException(
status_code=403,
detail="Aucun appairage en cours. Ouvrez-en un depuis l'interface du serveur.",
)
if not _is_private(client):
raise HTTPException(status_code=403, detail="Appairage réservé au réseau local")
_pairing_until = None
# Chaque appairage reçoit son propre jeton : révoquer l'un n'atteint pas les autres.
token = secrets.token_urlsafe(24)
name = str(payload.get("name") or "Appareil").strip()[:60]
storage.add_device(name, token)
return {"token": token, "name": "Noxely", "version": VERSION, "device": name}
@app.get("/api/bootstrap")
def bootstrap(request: Request) -> dict:
"""Livre le jeton — uniquement à une requête venue de la machine elle-même.
@@ -193,6 +281,21 @@ def bootstrap(request: Request) -> dict:
return {"token": config.token}
@app.get("/api/changelog", response_class=PlainTextResponse)
def changelog() -> str:
"""Le journal du dépôt, lu à la demande plutôt que recopié dans la page."""
path = REPO_ROOT / "CHANGELOG.md"
return path.read_text(encoding="utf-8") if path.exists() else ""
@app.get("/about.js", response_class=PlainTextResponse)
def about_script() -> Response:
return Response(
(WEB_DIR / "about.js").read_text(encoding="utf-8"),
media_type="application/javascript; charset=utf-8",
)
@app.get("/", response_class=HTMLResponse)
def index() -> str:
return (WEB_DIR / "index.html").read_text(encoding="utf-8")
+10
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Configuration du serveur Noxely, lue de l'environnement.
Aucune valeur sensible n'est écrite dans le dépôt : le jeton et les chemins viennent d'un
+10
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Catalogues libres et annuaire radio, portés depuis le client Android.
Les pièges vérifiés côté mobile restent valables ici et sont conservés en commentaire :
+10
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Recherche dans les catalogues libres de droits."""
from __future__ import annotations
+10
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Annuaire des stations et lecture des métadonnées ICY."""
from __future__ import annotations
+51
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Index des médias et des guets, en SQLite.
Les fichiers vivent sur le disque ; la base ne porte que leurs métadonnées et l'état de la
@@ -32,6 +42,15 @@ CREATE TABLE IF NOT EXISTS media (
);
CREATE INDEX IF NOT EXISTS media_updated ON media(updated_at);
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
last_seen INTEGER
);
CREATE INDEX IF NOT EXISTS devices_token ON devices(token);
CREATE TABLE IF NOT EXISTS watches (
id TEXT PRIMARY KEY,
query TEXT NOT NULL,
@@ -140,6 +159,38 @@ class Storage:
self._db.commit()
return item
# --- Appareils ------------------------------------------------------
def add_device(self, name: str, token: str) -> str:
device_id = uuid.uuid4().hex
self._db.execute(
"INSERT INTO devices (id,name,token,created_at) VALUES (?,?,?,?)",
(device_id, name.strip() or "Appareil", token, now()),
)
self._db.commit()
return device_id
def device_by_token(self, token: str) -> dict | None:
row = self._db.execute("SELECT * FROM devices WHERE token = ?", (token,)).fetchone()
return dict(row) if row else None
def touch_device(self, device_id: str) -> None:
"""Dernière utilisation : c'est ce qui permet de reconnaître un appareil oublié."""
self._db.execute("UPDATE devices SET last_seen = ? WHERE id = ?", (now(), device_id))
self._db.commit()
def devices(self) -> list[dict]:
return [
# Le jeton n'est jamais renvoyé : le lister reviendrait à le distribuer.
{k: v for k, v in dict(row).items() if k != "token"}
for row in self._db.execute("SELECT * FROM devices ORDER BY created_at DESC")
]
def delete_device(self, device_id: str) -> bool:
cursor = self._db.execute("DELETE FROM devices WHERE id = ?", (device_id,))
self._db.commit()
return cursor.rowcount > 0
# --- Guets ----------------------------------------------------------
def add_watch(self, query: str) -> str:
+73
View File
@@ -0,0 +1,73 @@
// Noxely — collecte côté serveur, écoute hors connexion côté mobile.
// Copyright © 2026 Johnny (tuxgyver)
//
// Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
// selon les termes de la GNU General Public License, version 3 ou ultérieure,
// telle que publiée par la Free Software Foundation.
//
// Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
// Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
// Contenu de la page « À propos », séparé de son rendu.
// Tout ce qui est écrit ici se vérifie dans le dépôt : une fonction décrite existe, une garantie
// annoncée est tenue par du code. La version vient de /api/health, jamais recopiée.
const ABOUT = {
name: 'Noxely — serveur',
tagline: 'Il cherche, il guette, il enregistre. Le téléphone n\'a plus qu\'à écouter.',
repository: 'https://git.tuxtech.fr/tuxgyver/Noxely',
author: 'Johnny',
authorRole: 'Conception, direction et supervision',
authorship:
'Application développée avec l\'assistance de l\'IA, sous la direction et la supervision de Johnny.',
license: 'GPL v3',
copyright: 'Copyright © 2026 Johnny (tuxgyver)',
// Ce que fait le serveur, du point de vue de qui s'en sert.
features: [
['Chercher partout à la fois',
'Internet Archive, Underground (netlabels et Free Music Archive), LibriVox et Wikimedia ' +
'Commons sont interrogés en parallèle, les résultats fondus et les plus légers mis en tête.'],
['Choisir avant de télécharger',
'Un album ou un livre affiche ses pistes avec leur taille : on met en file ce qu\'on veut, ' +
'pas l\'ensemble.'],
['Guetter un morceau sur les radios',
'On nomme un titre sans désigner de station : les plus écoutées sont balayées en continu, ' +
'et l\'enregistrement part dès que l\'une le diffuse.'],
['Enregistrer un direct',
'La capture prend le nom du morceau annoncé par la station, et s\'arrête quand il cède la ' +
'place au suivant.'],
['Tenir la file',
'Les téléchargements s\'enchaînent seuls ; un échec est repris, son état reste visible.'],
['Servir le mobile',
'L\'application demande ce qui a changé depuis son dernier passage et récupère des fichiers ' +
'déjà prêts — c\'est ce qui lui épargne sa batterie.'],
],
guaranteesIntro:
'Noxely est un outil personnel : rien n\'y transite par un service tiers, et le serveur ne ' +
'répond qu\'à ce qui présente un jeton valide.',
// Chaque promesse est tenue par du code.
guarantees: [
'Aucun compte, aucune clé d\'API : les quatre catalogues et l\'annuaire radio sont publics.',
'Toute route de données exige un jeton — celui du serveur, ou celui d\'un appareil appairé ; ' +
'seuls l\'état de santé, la page et ses ressources répondent sans.',
'Chaque appareil reçoit son propre jeton : en révoquer un ne coupe pas les autres.',
'Les jetons d\'appareil ne sont jamais renvoyés par la route qui les liste.',
'L\'appairage n\'est possible que pendant une fenêtre ouverte depuis cette interface, deux ' +
'minutes durant, une seule fois, et depuis le réseau local uniquement.',
'Les catalogues ne proposent que des œuvres libres ou du domaine public ; les éléments en ' +
'accès restreint sont écartés avant même d\'être affichés.',
'Une synchronisation partielle ne fait pas avancer le curseur du client : rien n\'est perdu.',
],
// Couche → outils.
stack: [
['Service', 'Python, FastAPI, uvicorn, unité systemd'],
['Données', 'SQLite pour l\'index, fichiers sur disque pour les médias'],
['Collecte', 'httpx asynchrone, flux ICY pour les titres de radio'],
['Catalogues', 'Internet Archive, netlabels, LibriVox, Wikimedia Commons'],
['Interface', 'HTML, CSS et JavaScript sans dépendance'],
['Synchronisation', 'HTTP avec jeton, curseur incrémental côté client'],
],
};
+187 -12
View File
@@ -1,4 +1,15 @@
<!doctype html>
<!--
Noxely — collecte côté serveur, écoute hors connexion côté mobile.
Copyright © 2026 Johnny (tuxgyver)
Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
selon les termes de la GNU General Public License, version 3 ou ultérieure,
telle que publiée par la Free Software Foundation.
Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
-->
<html lang="fr">
<head>
<meta charset="utf-8">
@@ -37,6 +48,25 @@
.pill.ok { background:rgba(17,168,148,.16); color:var(--mint); }
.pill.warn { background:rgba(224,98,155,.16); color:#e0629b; }
.empty { color:var(--muted); text-align:center; padding:40px 10px; }
/* Page « À propos » : en-tête d'identité figé, corps en sous-onglets. */
.about-head { display:flex; gap:16px; align-items:center; margin-bottom:16px; }
.about-logo { width:80px; height:80px; border-radius:22px; background:var(--accent);
display:grid; place-items:center; font-size:40px; flex:none; }
.about-head h2 { margin:0 0 2px; font-size:20px; }
.about-head p { margin:0 0 8px; color:var(--muted); font-size:14px; }
.pill.accent { background:var(--accent); color:#fff; }
.subtabs { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:14px; }
.subtabs button { background:none; border:1px solid var(--line); border-radius:999px;
padding:5px 12px; color:var(--muted); cursor:pointer; font-size:13px; }
.subtabs button[aria-selected="true"] { border-color:var(--accent); color:var(--accent); }
.about-body { max-height:none; }
.about-body .card { display:block; }
.about-body ul { margin:0; padding-left:20px; }
.about-body li { margin:5px 0; }
.tech { width:100%; border-collapse:collapse; }
.tech td { padding:9px 4px; border-bottom:1px solid var(--line); vertical-align:top; }
.tech td:first-child { color:var(--muted); width:38%; }
.log h4 { margin:0 0 4px; font-size:15px; }
</style>
</head>
<body>
@@ -44,8 +74,7 @@
<h1>🎧 Noxely — serveur</h1>
<span id="status" class="pill">…</span>
<span class="grow" style="flex:1"></span>
<input type="password" id="token" placeholder="Jeton" style="width:190px">
<button class="ghost" onclick="saveToken()">Enregistrer</button>
<button class="go" onclick="openPairing()">📱 Appairer</button>
</header>
<main>
<div class="tabs">
@@ -53,6 +82,9 @@
<button onclick="show('watch')" aria-selected="false" id="t-watch">📡 Guets</button>
<button onclick="show('queue')" aria-selected="false" id="t-queue">⏳ File</button>
<button onclick="show('library')" aria-selected="false" id="t-library">📚 Bibliothèque</button>
<button onclick="show('devices')" aria-selected="false" id="t-devices">📱 Appareils</button>
<button onclick="show('settings')" aria-selected="false" id="t-settings">⚙️ Réglages</button>
<button onclick="show('about')" aria-selected="false" id="t-about">ℹ️ À propos</button>
</div>
<section id="p-search">
@@ -76,8 +108,39 @@
<section id="p-queue" hidden><div id="queue"></div></section>
<section id="p-library" hidden><div id="library"></div></section>
<section id="p-settings" hidden>
<div class="card" style="display:block">
<b>Jeton d'accès</b>
<small>Rempli tout seul quand cette page est ouverte depuis la machine du serveur.
Depuis un autre poste, collez celui que donne <code>make server-token</code>.</small>
<div class="row" style="margin-top:12px">
<input type="password" id="token" placeholder="Jeton" style="flex:1">
<button class="go" onclick="saveToken()">Enregistrer</button>
</div>
</div>
<div id="serverInfo"></div>
</section>
<section id="p-about" hidden>
<div class="about-head">
<div class="about-logo">🎧</div>
<div>
<h2 id="aboutName">Noxely</h2>
<p id="aboutTagline"></p>
<div class="row" style="margin:0" id="aboutBadges"></div>
</div>
</div>
<div class="subtabs" id="aboutTabs"></div>
<div class="about-body" id="aboutBody"></div>
</section>
<section id="p-devices" hidden>
<div class="row"><button class="go" onclick="openPairing()">Appairer un appareil</button></div>
<div id="devices"></div>
</section>
</main>
<script src="/about.js"></script>
<script>
const $ = (id) => document.getElementById(id);
let token = localStorage.getItem('noxely-token') || '';
@@ -86,16 +149,7 @@ $('token').value = token;
function saveToken() {
token = $('token').value.trim();
localStorage.setItem('noxely-token', token);
// Consultée depuis la machine du serveur, la page se remplit seule.
(async () => {
if (!token) {
try {
const { token: found } = await fetch('/api/bootstrap').then((r) => r.ok ? r.json() : {});
if (found) { token = found; $('token').value = found; localStorage.setItem('noxely-token', found); }
} catch {}
}
refreshAll();
})();
}
async function api(path, options = {}) {
@@ -108,13 +162,16 @@ async function api(path, options = {}) {
}
function show(name) {
for (const tab of ['search', 'watch', 'queue', 'library']) {
for (const tab of ['search', 'watch', 'queue', 'library', 'devices', 'settings', 'about']) {
$('p-' + tab).hidden = tab !== name;
$('t-' + tab).setAttribute('aria-selected', String(tab === name));
}
if (name === 'queue') loadQueue();
if (name === 'library') loadLibrary();
if (name === 'watch') loadWatches();
if (name === 'devices') loadDevices();
if (name === 'settings') loadServerInfo();
if (name === 'about') loadAbout();
}
async function refreshAll() {
@@ -123,6 +180,10 @@ async function refreshAll() {
$('status').textContent = health.name + ' ' + health.version;
$('status').className = 'pill ok';
} catch { $('status').textContent = 'injoignable'; $('status').className = 'pill warn'; }
if (!token) {
$('status').textContent = 'jeton requis → Réglages';
$('status').className = 'pill warn';
}
try {
const { catalogs } = await api('/catalogs');
$('catalog').innerHTML = Object.entries(catalogs)
@@ -245,6 +306,120 @@ async function sweep() {
loadWatches();
}
async function loadServerInfo() {
try {
const health = await fetch('/api/health').then((r) => r.json());
const { devices } = await api('/devices');
$('serverInfo').innerHTML = `
<div class="card"><div class="grow"><b>${esc(health.name)} ${esc(health.version)}</b>
<small>${devices.length} appareil(s) appairé(s)</small></div>
<span class="pill ok">en service</span></div>`;
} catch (e) {
$('serverInfo').innerHTML = `<div class="card"><div class="grow"><b>Jeton requis</b>
<small>${esc(String(e))}</small></div><span class="pill warn">non authentifié</span></div>`;
}
}
async function loadDevices() {
try {
const { devices } = await api('/devices');
$('devices').innerHTML = devices.length ? devices.map((d) => `
<div class="card"><div class="grow"><b>${esc(d.name)}</b><small>appairé le ${
new Date(d.created_at).toLocaleString('fr-FR')}${
d.last_seen ? ' · vu le ' + new Date(d.last_seen).toLocaleString('fr-FR')
: ' · jamais synchronisé'}</small></div>
<button class="ghost" onclick="revokeDevice('${d.id}','${esc(d.name)}')">Révoquer</button></div>`).join('')
: '<div class="empty">Aucun appareil appairé</div>';
} catch (e) { $('devices').innerHTML = `<div class="empty">${esc(String(e))}</div>`; }
}
async function revokeDevice(id, name) {
if (!confirm(`Révoquer « ${name} » ? Il ne pourra plus se synchroniser tant qu'il n'est pas réappairé.`)) return;
await api('/devices/' + id, { method: 'DELETE' });
loadDevices();
}
async function openPairing() {
try {
const { seconds } = await api('/pair/open', { method: 'POST' });
alert(`Appairage ouvert pour ${seconds} secondes.\n\n` +
`Sur le téléphone : Réglages ▸ Connexion ▸ Appairer.\n` +
`La fenêtre se referme dès qu'un appareil s'est servi.`);
} catch (e) { alert('Appairage impossible : ' + e); }
}
/* ── À propos ──────────────────────────────────────────────────────────────
Le contenu vient d'about.js, la version de /api/health et le journal du
CHANGELOG.md du dépôt : rien n'est recopié ici, donc rien ne peut diverger. */
let aboutTab = 'features';
const ABOUT_TABS = [
['features', '⭐ Fonctions'], ['guarantees', '🛡️ Garanties'],
['stack', '🧱 Technique'], ['log', '🕓 Journal'], ['author', '👤 Auteur'],
];
async function loadAbout() {
$('aboutName').textContent = ABOUT.name;
$('aboutTagline').textContent = ABOUT.tagline;
let version = '—', live = false;
try {
const health = await fetch('/api/health').then((r) => r.json());
version = health.version; live = true;
} catch {}
$('aboutBadges').innerHTML =
`<span class="pill accent">v${esc(version)}</span>` +
`<span class="pill">${esc(ABOUT.license)}</span>` +
`<span class="pill ${live ? 'ok' : 'warn'}">${live ? 'en service' : 'injoignable'}</span>` +
`<span class="pill ok">4 catalogues libres</span>`;
$('aboutTabs').innerHTML = ABOUT_TABS.map(([id, label]) =>
`<button onclick="showAbout('${id}')" aria-selected="${id === aboutTab}">${label}</button>`).join('');
renderAbout();
}
function showAbout(id) { aboutTab = id; loadAbout(); }
async function renderAbout() {
const body = $('aboutBody');
if (aboutTab === 'features') {
body.innerHTML = ABOUT.features.map(([title, text]) =>
`<div class="card"><b>${esc(title)}</b><small>${esc(text)}</small></div>`).join('');
} else if (aboutTab === 'guarantees') {
body.innerHTML = `<div class="card"><small>${esc(ABOUT.guaranteesIntro)}</small>
<ul style="margin-top:10px">${ABOUT.guarantees.map((g) => `<li>${esc(g)}</li>`).join('')}</ul></div>`;
} else if (aboutTab === 'stack') {
body.innerHTML = `<div class="card"><table class="tech">${ABOUT.stack.map(([layer, tools]) =>
`<tr><td>${esc(layer)}</td><td>${esc(tools)}</td></tr>`).join('')}</table></div>`;
} else if (aboutTab === 'author') {
body.innerHTML = `<div class="card" style="display:flex;align-items:center">
<div class="about-logo" style="width:52px;height:52px;border-radius:16px;font-size:24px">${
esc(ABOUT.author[0])}</div>
<div class="grow" style="margin-left:14px">
<b>${esc(ABOUT.author)}</b><small>${esc(ABOUT.authorRole)}</small>
<small style="user-select:text;display:block;margin-top:6px">${esc(ABOUT.repository)}</small>
</div></div>
<div class="card"><small>${esc(ABOUT.authorship)}</small>
<small style="display:block;margin-top:6px">${esc(ABOUT.copyright)} — ${esc(ABOUT.license)}</small></div>`;
} else {
body.innerHTML = '<div class="empty">Lecture du journal…</div>';
try {
const text = await fetch('/api/changelog').then((r) => r.text());
body.innerHTML = renderChangelog(text) || '<div class="empty">Journal vide</div>';
} catch { body.innerHTML = '<div class="empty">Journal indisponible</div>'; }
}
}
/* Le CHANGELOG n'emploie que des titres « ## », des puces « - » et du gras :
on n'interprète que cela, plutôt que d'embarquer un moteur Markdown. */
function renderChangelog(text) {
const bold = (s) => esc(s).replace(/\*\*(.+?)\*\*/g, '<b style="display:inline">$1</b>');
const entries = [];
for (const line of text.split('\n')) {
if (line.startsWith('## ')) entries.push({ title: line.slice(3).trim(), items: [] });
else if (line.startsWith('- ') && entries.length) entries[entries.length - 1].items.push(line.slice(2).trim());
}
return entries.map((e) => `<div class="card log"><h4>${bold(e.title)}</h4>
<ul>${e.items.map((i) => `<li>${bold(i)}</li>`).join('')}</ul></div>`).join('');
}
const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
+10
View File
@@ -1,3 +1,13 @@
# Noxely — collecte côté serveur, écoute hors connexion côté mobile.
# Copyright © 2026 Johnny (tuxgyver)
#
# Ce programme est un logiciel libre : vous pouvez le redistribuer et le modifier
# selon les termes de la GNU General Public License, version 3 ou ultérieure,
# telle que publiée par la Free Software Foundation.
#
# Il est distribué dans l'espoir d'être utile, mais SANS AUCUNE GARANTIE.
# Voir le fichier LICENSE à la racine du dépôt pour le texte complet.
"""Tâches de fond : file de téléchargement, guet radio et enregistrement.
C'est ici que se déplace la consommation qui pesait sur le téléphone : le serveur balaie les