From e53fb1b54f5d9c47086917e3c9da83c72f463c8e Mon Sep 17 00:00:00 2001 From: Mac Call Date: Sat, 4 Mar 2023 14:12:26 +0100 Subject: [PATCH] initial --- .gitignore | 6 ++ go.mod | 3 + go.sum | 0 main.go | 97 +++++++++++++++++++++++++ readme.md | 22 ++++++ scripts/readme.md | 23 ++++++ scripts/xt | 42 +++++++++++ stream_cut.go | 144 ++++++++++++++++++++++++++++++++++++++ stream_cut_test.go | 100 ++++++++++++++++++++++++++ stream_enregistre.go | 77 ++++++++++++++++++++ stream_post_traitement.go | 12 ++++ stream_titre.go | 90 ++++++++++++++++++++++++ 12 files changed, 616 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readme.md create mode 100644 scripts/readme.md create mode 100755 scripts/xt create mode 100644 stream_cut.go create mode 100644 stream_cut_test.go create mode 100644 stream_enregistre.go create mode 100644 stream_post_traitement.go create mode 100644 stream_titre.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e6c4ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +test +data +*.txt +*.mp3 + +stream diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..86ed46c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module maccall/stream + +go 1.20 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go new file mode 100644 index 0000000..9eba46d --- /dev/null +++ b/main.go @@ -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(" 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) + } + +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a11a3ba --- /dev/null +++ b/readme.md @@ -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 diff --git a/scripts/readme.md b/scripts/readme.md new file mode 100644 index 0000000..73373b6 --- /dev/null +++ b/scripts/readme.md @@ -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 +``` diff --git a/scripts/xt b/scripts/xt new file mode 100755 index 0000000..d4c4a35 --- /dev/null +++ b/scripts/xt @@ -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 diff --git a/stream_cut.go b/stream_cut.go new file mode 100644 index 0000000..d04824c --- /dev/null +++ b/stream_cut.go @@ -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) + } +} diff --git a/stream_cut_test.go b/stream_cut_test.go new file mode 100644 index 0000000..e2eb6b8 --- /dev/null +++ b/stream_cut_test.go @@ -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]) + } + } +} diff --git a/stream_enregistre.go b/stream_enregistre.go new file mode 100644 index 0000000..a964f51 --- /dev/null +++ b/stream_enregistre.go @@ -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 +} diff --git a/stream_post_traitement.go b/stream_post_traitement.go new file mode 100644 index 0000000..e473547 --- /dev/null +++ b/stream_post_traitement.go @@ -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) +} diff --git a/stream_titre.go b/stream_titre.go new file mode 100644 index 0000000..fa73d59 --- /dev/null +++ b/stream_titre.go @@ -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 +}