weidenfeld-it
Anmeldungsdatum: 14. April 2024
Beiträge: Zähle...
|
Hallo liebe Ubuntu Gemeinde, Für ein privates Projekt (Radio Automation) suche ich nach einer Möglichkeit rekursiv alle MP3 Tags aus Dateien auszulesen und mit Semikolon getrennt in eine .csv zu speichern. Nach stundenlanger Google Suche habe ich mir gedacht ich versuch mal hier mein Glück. Mit dem Tool ID3V2 habe ich bisher gute Erfahrungen gemacht. Allerdings bekomme ich es nicht hin, mehrere Tag Felder mit ";" getrennt in eine .csv zu exportieren. Meine bisherige Herangehensweise war folgende: id3v2 -l *.mp3 | grep 'TIT2' | cut -d":" -f2 Das liefert mit beispielsweise alle Titel Tags in einem Rutsch. Wie könnte man nun eine Befehlskette bauen so das ich weitere Tag Felder getrennt durch Semikolon anhängen kann? Würde mich riesig freuen wenn mir jemand auf die Sprünge helfen könnte. Schon mal vorab riesen Dank an euch. Gruß - Karsten
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4658
Wohnort: Berlin
|
Da würde man besser jede MP3-Datei einzeln verarbeiten. cut funktioniert hier nur solange bis mal ein Doppelpunkt in einem Titel oder sonstigen Feldwert vorkommt. Und auch das Semikolon kann in Feldwerten vorkommen und damit die CSV-Datei ”kaputt” machen.
Ich würde da eine echte Programmiersprache mit entsprechenden Bibliotheken verwenden wo man sich die Daten nicht aus Text rauspopeln muss, und wo man saubere CSV-Dateien erstellen kann ohne Probleme zu bekommen wenn das Trennzeichen, Anführungszeichen, oder Zeilenumbrüche in den Daten/Zellen vorkommen. Bei dem Beispiel ist übrigens nichts „rekursiv“. Edit: Beispiel in Python mit der mutagen -Bibliothek und ohne Fehlerbehandlung:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | #!/usr/bin/env python3
import csv
from pathlib import Path
from mutagen.mp3 import MP3
def main():
with open("test.csv", "w", encoding="utf-8", newline="") as csv_file:
writer = csv.writer(csv_file, delimiter=";")
writer.writerow(["Album", "Track", "Title"])
for mp3 in map(MP3, Path(".").glob("*.mp3")):
writer.writerow([mp3["TALB"], mp3["TRCK"], mp3["TIT2"]])
if __name__ == "__main__":
main()
|
*Das* liesse sich tatsächlich rekursiv machen wenn man statt glob() einfach rglob() aufruft.
|
Balu62
Anmeldungsdatum: 22. Oktober 2007
Beiträge: 967
Wohnort: Bern / Schweiz
|
Das dürfte wohl so mit id3v2 nicht gehen. Einfacher ist es aber, gleich ein Programm zu nehmen, dass Dir alle gewünschten Tags in einer Zeile in ein CSV ausgeben kann. Dafür eignet sich die Commandline-Version von Mediainfo hervorragend. Was meines Wissens leider nicht in einem Rutsch geht, ist ein Semikolon-getrenntes CSV direkt zu erzeugen, da mediainfo das Semikolon intern für die Katergorien-Trennung verwendet. Ist aber auch kein Problem, Pipe-getrennte CVS's sind ja auch verwendbar (z.B. für LO Calc) oder wenn es unbedingt Semikolon-getrennt sein muss, dann jagst Du das CSV anschliessend noch durch sed. Hier mal ein Beispiel, für das rekursive auslesen mit ein paar Tags. Mediainfo ist sehr mächtig und unterschützt nebst allen offiziellen Tags auch nicht Standard-Tags. Das folgende Besipiel arbeitet mit einem Template-File, in dem Du alle Tags reinschreibst, die Du haben möchtest. Alternativ kannst Du die Tags auch direkt in die Commandozeile schreiben, wird dann aber IMO ziemlich unübersichtlich.
find -type f -name "*.mp3" -execdir mediainfo --Inform=file:///Pfad/zu/Template.txt *.mp3 > /Pfad/zu/Ausgabe.csv {} \; Die Template.txt sieht so aus
General;%FileName%|%Performer%|%Album%|%Track/Position%|%Title%|%OverallBitRate_Mode%|%OverallBitRate%|%Genre%|%Cover%|%FileSize%|%FileSize/String%|
Audio;%Duration/String%|%Duration%
und das ergibt dann folgendes Ausgabe-CSV (Auszug) - hier am Beispiel von Hörbüchern und Musik, aber das ist ja gehupft wie gesprungen 😉
123 - Lost in Fuseta 1 - 123v180|Gil Ribeiro|Lost in Fuseta. Ein Portugal-Krimi|123|Lost in Fuseta 1 - 123v180|CBR|128000|Audiobook|Yes|4711732|4.49 MiB|4 min 50 s|290926
104 - Einsam und kalt ist der Tod - 104v125|Lars Pettersson|Einsam und kalt ist der Tod|104|Einsam und kalt ist der Tod - 104v125|VBR|101133|Audiobook|Yes|2612916|2.49 MiB|3 min 24 s|204669
093 - Einsam und kalt ist der Tod - 093v125|Lars Pettersson|Einsam und kalt ist der Tod|093|Einsam und kalt ist der Tod - 093v125|VBR|99251|Audiobook|Yes|2511295|2.39 MiB|3 min 20 s|200359
12.Blueschild;Anthony Gomes;High Voltage Blues;12;Blueschild;CBR;320000;Blues Rock;Yes;8804819;8.40 MiB;3 min 33 s;213891
15.Let Me Get Up On It (2023 Remaster);Tom Waits;Bone Machine (2023 Remaster);15;Let Me Get Up On It (2023 Remaster);CBR;320000;Blues;Yes;2517355;2.40 MiB;55 s 615 ms;55615
Eine komplette Übersicht aller möglichen Tags erhälst Du mit mediainfo --Info-Parameters oder recht ausführlich, in Tabellenform auf dieser Webseite. Falls Du unbedingt Semikolon-Trennung benötigst, übergibst Du das CSV anschliessend einfach noch sed, da sieht dann so aus
find -type f -name "*.mp3" -execdir mediainfo --Inform=file:///Pfad/zu/Template.txt *.mp3 > /Pfad/zu/Ausgabe.csv {} \; && sed -i 's/|/;/g' Ausgabe.csv
und schon hast Du die selbe Ausgabe wie oben im Besipiel, nur halt mit Semikolon anstatt mit Pipes getrennt. Gruss, Balu
|
shiro
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1214
|
Hallo weidenfeld-it, Willkommen im Forum. Ich nutze für diese Aufgabe zwar "id3mtag", da man dort die Informationen per "-q" Qualifier sehr einfach ziehen kann. Aber mit "id3v2" geht es auch, nur ein wenig umständlicher z.B. mit awk:
#!/bin/bash
id3v2 -R *.mp3 |
awk -v HDR="TPE1,TALB,TRCK,TIT2" 'BEGIN {
nh=split(HDR,val,",")
for (i in val){ahead[i]=val[i]}
}
function p(val,n){
o=""
for (i=1; i<=n; i++){
o=o"\";\""val[i]
}
print substr(o,3)"\""
}
{
if ( $0 == "" ){
p(val,nh)
delete val
} else {
n=split($0,arr,": ")
for ( i in ahead ){
if ( ahead[i] == arr[1] ){
val[i]=arr[2];
}
}
}
}'
Im obigen Beispiel habe ich nicht alles in eine Zeile geschrieben, da sie so eventuell übersichtlicher ist. Mit der awk Variablen "HDR" beschreibt man, welche Informationen in welcher Reihenfolge man gelistet haben will. Solltest bei dem awk Script etwas für dich unklar sein, frag einfach. PS. Die gleiche Ausgabe würdest du mit id3mtag wie folgt bekommen:
id3 -q '"%a";"%l";"%n";"%t"' *.mp3
Wenn man rekursiv durch die Verzeichnisse gehen will, muss man noch den Qualifier "-R" beim Aufruf angeben. Wenn dich id3mtag interessiert, schau mal im Git nach → https://github.com/squell/id3
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4658
Wohnort: Berlin
|
@shiro Wie sieht das aus wenn ": " im Wert selbst vorkommt? Habe ich in meiner Musiksammlung mindestens bei Album- und Songtiteln. ID3-Tags sind für einen „ich behandel das mal alles irgendwie mit Textwerkzeugen“-Ansatz nicht wirklich geeignet, weil da so ziemlich alle Zeichen drin vorkommen können, man sich also nicht drauf verlassen kann, dass man ein einfaches Zeichen als sicheren Trenner nehmen kann der garantiert nicht in den Daten vorkommt. Und bei ;-getrennter CSV-Datei hat auch das Problem das ; in den Daten selbst vorkommen kann. Das Python-Programm um ein bisschen Fehlerbehandlung erweitert und es kann jetzt auch damit umgehen falls ein Tag nicht vorhanden ist, oder falls ein Tag mehr als einen Wert hat:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | #!/usr/bin/env python3
import csv
from pathlib import Path
from mutagen.mp3 import MP3
def get_text(mp3, key, default=None):
tag = mp3.get(key)
if tag is None:
return default
#
# Take the first value if there are multiple values, for instance because
# the file has idv1 *and* idv2 tags.
#
return tag.text[0] if isinstance(tag.text, list) else tag.text
def main():
with open("test.csv", "w", encoding="utf-8", newline="") as csv_file:
writer = csv.writer(csv_file, delimiter=";")
writer.writerow(["Artist", "Album", "Track", "Title", "File"])
for mp3_path in Path(".").rglob("*.mp3"):
if mp3_path.is_file():
row = [
get_text(MP3(mp3_path), key, "")
for key in ["TPE1", "TALB", "TRCK", "TIT2"]
]
row.append(str(mp3_path))
writer.writerow(row)
if __name__ == "__main__":
main()
|
|
shiro
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1214
|
Wie sieht das aus wenn ": " im Wert selbst vorkommt?
Ok, danke für den Hinweis. Dann ist in dem "awk" Script folgende Änderung vorzunehmen:
# statt
n=split($0,arr,": ")
# nun besser
n=split($0,arr,": ");arr[2]=substr($0,length(arr[1])+3);gsub("\"","\\\"",arr[2])
Jedes Element wird bereits in Tüddel(") eingeschlossen ausgegeben, damit eingestreute Semikola ";" oder Zeilenumbrüche nicht stören und bei Zahlen mit führender "0" (z.B. TrackNr) nicht die Oktalzahlen-Falle zuschlägt. Um unempfindlicher für eingestreute Tüddel im Text zu sein, werden diese per "gsub" auch noch escaped. Ich würde aber dennoch "id3mtag" (id3) bevorzugen (in Abwandlung der steten Aussage von Cato dem Älteren).
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4658
Wohnort: Berlin
|
@shiro: Wenn ich das richtig sehe wird aus " innerhalb eines Wertes \" gemacht? Das ist falsch. Bei CSV wird das Zeichen dann einfach wiederholt. Beispieltitel:
"Hooray For Santa Claus (Theme From ""Santa Claus Conquers The Martians"")"
"Theme From ""Masterburner"""
"Echo's On ""A"" Street"
|
shiro
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1214
|
Marc_BlackJack_Rintsch schrieb: @shiro: Wenn ich das richtig sehe wird aus " innerhalb eines Wertes \" gemacht? Das ist falsch. Bei CSV wird das Zeichen dann einfach wiederholt.
Hmm, sollte das stimmen, wäre der "gsub" Befehl zu ändern:
#statt \"
gsub("\"","\\\"",arr[2])
#nun ""
gsub("\"","\"\"",arr[2])
|
weidenfeld-it
(Themenstarter)
Anmeldungsdatum: 14. April 2024
Beiträge: 8
|
Hallo zusammen, vielen Dank für die Lösungsvorschläge, ich werde die alle mal durcharbeiten. Auf jeden Fall bin ich hier schon mal richtig ☺
Ich melde mich wieder. LG Karsten
|
CarstenHa
Anmeldungsdatum: 1. Mai 2020
Beiträge: 137
|
Balu62 schrieb:
... Das folgende Besipiel arbeitet mit einem Template-File, in dem Du alle Tags reinschreibst, die Du haben möchtest. ...
Oh, das mit dem Template-File hatte ich noch gar nicht auf dem Schirm. Super Tipp! Gruß Carsten
|
weidenfeld-it
(Themenstarter)
Anmeldungsdatum: 14. April 2024
Beiträge: 8
|
Hallo zusammen, alle Beispiele funktionieren und sind brauchbar. Ich habe mich jetzt zunächst für die Python Variante entschieden. Mir war nicht bewusst das man das mit Python so einfach realisieren kann, weil ich mit dieser Scriptsprache so gut wie keine Erfahrung habe. Vielen Dank an alle für die hilfreichen Antworten, so eine Gemeinschaft wie hier ist schon ne ganze Menge wert. LG Karsten
|
weidenfeld-it
(Themenstarter)
Anmeldungsdatum: 14. April 2024
Beiträge: 8
|
Hallo,
ist es auch möglich die Länge einer MP3 Datei (Spielzeit) in dem Python Script mit auszugeben, so das ich diese ebenfalls mit in der .csv habe? Ansonsten funktioniert das Script 1a und macht genau das was es soll, vielen Dank dafür. LG Karsten Marc_BlackJack_Rintsch schrieb: @shiro Wie sieht das aus wenn ": " im Wert selbst vorkommt? Habe ich in meiner Musiksammlung mindestens bei Album- und Songtiteln. ID3-Tags sind für einen „ich behandel das mal alles irgendwie mit Textwerkzeugen“-Ansatz nicht wirklich geeignet, weil da so ziemlich alle Zeichen drin vorkommen können, man sich also nicht drauf verlassen kann, dass man ein einfaches Zeichen als sicheren Trenner nehmen kann der garantiert nicht in den Daten vorkommt. Und bei ;-getrennter CSV-Datei hat auch das Problem das ; in den Daten selbst vorkommen kann. Das Python-Programm um ein bisschen Fehlerbehandlung erweitert und es kann jetzt auch damit umgehen falls ein Tag nicht vorhanden ist, oder falls ein Tag mehr als einen Wert hat:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | #!/usr/bin/env python3
import csv
from pathlib import Path
from mutagen.mp3 import MP3
def get_text(mp3, key, default=None):
tag = mp3.get(key)
if tag is None:
return default
#
# Take the first value if there are multiple values, for instance because
# the file has idv1 *and* idv2 tags.
#
return tag.text[0] if isinstance(tag.text, list) else tag.text
def main():
with open("test.csv", "w", encoding="utf-8", newline="") as csv_file:
writer = csv.writer(csv_file, delimiter=";")
writer.writerow(["Artist", "Album", "Track", "Title", "File"])
for mp3_path in Path(".").rglob("*.mp3"):
if mp3_path.is_file():
row = [
get_text(MP3(mp3_path), key, "")
for key in ["TPE1", "TALB", "TRCK", "TIT2"]
]
row.append(str(mp3_path))
writer.writerow(row)
if __name__ == "__main__":
main()
|
|
shiro
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1214
|
...die Länge einer MP3 Datei (Spielzeit) in dem Python Script mit auszugeben ...
Suchen hilft manchmal. Hier ein paar Beispiele: https://stackoverflow.com/questions/119404/time-length-of-an-mp3-file Da die Spielzeit kein mp3-Tag ist, kannst du das entweder selbst berechnen oder z.B. mit "mediainfo" oder "ffmpeg" auslesen:
mp3File="Musik.mp3"
ffmpeg -i "$mp3File" 2>&1 | sed -En 's/.*Duration: ([^.]+).*/\1/p'
mediainfo "$mp3File" | sed -En '/^Audio/,$ s/^Duration\s*: (.*)/\1/p'
|
weidenfeld-it
(Themenstarter)
Anmeldungsdatum: 14. April 2024
Beiträge: 8
|
Mit ffmpeg, mediainfo oder auch exiftool kann man es auf diese Art auslesen das weiß ich, doch das ist nicht sehr zuverlässig. Ich dachte man könnte es auch direkt über das Python Script lösen und die Spieldauer mit in die .csv schreiben. Leider kenne ich mich mit Python sehr wenig bis gar nicht aus. > Suchen hilft manchmal. Ich habe natürlich sehr viel Zeit in die Suche investiert bevor ich hier um Rat gefragt habe. LG - Karsten shiro schrieb: ...die Länge einer MP3 Datei (Spielzeit) in dem Python Script mit auszugeben ...
Suchen hilft manchmal. Hier ein paar Beispiele: https://stackoverflow.com/questions/119404/time-length-of-an-mp3-file Da die Spielzeit kein mp3-Tag ist, kannst du das entweder selbst berechnen oder z.B. mit "mediainfo" oder "ffmpeg" auslesen:
mp3File="Musik.mp3"
ffmpeg -i "$mp3File" 2>&1 | sed -En 's/.*Duration: ([^.]+).*/\1/p'
mediainfo "$mp3File" | sed -En '/^Audio/,$ s/^Duration\s*: (.*)/\1/p'
|
shiro
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1214
|
Suchen hilft manchmal.
Jetzt fühle ich mich nun ein wenig veralbert. Ich habe doch die URL angegeben. Hast du das nicht gelesen? Aber ok, wenn du gern ein "python" Script geliefert haben willst, statt es selbst zu schreiben, habe ich (wohl didaktisch unklug) es dir unten eingefügt:
#!/usr/bin/env python3
import csv
import datetime
from pathlib import Path
from mutagen.mp3 import MP3
def get_text(mp3, key, default=None):
tag = mp3.get(key)
if tag is None:
return default
#
# Take the first value if there are multiple values, for instance because
# the file has idv1 *and* idv2 tags.
#
return tag.text[0] if isinstance(tag.text, list) else tag.text
def main():
with open("test.csv", "w", encoding="utf-8", newline="") as csv_file:
writer = csv.writer(csv_file, delimiter=";")
writer.writerow(["Artist", "Album", "Track", "Title", "Dauer", "File"])
for mp3_path in Path(".").rglob("*.mp3"):
if mp3_path.is_file():
audio=MP3(mp3_path)
row = [
get_text(audio, key, "")
for key in ["TPE1", "TALB", "TRCK", "TIT2"]
]
row.append(str(datetime.timedelta(seconds=round(audio.info.length))))
row.append(str(mp3_path))
writer.writerow(row)
if __name__ == "__main__":
main()
Post Scriptum: Ich habe leider wegen der "python" Variante die "id3v2" und "id3" Variante vergessen beizufügen, was hiermit erfolgen soll. Zuerst die "id3v2" Variante:
#!/bin/bash
id3v2 -R *.mp3 |
awk -v HDR="TPE1,TALB,TRCK,TIT2,TLEN" 'BEGIN {
nh=split(HDR,val,",")
for (i in val){ahead[i]=val[i]}
}
function p(val,n){
o=""
for (i=1; i<=n; i++){
o=o"\";\""val[i]
}
print substr(o,3)"\""
}
{
if ( $0 == "" ){
p(val,nh)
delete val
} else {
n=split($0,arr,": ");arr[2]=substr($0,length(arr[1])+3);gsub("\"","\"\"",arr[2])
for ( i in ahead ){
if ( ahead[i] == arr[1] ){
if ( arr[1] == "TLEN" ) {
m=int(arr[2]/60000);s=arr[2]/1000-m*60;val[i]=sprintf("%02d:%02d",m,s);
} else {
val[i]=arr[2];
}
}
}
}
}'
und nun die "id3" Variante:
id3 -q '"%a";"%l";"%n";"%t";%{TLEN};"%f"' *.mp3 | awk -F";" -v OFS=";" '{m=int($5/60000);s=$5/1000-m*60;$5=sprintf("%02d:%02d",m,s);print}'
|