This commit is contained in:
Mac Call
2023-03-04 14:12:26 +01:00
commit e53fb1b54f
12 changed files with 616 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
test
data
*.txt
*.mp3
stream

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module maccall/stream
go 1.20

0
go.sum Normal file
View File

97
main.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"fmt"
"os"
"time"
// "stream_enregistre"
// "stream_titre"
)
const autocutDelai = 60 //s
// Le filename reçu se verra juste ajouter une extension ici (.mp3 ou .index)
func Enregistre(filename string) {
startTime := time.Now()
fmt.Println("Started... ", startTime)
fmt.Println(" Attendre quelques minutes et regarder dans le repertoire courant... ☺ ")
fmt.Println(" <ctrl>C pour arrêter")
doneChanRecord := make(chan bool)
errChanRecord := make(chan error)
doneChanTitle := make(chan bool)
errChanTitle := make(chan error)
filenameStream := filename + ".mp3"
filenameIndex := filename + ".index"
go Enregistre_stream(doneChanRecord, errChanRecord, filenameStream)
go Enregistre_titres(doneChanTitle, errChanTitle, filenameIndex)
// Exécuter la fonction toutes les 'timer' millisecondes
ticker := time.Tick(autocutDelai * time.Second)
for range ticker {
Cut(filenameStream, filenameIndex)
}
// Attendre que les goroutines aient terminé
successRecord := <-doneChanRecord
if !successRecord {
err := <-errChanRecord
fmt.Println("Erreur enregistrement :", err)
return
}
successTitle := <-doneChanTitle
if !successTitle {
err := <-errChanTitle
fmt.Println("Erreur titres :", err)
return
}
fmt.Println("Programme terminé")
}
func DecoupeAll() {
Cut("data/stream_recorded_2023-03-03T09:10:15.stripped.mp3", "data/index2023-03-03T09:10:14.txt")
Cut("data/stream_recorded_2023-03-02T10:12:03.stripped.mp3", "data/index2023-03-02T10:12:03.txt")
//Cut("data/stream_recorded_2023-02-27T22:55:39.stripped.mp3", "data/index2023-02-27T22:55:39.txt")
Cut("data/stream_recorded_2023-03-01T20:20:48.stripped.mp3", "data/index2023-03-01T20:20:48.txt")
Cut("data/stream_recorded_2023-02-28T18:33:07.stripped.mp3", "data/index2023-02-28T18:33:07.txt")
Cut("data/stream_recorded_2023-03-01T08:16:54.stripped.mp3", "data/index2023-03-01T08:16:53.txt")
Cut("data/stream_recorded_2023-03-04T11:59:43.stripped.mp3", "data/index2023-03-04T11:59:43.txt")
Cut("data/stream_recorded_2023-03-04T13:39:29.stripped.mp3", "data/index2023-03-04T13:39:28.txt")
Cut("data/rolanradio_2023-03-04T14:58:40.mp3", "data/index2023-03-04T14:58:40.txt")
Cut("data/rolanradio_2023-03-04T18:26:17.mp3", "data/index2023-03-04T18:26:16.txt")
Cut("data/rolanradio_2023-03-04T22:17:41.mp3", "data/index2023-03-04T22:17:40.txt")
//Cut("data/", "data/")
}
func main() {
if len(os.Args) != 1 && len(os.Args) != 4 {
fmt.Println("Usage: ")
fmt.Println(" stream : enregistre et decoupe 'en direct' ")
fmt.Println(" stream cut ficher_mp3 fichier_index : découpe un mp3 compte tenu de l'index")
os.Exit(1)
}
if len(os.Args) == 4 && os.Args[1] == "cut" {
mp3 := os.Args[2]
index := os.Args[3]
Cut(mp3, index)
} else {
strdate := time.Now().Format("2006-01-02T15:04:05")
filename := fmt.Sprintf("%s_%s", strdate, "enregistrement")
Enregistre(filename)
}
}

22
readme.md Normal file
View File

