2 Commits
Author SHA1 Message Date
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
53 changed files with 842 additions and 7 deletions

No files matched your search

+42
View File
@@ -5,6 +5,48 @@ Toutes les évolutions notables de Noxely sont documentées ici.
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
et le versionnage [SemVer](https://semver.org/lang/fr/). et le versionnage [SemVer](https://semver.org/lang/fr/).
## [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 ## [2.0.1] — 2026-09-13
### Ajouté ### 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) [![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) [![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) [![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) [![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) [![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 | | 🔄 **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 | | 🎵 **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 | | 📚 **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 | | 📋 **Playlists** | Listes nommées, appui long pour y ajouter un titre |
+1 -1
View File
@@ -1 +1 @@
2.0.1 2.2.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 package fr.tuxgyver.noxely
import android.Manifest 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 package fr.tuxgyver.noxely
import android.app.Application 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 package fr.tuxgyver.noxely.library
import fr.tuxgyver.noxely.model.MediaEntry 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 package fr.tuxgyver.noxely.library
import android.content.Context 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 package fr.tuxgyver.noxely.library
import fr.tuxgyver.noxely.model.MediaEntry 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 package fr.tuxgyver.noxely.library
import android.content.ContentUris 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 package fr.tuxgyver.noxely.library
import android.content.Context 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 package fr.tuxgyver.noxely.model
import kotlinx.serialization.Serializable 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 package fr.tuxgyver.noxely.model
import kotlinx.serialization.Serializable 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 package fr.tuxgyver.noxely.offline
import android.content.Context 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 package fr.tuxgyver.noxely.offline
import android.app.Notification 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 package fr.tuxgyver.noxely.offline
import android.content.Context 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 package fr.tuxgyver.noxely.playback
import androidx.core.net.toUri 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 package fr.tuxgyver.noxely.playback
import android.app.PendingIntent 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 package fr.tuxgyver.noxely.playback
import android.content.ComponentName 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 package fr.tuxgyver.noxely.radio
import android.content.ContentValues 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 package fr.tuxgyver.noxely.sources
import java.net.HttpURLConnection 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 package fr.tuxgyver.noxely.sync
import android.content.Context 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 package fr.tuxgyver.noxely.sync
import android.content.Context import android.content.Context
import android.os.Build
import fr.tuxgyver.noxely.library.ImportedStore import fr.tuxgyver.noxely.library.ImportedStore
import fr.tuxgyver.noxely.model.MediaEntry import fr.tuxgyver.noxely.model.MediaEntry
import fr.tuxgyver.noxely.model.MediaKind 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) { suspend fun sync(): SyncResult = withContext(Dispatchers.IO) {
val config = settings.config.value val config = settings.config.value
if (!config.isConfigured) return@withContext SyncResult(0, 0, "Serveur non configuré") 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 package fr.tuxgyver.noxely.sync
import android.content.Context 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 package fr.tuxgyver.noxely.ui
import androidx.activity.compose.BackHandler 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 package fr.tuxgyver.noxely.ui.about
import androidx.compose.foundation.Image 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 package fr.tuxgyver.noxely.ui.about
import fr.tuxgyver.noxely.BuildConfig 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 package fr.tuxgyver.noxely.ui.about
import android.content.Context 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 package fr.tuxgyver.noxely.ui.components
import android.graphics.Bitmap 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 package fr.tuxgyver.noxely.ui.components
import java.util.Locale 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 package fr.tuxgyver.noxely.ui.components
import androidx.compose.animation.animateColorAsState 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 package fr.tuxgyver.noxely.ui.library
import androidx.activity.compose.rememberLauncherForActivityResult 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 package fr.tuxgyver.noxely.ui.offline
import androidx.compose.foundation.layout.Arrangement 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 package fr.tuxgyver.noxely.ui.player
import androidx.compose.foundation.basicMarquee 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 package fr.tuxgyver.noxely.ui.player
import androidx.annotation.OptIn 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 package fr.tuxgyver.noxely.ui.playlists
import androidx.compose.foundation.clickable 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 package fr.tuxgyver.noxely.ui.playlists
import androidx.compose.foundation.layout.Arrangement 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 package fr.tuxgyver.noxely.ui.playlists
import androidx.compose.foundation.clickable 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 package fr.tuxgyver.noxely.ui.server
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -127,7 +139,7 @@ fun ServerScreen(
OutlinedTextField( OutlinedTextField(
value = token, value = token,
onValueChange = { token = it; probe = null }, onValueChange = { token = it; probe = null },
label = { Text("Jeton") }, label = { Text("Jeton (rempli par l'appairage)") },
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -135,6 +147,26 @@ fun ServerScreen(
Spacer(Modifier.size(14.dp)) Spacer(Modifier.size(14.dp))
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Button( 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(), enabled = !busy && url.isNotBlank() && token.isNotBlank(),
onClick = { onClick = {
busy = true busy = true
@@ -152,7 +184,7 @@ fun ServerScreen(
busy = false busy = false
} }
}, },
) { Text("Tester et enregistrer") } ) { Text("Tester") }
Spacer(Modifier.width(10.dp)) 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 package fr.tuxgyver.noxely.ui.sources
import androidx.compose.foundation.layout.Column 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 package fr.tuxgyver.noxely.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme 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 package fr.tuxgyver.noxely.update
import android.content.Context 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 package fr.tuxgyver.noxely.update
import android.Manifest 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 package fr.tuxgyver.noxely.update
import android.content.Context 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`.""" """Point d'entrée : `python -m noxely_server`."""
from __future__ import annotations from __future__ import annotations
+87 -2
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. """API HTTP du serveur Noxely.
Le client Android ne consomme que `/api/health`, `/api/library` et `/api/media/{id}/file` : Le client Android ne consomme que `/api/health`, `/api/library` et `/api/media/{id}/file` :
@@ -7,6 +17,9 @@ mobile doit pouvoir se contenter d'un sondage bref, c'est là qu'est le gain de
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import ipaddress
import secrets
import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
@@ -19,6 +32,7 @@ from .storage import Storage
from .workers import Downloader, Watcher, safe_name from .workers import Downloader, Watcher, safe_name
VERSION = "1.0.0" VERSION = "1.0.0"
PAIRING_WINDOW_SECONDS = 120
WEB_DIR = Path(__file__).parent / "web" WEB_DIR = Path(__file__).parent / "web"
config = config_module.load() config = config_module.load()
@@ -28,9 +42,18 @@ watcher = Watcher(config, storage)
def authorised(x_noxely_token: str = Header(default="")) -> None: def authorised(x_noxely_token: str = Header(default="")) -> None:
"""Sans jeton valide, le serveur ne répond rien : il expose des fichiers.""" """Sans jeton valide, le serveur ne répond rien : il expose des fichiers.
if x_noxely_token != config.token:
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") raise HTTPException(status_code=401, detail="Jeton invalide")
storage.touch_device(device["id"])
@asynccontextmanager @asynccontextmanager
@@ -180,6 +203,68 @@ def queue_state() -> dict:
return {"items": [m.public() for m in pending if m.state != "ready"][:50]} 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") @app.get("/api/bootstrap")
def bootstrap(request: Request) -> dict: def bootstrap(request: Request) -> dict:
"""Livre le jeton — uniquement à une requête venue de la machine elle-même. """Livre le jeton — uniquement à une requête venue de la machine elle-même.
+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. """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 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. """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 : 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.""" """Recherche dans les catalogues libres de droits."""
from __future__ import annotations 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.""" """Annuaire des stations et lecture des métadonnées ICY."""
from __future__ import annotations 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. """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 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 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 ( CREATE TABLE IF NOT EXISTS watches (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
query TEXT NOT NULL, query TEXT NOT NULL,
@@ -140,6 +159,38 @@ class Storage:
self._db.commit() self._db.commit()
return item 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 ---------------------------------------------------------- # --- Guets ----------------------------------------------------------
def add_watch(self, query: str) -> str: def add_watch(self, query: str) -> str:
+47 -1
View File
@@ -1,4 +1,15 @@
<!doctype html> <!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"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -46,6 +57,7 @@
<span class="grow" style="flex:1"></span> <span class="grow" style="flex:1"></span>
<input type="password" id="token" placeholder="Jeton" style="width:190px"> <input type="password" id="token" placeholder="Jeton" style="width:190px">
<button class="ghost" onclick="saveToken()">Enregistrer</button> <button class="ghost" onclick="saveToken()">Enregistrer</button>
<button class="go" onclick="openPairing()">📱 Appairer</button>
</header> </header>
<main> <main>
<div class="tabs"> <div class="tabs">
@@ -53,6 +65,7 @@
<button onclick="show('watch')" aria-selected="false" id="t-watch">📡 Guets</button> <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('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('library')" aria-selected="false" id="t-library">📚 Bibliothèque</button>
<button onclick="show('devices')" aria-selected="false" id="t-devices">📱 Appareils</button>
</div> </div>
<section id="p-search"> <section id="p-search">
@@ -76,6 +89,10 @@
<section id="p-queue" hidden><div id="queue"></div></section> <section id="p-queue" hidden><div id="queue"></div></section>
<section id="p-library" hidden><div id="library"></div></section> <section id="p-library" hidden><div id="library"></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> </main>
<script> <script>
@@ -108,13 +125,14 @@ async function api(path, options = {}) {
} }
function show(name) { function show(name) {
for (const tab of ['search', 'watch', 'queue', 'library']) { for (const tab of ['search', 'watch', 'queue', 'library', 'devices']) {
$('p-' + tab).hidden = tab !== name; $('p-' + tab).hidden = tab !== name;
$('t-' + tab).setAttribute('aria-selected', String(tab === name)); $('t-' + tab).setAttribute('aria-selected', String(tab === name));
} }
if (name === 'queue') loadQueue(); if (name === 'queue') loadQueue();
if (name === 'library') loadLibrary(); if (name === 'library') loadLibrary();
if (name === 'watch') loadWatches(); if (name === 'watch') loadWatches();
if (name === 'devices') loadDevices();
} }
async function refreshAll() { async function refreshAll() {
@@ -245,6 +263,34 @@ async function sweep() {
loadWatches(); loadWatches();
} }
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); }
}
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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. """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 C'est ici que se déplace la consommation qui pesait sur le téléphone : le serveur balaie les