@@ -0,0 +1,22 @@
# Enregistrement de rolandradio.net et séparation des morceaux en fichiers distincts
## Compilation
`go test` pour les tests unitaires
`go build` pour compiler
`CGO_ENABLED=0 ; go build` pour compiler pour des systemes avec une version de libc < 2.32
## Utilisation
Copier l'executable `stream` compilé ci-dessus dans un répertoire vide au préalable, et se placer dans ce répertoire. Ce répertoire contiendra tous les fichier mp3 téléchargé, plus le gros fichier mp3 complet téléchargé ainsi qu'un fichier d'index (timestamp des morceaux avec les titres)
```
./stream
```
Attendre quelques minutes... Des fichiers mp3 vont apparaitre dans le répertoire courant

23
scripts/readme.md Normal file
View File

@@ -0,0 +1,23 @@
# petits scripts shell utilitaires
## xt pour strippe un "extraire" un bloc d'octets d'un binaire
Permet d'enlever le générique roland radio et les entetes mp3 ajouté a chaque stream au début
- premier parametre : la position du premier octet a récupérer
- deuxième parametre : le nombre d'octets a récupérer ( - pour aller jusqu'a la fin)
Dans l'exemple ci-dessous on utilise l'astuce `$((16#valeur_hexa))` pour convertir une valeur hexa en decimal quand la commande attend un decimal
```
./xt $((16#702a0)) 200000000 stream_recorded.mp3 > stream_recorded.stripped.mp3
./xt $((16#702a0)) - stream_recorded.mp3 > stream_recorded.stripped.mp3
```
## astuce bash : conversion d'une chaine de date en nanosecondes
```
date -d "2023-02-27T16:30:06.386" +%s%N
```

42
scripts/xt Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
## extrait d'un fichier binaire 'file' a partir du 'first' octet (compris, commence a compter a 1)
## en comptant 'count' octets
usage() {
echo "Usage: $0 first count file (chemin du fichier a traiter)"
echo " ou: $0 first count (avec l'utilisation de l'entrée standard)"
echo " Extrait les octets d'un fichier binaire 'file' ( ou de l'entrée standard) "
echo " a partir du 'first' octet (compris, commence a compter a 1)"
echo " en comptant 'count' octets"
echo " example : ./xt 4097 $((16#b0)) stream1.mp3 > stream.entete2.bin"
exit 1
}
# Vérifie que 2 ou 3 arguments ont été passés
if [ "$#" -ne 2 ] && [ "$#" -ne 3 ]; then
usage
exit 1
fi
# Par défaut, utilise l'entrée standard comme entrée
input="/dev/stdin"
# Récupère les arguments passés en paramètre
first="$1"
count="$2"
if [ "$#" -eq 3 ]; then
input="$3"
fi
if [ "$count" = "-" ] ; then
# Lance tail ans head car on veut jusq'ua bout du fichier
cat "$input"| tail -c+$first
else
# Lance tail et head pour récupérer les octest demandés
cat "$input"| tail -c+$first |head -c$count
fi

144
stream_cut.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
)
const delaiDebut float64 = 2.00 //secondes
const delaiFin float64 = 0.20 //secondes
/* transforme la chaine de temps en ms
* format d'entrée : une chaine au format hh:mm:ss.ms (pas d'obligation d'avoir des heurs ou minutes )
*/
func strTimeToS(strTime string) (float64, error) {
tabTime := strings.Split(strTime, ":")
var (
timeMultiplicateur int = 1
time float64
)
for i := len(tabTime) - 1; i >= 0; i-- {
timeVal, err := strconv.ParseFloat(tabTime[i], 64)
if err != nil {
return 0, errors.New("format invalide, nombre incorrect")
}
time += float64(timeMultiplicateur) * timeVal
timeMultiplicateur *= 60
}
return time, nil
}
func GetInfo(ligne string) (float64, string, string, error) {
//split la ligne en 2 sur le premier espace
tabInfo := strings.SplitN(ligne, " ", 2)
strTime, strTitre := tabInfo[0], tabInfo[1]
debTime, err := strTimeToS(strTime)
if err != nil {
return 0, "", "", err
}
//split tmpTitre en artiste + titre en séparant par " - "
split := strings.SplitN(strTitre, " - ", 2)
var artiste, titre string
if len(split) > 1 {
artiste, titre = split[0], split[1]
} else {
titre = split[0]
}
return debTime, artiste, titre, nil
}
// on suppose un flux a 128kb/s soit 128000/8 kB/s
func timerToBytes(timer float64) int64 {
return int64(128000. / 8. * timer)
}
func cutAndSave(mp3File string, timerDebut float64, timerFin float64, artiste string, titre string) {
octetDebut := timerToBytes(timerDebut)
octetsFin := timerToBytes(timerFin)
//fmt.Printf("je coupe le fichier %s de %v a %v le mp3 de %s s'apellant %s \n", mp3File, timerDebut, timerFin, artiste, titre)
// ouvrir le fichier
f, err := os.Open(mp3File)
if err != nil {
fmt.Fprintf(os.Stderr, "Erreur d'ouverture du fichier mp3 : %v", err)
panic(err)
}
defer f.Close()
// créer un tampon pour stocker les octets lus
tampon := make([]byte, octetsFin-octetDebut)
// lire les octets spécifiés à partir de la position spécifiée
n, err := f.ReadAt(tampon, int64(octetDebut))
if err != nil {
fmt.Fprintf(os.Stderr, "Erreur de lecture du fichier mp3 de %d a %d soit %d octets: %v", octetDebut, octetsFin, n, err)
panic(err)
}
// Écriture du tableau d'octets dans un fichier
artiste = strings.ReplaceAll(artiste, "/", "_")
if artiste == "" {
artiste = "Inconnu"
}
titre = strings.ReplaceAll(titre, "/", "_")
var outputFile string = fmt.Sprintf("%s.__.%s.mp3", artiste, titre)
err = os.WriteFile(outputFile, tampon, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Erreur d'écriture du fichier mp3 du morceau: %v", err)
panic(err)
}
}
func Cut(mp3File string, indexFile string) {
// Ouvrir le fichier index en lecture
fichier, err := os.Open(indexFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Erreur d'ouverture du fichier index : %v", err)
panic(err)
}
defer fichier.Close()
// Créer un scanner pour lire le fichier ligne par ligne
scanner := bufio.NewScanner(fichier)
// Parcourir chaque ligne du fichier
timerPrecedent := -1.
artistePrecedent := ""
titrePrecedent := ""
for scanner.Scan() {
// Faire quelque chose avec la ligne, par exemple :
ligne := scanner.Text()
timer, artiste, titre, err := GetInfo(ligne)
if err != nil {
fmt.Fprintf(os.Stderr, "Erreur de récupération des infos de la ligne %s : %v", ligne, err)
panic(err)
}
if timerPrecedent != -1. {
cutAndSave(mp3File, timerPrecedent-delaiDebut, timer+delaiFin, artistePrecedent, titrePrecedent)
timerPrecedent = timer
} else {
timerPrecedent = 0. + delaiDebut
}
artistePrecedent = artiste
titrePrecedent = titre
}
// Vérifier s'il y a eu une erreur lors de la lecture du fichier
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Erreur de lecture du fichier index : %v", err)
panic(err)
}
}

100
stream_cut_test.go Normal file
View File

@@ -0,0 +1,100 @@
package main
import (
"fmt"
"testing"
)
func TestStrTimeToS_erreur(t *testing.T) {
testName := "Test erreur"
inputs := []string{"", "az", "0x00", "00:0x0:00.0", "0::0.0"}
expected := 0.0
for i, input := range inputs {
result, err := strTimeToS(input)
if err == nil {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner une erreurmais ce n'est pas le cas. ", testName, i, input)
t.Errorf(msg, expected, result)
}
}
}
func TestStrTimeToS_0(t *testing.T) {
testName := "Test 0"
inputs := []string{"00", "0", "0.0", "0:0.0", "0:0", "0:0:0", "00:00:00", "0:00:0.0", "00:0:0", "00:00:00.0"}
expected := 0.0
for i, input := range inputs {
result, err := strTimeToS(input)
if err != nil || result != expected {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner '%%f' mais on a obtenu '%%f' avec erreur : '%v'", testName, i, input, err)
t.Errorf(msg, expected, result)
}
}
}
func TestStrTimeToS_10(t *testing.T) {
testName := "Test 10"
inputs := []string{"10", "10.0", "0:10.0", "0:10", "0:0:10", "00:00:10", "0:00:10.0", "00:0:10"}
expected := 10.0
for i, input := range inputs {
result, err := strTimeToS(input)
if err != nil || result != expected {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner '%%f' mais on a obtenu '%%f' avec erreur : '%v'", testName, i, input, err)
t.Errorf(msg, expected, result)
}
}
}
func TestStrTimeToS_100_42(t *testing.T) {
testName := "Test 100.42"
inputs := []string{"100.42", "1:40.420", "0:1:40.42", "0:1:40.42", "00:01:40.42"}
expected := 100.42
for i, input := range inputs {
result, err := strTimeToS(input)
if err != nil || result != expected {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner '%%f' mais on a obtenu '%%f' avec erreur : '%v'", testName, i, input, err)
t.Errorf(msg, expected, result)
}
}
}
func TestStrTimeToS_4000(t *testing.T) {
testName := "Test 4000"
inputs := []string{"0001:06:040", "1:6:40", "01:06:40.00", "1:5:100.0"}
expected := 4000.
for i, input := range inputs {
result, err := strTimeToS(input)
if err != nil || result != expected {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner '%%f' mais on a obtenu '%%f' avec erreur : '%v'", testName, i, input, err)
t.Errorf(msg, expected, result)
}
}
}
// -------------------------------------------------
func TestGetInfo_erreur(t *testing.T) {
testName := "Test getInfo"
inputs := []string{"00:01:aa.500 artiste - titre (etc) ☺"}
for i, input := range inputs {
_, _, _, err := GetInfo(input)
if err == nil {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner une erreur.", testName, i, input)
t.Errorf(msg)
}
}
}
func TestGetInfo(t *testing.T) {
testName := "Test getInfo"
inputs := []string{"00:00:00.500 artiste - titre (etc) ☺", "0:01:2.500 - titr - e (etc) ☺", "1:8:10.500 artiste titre (etc) ☺"}
expectedTime := []float64{0.5, 62.5, 4090.5}
expectedArtiste := []string{"artiste", "", ""}
expectedTitre := []string{"titre (etc) ☺", "titr - e (etc) ☺", "artiste titre (etc) ☺"}
for i, input := range inputs {
resultTime, resultArtiste, resultTitre, err := GetInfo(input)
if err != nil || resultTime != expectedTime[i] || resultArtiste != expectedArtiste[i] || resultTitre != expectedTitre[i] {
msg := fmt.Sprintf("%s , n° %d | la valeur '%s' aurait du retourner '%%f' '%%s' '%%s' mais on a obtenu '%%f' '%%s' '%%s' avec erreur : '%v'", testName, i, input, err)
t.Errorf(msg, resultTime, resultArtiste, resultTitre, expectedTime[i], expectedArtiste[i], expectedTitre[i])
}
}
}

77
stream_enregistre.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"fmt"
"io"
"net/http"
"os"
)
const stream_url = "http://streaming.rolandradio.net/rolandradio"
const stream_file = "./rolanradio_%s.mp3"
const sizeGeneriqueToStrip = 0x702A0 //octets
const bufferSize = 2048 //octets
func stopRecording() bool {
return false
}
// Enregistre le stream_url dans stream_file en ajoutant la date (%s)
// prend un channel de type booleen en entrée : true a la fin de la fonction OK, false si erreur
func Enregistre_stream(done chan bool, errChan chan error, filename string) {
// Obtenir la réponse HTTP depuis l'URL
resp, err := http.Get(stream_url)
if err != nil {
fmt.Println("Error fetching stream:", err)
done <- false
errChan <- err
}
defer resp.Body.Close()
// Créer un fichier local pour écrire les données du flux audio
file, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating file : %s", err)
done <- false
errChan <- err
}
defer file.Close()
//fmt.Println("Rejet des ", sizeGeneriqueToStrip, " premiers octets du stream")
fmt.Println("Recording...")
// met a la poubelle les premiers octets du stream (entete + generique roland radio)
_, err = io.CopyN(io.Discard, resp.Body, sizeGeneriqueToStrip)
if err != nil {
fmt.Fprintf(os.Stderr, "Erreur du strip de l'entête et générique du stream: %s", err)
done <- false
errChan <- err
}
buffer := make([]byte, bufferSize)
var totalBytes int64
for {
n, err := resp.Body.Read(buffer)
if err != nil && err != io.EOF {
fmt.Fprintf(os.Stderr, "Error lecture buffer stream : %s", err)
done <- false
errChan <- err
}
_, err = file.Write(buffer[:n])
if err != nil {
fmt.Fprintf(os.Stderr, "Error copying stream : %s", err)
done <- false
errChan <- err
}
totalBytes += int64(n)
if stopRecording() {
break
}
}
done <- true
}

12
stream_post_traitement.go Normal file
View File

@@ -0,0 +1,12 @@
package main
const delaiBeforeTrack = 350 //ms
const delaiAfterTrack = delaiBeforeTrack //ms
/*
Enleve le début du fichier enregistré qui contient un entete mp3 et le "générique roland radio"
Une fois ce strip effectuié, le fichier obtenu est lisible par vlc et les delai des titres sont calés (a environ 300ms pret)
*/
func StripGenerique() {
println(delaiBeforeTrack, delaiAfterTrack)
}

90
stream_titre.go Normal file
View File

@@ -0,0 +1,90 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const metadata_url = "http://streaming.rolandradio.net/status-json.xsl"
const index_file = "./index%s.txt"
const timer = 200 //ms
const dateFormat = "2006-01-02T15:04:05.000"
func get_actual_titre() string {
// Récupérer l'URL renvoyant du JSON
response, err := http.Get(metadata_url)
if err != nil {
fmt.Println("Erreur lors de la récupération de l'URL:", err)
return ""
}
defer response.Body.Close()
// Parser le JSON
var data map[string]interface{}
err = json.NewDecoder(response.Body).Decode(&data)
if err != nil {
fmt.Println("Erreur lors du parsing du JSON:", err)
return ""
}
//fmt.Printf("%v", data)
// Récupérer le titre du premier flux
streamTitle := data["icestats"].(map[string]interface{})["source"].([]interface{})[0].(map[string]interface{})["title"].(string)
return streamTitle
}
func getDelai(startTime time.Time) string {
elapsedTime := time.Since(startTime)
hours := elapsedTime / time.Hour
elapsedTime -= hours * time.Hour
minutes := elapsedTime / time.Minute
elapsedTime -= minutes * time.Minute
seconds := elapsedTime / time.Second
elapsedTime -= seconds * time.Second
ms := elapsedTime / time.Millisecond
retour := fmt.Sprintf("%02d:%02d:%02d.%d", hours, minutes, seconds, ms)
return retour
}
func Enregistre_titres(done chan bool, errChan chan error, outFilename string) {
// ouvre le fichier contenant l'index
file, err := os.Create(outFilename)
if err != nil {
errChan <- err
done <- false
}
defer file.Close()
titreActuel := ""
// Exécuter la fonction toutes les 'timer' millisecondes
ticker := time.Tick(timer * time.Millisecond)
startTime := time.Now()
for range ticker {
titre := get_actual_titre()
if titre != titreActuel {
titreActuel = titre
delai := getDelai(startTime)
info := fmt.Sprintf("%s %s\n", delai, titreActuel)
//fmt.Printf (info)
_, err = file.WriteString(info)
if err != nil {
errChan <- err
done <- false
}
}
}
done <- true
}