ubuntuusers.de

Bash-Skript: jeder 2. Dateipfad wird am Anfang merkwürdig abgeschnitten

Status: Gelöst | Ubuntu-Version: Ubuntu 24.04 (Noble Numbat)
Antworten |

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Marc_BlackJack_Rintsch schrieb:

Kannst ja ein Ticket bei Python aufmachen ob die eventuell so etwas einbauen, mit find als ”Präzedenzfall”. Insbesondere das da auch nach dem Muster gefiltert wird, verhindert das man da einfach „Nimm doch chain()“ sagen kann.

Ware eine Idee. Mal gucken ob ich dafür Zeit finde, denn ich müsste mich dann da ja auch noch registrieren und in deren Workflow einarbeiten.

Bei find wird das ja über -mindepth und -maxdepth gesteuert (und ls kennt die Option -a). Weil damit mehr Möglichkeiten gegeben wären, wäre das vielleicht besser vorzuschlagen statt include_self.

Was soll denn [start_dir] + Path(start_dir).rglob("*") bedeuten? Sollte da a) das erste Argument in einen Iterator umgewandelt werden der mit dem zweiten Argument verkettet wird? Oder b) eine Liste mit den verketteten Inhalten der beiden iterierbaren Objekte erstellt werden?

Ja genau das kann der Interpreter nicht wissen. Deshalb hatte ich mit verschiedenen Klammerungen und Typumwandlungen rumprobiert, doch kam, zu keiner Lösung.

# b) Liste mit allem. paths = [start_dir, *Path(start_dir).rglob("*")] }}}

Ach der Stern ist der Trick um aus einem "generator" wieder eine Liste zu machen. Da bin ich _hier_ natürlich nicht drauf gekommen. Tut aber leider nicht, denn am Ende brauche ich ja eine Liste von Path-Objekten und nicht von Strings:

$ ./volume-detect.py "test"
  File "/mnt/Daten/Users/ich/Momentum/./volume-detect.py", line 39
    path for path in [start_path + *start_path.rglob("*")] if path.is_dir()
                                  ^
SyntaxError: invalid syntax

Edit: Ich sehe nicht was an dem + so toll sein soll das man auf chain() verzichtet. Mich stört da der Speicherverbrauch, ...

Gutes Argument. Allerdings könnte der Python-Optimierer evtl. merken, dass hier nicht wirklich eine neue Liste gebaut muss, sondern nur sequentiell iteriert.

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Marc_BlackJack_Rintsch schrieb:

@UlfZibis: Ich verstehe die Kritik am self nicht wirklich. Das ist ein Argument das eben erwartet wird, das ist nicht wirklich redundant.

Ja unter Betrachtung all der wirklich reichhaltigen Möglichkeiten in Python (danke für die fleißige Auflistung und Erklärung) ist das vielleicht so. Aber zumindest für den einfachsten Fall einer Klassendefinition fände ich es schön, wenn die Angabe von self sowohl in der Argumentliste als auch für Membervariablen optional wäre.

Da finde ich JavaScript deutlich unschöner. def __iter__(self): yield 42 vs. *[Symbol.iterator]() { yield 42; }.

Ja JavaSkript ist an vielen Stellen auch hässlich, aber ich bezog mich immer auf Java.

Und iter(a) + iter(b) finde ich jetzt auch nicht wirklich besser als chain(a, b).

Erspart halt den Import und das Lernen einer zusätzlichen Bibliothek.

VarArgs für sorted()? Da stehe ich gerade auf dem Schlauch? Meinst Du sorted(a, b, c) statt sorted(chain(a, b, c))?

Ja genau.

So etwas wie ein Modul kennt Java nicht.

Doch, kennt Java mittlerweile auch, ist aber ziemlich kompliziert.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4784

Wohnort: Berlin

@UlfZibis: Ich würde sagen itertools gehört zu den Modulen, ohne die man nicht wirklich auskommt. Das muss man sowieso lernen. Ebenso functools. contextlib enthält sehr nützliche Sachen für die with-Anweisung, also wenn man Ressourcen ordentlich hinter sich aufräumen will, ohne das alles über try/finally zu regeln, beziehungsweise wenn man wiederholten Code herausziehen möchte der sich nicht wiederholenden Code ”umschliesst”. Beim operator-Modul reicht es dessen Existenz zu kennen — es gibt dort für jeden Operator eine Funktion die das gleiche macht.

Das sind alles Module die sehr allgemeine Werkzeuge enthalten, die man sich sonst nachprogrammieren würde, oder ohne sie umständlicheren Code schreiben müsste.

Externe Bibliotheken die ich fast immer importiere sind attrs und more_itertools. attrs erleichtert das schreiben von Klassen mit weniger „boilerplate code“. Das self bei Methoden wird man nicht los, aber in der Regel die komplette __init__()-Methode und noch einiges mehr.

1
2
3
4
5
6
7
from attrs import field, frozen


@frozen
class Volume_:
    mean = field()
    max = field()

Da müsste man händisch ungefähr das hier schreiben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Volume:
    __slots__ = __match_args__ = ("mean", "max")

    def __init__(self, mean, max):
        object.__setattr__(self, "mean", mean)
        object.__setattr__(self, "max", max)

    def __setattr__(self, name, value):
        raise AttributeError(
            f"can't set attributes on {self.__class__.__name__} instances"
        )

    def __repr__(self):
        return (
            f"{self.__class__.__name__}(mean={self.mean!r}, max={self.max!r})"
        )

    def __eq__(self, other):
        return self.mean == other.mean and self.max == other.max

    def __hash__(self):
        return hash((self.mean, self.max))

Dann hat man fast das namedtuple() abzüglich dem Verhalten vom Tupel (Länge, Indexzugriff, iterierbar, …) und der totalen Ordnung. Für letzteres müsste man noch eine Methode implementieren, beispielsweise __lt__(self, other) für <= und die Klasse mit functools.total_ordering() dekorieren, damit die anderen Vergleichsmethoden nachgerüstet werden. Bei attrs würde es ausreichen statt @frozen mit @frozen(order=True) zu dekorieren.

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Nach freundlicher Unterstützung hier mal mein aktueller Stand des Skripts zur Audioauswertung. Eine Reihe von statistischen Werten werden erhoben. Vielleicht ist es ja für andere nützlich :

  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
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#!/usr/bin/env python3
import re
import subprocess
import sys
from pathlib import Path


def file(file_path):
    result = subprocess.check_output(
        ["file", "--brief", str(file_path)], text=True)
    return re.split(r",\s", (result[0:-1] if result[-1:] == '\n' else result))


def mediainfo(file_path):
    result = subprocess.check_output(
        ["mediainfo", str(file_path)], text=True)
    props = re.split(r"\n", result)
    general = dict()
    video = dict()
    audio = dict()
    image = dict()
    map = general
    for prop in props :
        key_value = re.split(r"\s*:\s", prop)
        if len(key_value) > 0 and key_value[0] == "" : continue
        if len(key_value) == 1 :
            match key_value[0] :
                case "General": map = general
                case "Video": map = video
                case "Audio": map = audio
                case "Image": map = image
                case "Image #1": map = image  # there may be more images
                case _: map = None; print("mediainfo: unknown section:", key_value[0])
            continue
        if map != None : map[key_value[0]] = key_value[1]
    return general, video, audio, image

    # print("[General]")
    # for key in general :
        # print(f"{key}={general[key]}")
    # print("[Video]")
    # for key in video :
        # print(f"{key}={video[key]}")
    # print("[Audio]")
    # for key in audio :
        # print(f"{key}={audio[key]}")
    # print("[Image]")
    # for key in image :
        # print(f"{key}={image[key]}")

#  ToDo: Use pymediainfo module instead


def detect_volumes(file_path):
    result = subprocess.run(
        ["ffmpeg", "-hide_banner", "-nostdin",
            *("-i", str(file_path)),
            *("-af", "volumedetect"), *("-f", "null"), "/dev/null"],
        capture_output=True,
        check=True
    )
    return (
        float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
        float(re.search(rb"max_volume: (\S*)", result.stderr)[1])
    )
#    volumes = { # use dictionary (= HashMap)
#        match[1]: float(match[2])
#        for match in re.finditer(rb"m(ean|ax)_volume: (\S*)", result.stderr)
#    }
#    return (volumes[b"ean"], volumes[b"ax"])


def main():
    if len(sys.argv) <= 3:
        print("Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]")
        sys.exit(1)
    start_path = Path(sys.argv[1])
    extension = sys.argv[2]
    threshold = float(sys.argv[3])
    verbose = "--verbose" in sys.argv[4:]
    num_albums = 0
    album_mean_sum = 0.0
    album_mean_min = 0.0
    album_mean_max = -128.0
    album_peak_sum = 0.0
    album_peak_min = 0.0
    album_peak_max = -128.
    albums_with_low_volume = []
    albums_with_mixed_mode = []
    albums_with_mixed_constant_rate = []
    bitrate_unknown = []
    files_with_errors = []
    
    csv = open("mediainfo_albums"+extension+".csv", "w")
    csv_verbose = open("mediainfo_files"+extension+".csv", "w")

    directory_paths = [start_path] + sorted(
        path for path in start_path.rglob("*") if path.is_dir()
    )
    for directory_path in directory_paths :
        num_files = 0
        mean_sum = 0.0
        mean_min = 0.0
        mean_max = -128.0
        peak_sum = 0.0
        peak_min = 0.0
        peak_max = -128.0
        rate_mode = "None"
        rate_sum = 0.0
        rate = 0.0
        rate_old = 0.0
        rate_mixed = False
#        file_paths = sorted(
#            directory_path.glob("*"+extension, case_sensitive=False)
#            # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
#        )
        file_paths = sorted(path for path in
            directory_path.glob("*"+extension, case_sensitive=False)
            # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
                if (extension == ".flac" or not path.with_suffix(".flac").exists())
        )
        for file_path in file_paths :
            if verbose :
                print(file_path)
            try:
                general, video, audio, image = mediainfo(file_path)
                if verbose :
                    print(f"  Format={audio["Format"]}, Format settings={audio.get("Format settings")},",
                        f"Channel(s)={audio.get("Channel(s)")},",
                        f"Bit rate mode={audio["Bit rate mode"]}, Bit rate={audio.get("Bit rate")},",
                        f"Sampling rate={audio["Sampling rate"]}, Bit depth={audio.get("Bit depth")}")
                FLAC = "FLAC" in audio["Format"]
                MP3 = ("MPEG Audio" in audio["Format"] and "Layer 3" in audio["Format profile"]) or extension == ".mp3"
                if FLAC or MP3 :
                    mean_volume, max_volume = detect_volumes(file_path)
                    mean_sum += mean_volume
                    mean_min = min(mean_min, mean_volume)
                    mean_max = max(mean_max, mean_volume)
                    peak_sum += max_volume
                    peak_min = min(peak_min, max_volume)
                    peak_max = max(peak_max, max_volume)
                    rate_mode = str(audio.get("Bit rate mode")) if num_files == 0 else\
                        rate_mode if str(audio.get("Bit rate mode")) == rate_mode else "Mixed"
                    if entry := audio.get("Bit rate") :
                        rate = float(re.sub(r"[^-+\d.]", "", entry))
                        rate_mixed = rate_mixed or (num_files > 0 and rate != rate_old)
                        rate_sum += (rate_old := rate)
                    else :
                        bitrate_unknown.append(file_path)
                    num_files += 1
                    if verbose :
                        tail = "      ("+str(audio.get("Bit rate"))+", "+str(audio.get("Bit rate mode"))+")"
                        print("  mean volume: %5.1f dB, peak volume: %5.1f dB%s"
                            %(mean_volume, max_volume, tail))
#                        print(f"  mean volume: {mean_volume: 5.1f} dB,"
#                            f" max volume: {max_volume: 5.1f} dB")

#                ToDo: Write values to csv files

                else :
                    print (file(file_path))
            except subprocess.CalledProcessError as error :
                files_with_errors.append((file_path, error.returncode))

        if file_paths :
            num_albums += 1
            print(len(file_paths), "files,", len(file_paths) - num_files,
                "errors in:", directory_path)
            if not extension in [".flac", ".mp3"] :
                if verbose : print(); continue
            if num_files :
                mean_average = mean_sum / num_files
                peak_average = peak_sum / num_files
                rate_average = rate_sum / num_files
                print("Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB      (%1.0f, %s)"
                    %(mean_average, mean_min, mean_max, peak_average, peak_min, peak_max, rate_average, rate_mode))
                album_mean_sum += mean_average
                album_mean_min = min(album_mean_min, mean_min)
                album_mean_max = max(album_mean_max, mean_max)
                album_peak_sum += peak_average
                album_peak_min = min(album_peak_min, peak_min)
                album_peak_max = max(album_peak_max, peak_max)
            if peak_max < threshold :
                albums_with_low_volume.append(
                    (directory_path, mean_sum / num_files, peak_min, peak_max))
            if rate_mode == "Mixed" : albums_with_mixed_mode.append((directory_path, rate_average))
            if rate_mixed and rate_mode == "Constant" :
                albums_with_mixed_constant_rate.append((directory_path, rate_average))
            if verbose : print()

#            ToDo: Write values to csv files


    print()
    print("Directories total:     ", len(directory_paths))
    print("Albums with "+extension+" files:", num_albums)
    if num_albums :
        album_mean_average = album_mean_sum / num_albums
        album_peak_average = album_peak_sum / num_albums
        print("All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
            %(album_mean_average, album_mean_min, album_mean_max, album_peak_average, album_peak_min, album_peak_max))
    if albums_with_low_volume :
        print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
        for album, average_mean, peak_min, peak_max in albums_with_low_volume:
            print("%s\n"
                "    volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
                %(album, average_mean, peak_min, peak_max))
    if albums_with_mixed_mode :
        print(f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):")
        for album, rate_average in albums_with_mixed_mode:
            print(album, "==>", rate_average, "kb/s")
    if albums_with_mixed_constant_rate :
        print(f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):")
        for album, rate_average in albums_with_mixed_constant_rate:
            print(album, "==>", rate_average, "kb/s")
    if bitrate_unknown :
        print(f"\n{extension} files without bit rate: ({len(bitrate_unknown)}):")
        for file_path in bitrate_unknown:
            print(file_path)
    if files_with_errors :
        print(f"\n{extension} files with errors: ({len(files_with_errors)}):")
        for file_path, returncode in files_with_errors:
            print(file_path, "==> return code:", returncode)

    if csv and not csv.closed : csv.close()
    if csv_verbose and not csv_verbose.closed : csv_verbose.close()


if __name__ == "__main__" :
    main()

Noch 2 weitere Verbesserungen stehen aus:

  1. Nutzung der pymediainfo-Bibliothek, statt selbst zu parsen.

  2. Abspeichern der Daten in CSV-Dateien, um per Libre-Office Calc weitere Berechnungen anstellen zu können.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4784

Wohnort: Berlin

@UlfZibis: Bei regulären Ausdrücken schaue ich immer ob es nicht auch ohne geht. file() zum Beispiel, da könnte die letzte Zeile auch einfach das hier sein: return [part.strip() for part in result.split(",")].

Und für Muster die einfach nur aus literalen Zeichen bestehen, braucht man das auch nicht: re.split(r"\n", text) ist doch einfach text.split("\n"). Beziehungsweise text.splitlines() wenn man sich nicht selber um den Fall kümmern will das ganz am Ende eine leere Zeichenkette übrigbleibt wenn der Text mit einem Zeilenendezeichen endet.

map ist der Name einer eingebauten, recht nützlichen Funktion, den sollte man nicht an andere Werte binden..

Das Ergebnis von re.split() und auch str.split() hat immer mindestens ein Element als Ergebnis, das muss man also nicht prüfen. Auch hier würde ich keinen regulären Ausdruck verwenden, und auch nicht split() sondern partition(). Denn bei split() müsste man das auf maximal eine Trennstelle begrenzen und dann immer noch mit den ”Sonderfällen” klar kommen. partition() hat immer drei Ergebnisse.

match/case wird hier ”missbraucht”. Das ist ein Mittel um Strukturen zu matchen und Namen Werte zuzuweisen. Wenn man da nicht wenigstens in einem case einen Namen sinnvoll zuweist, ist das so ähnlich wie mit regulären Ausdrücken ein Muster zu verwenden das nur aus literalen Zeichen besteht. Kanonen und Spatzen. Das wäre einfach if/elif/else oder man operiert hier gleich mit einem Wörterbuch das die Vergleichswerte auf die Datenstruktur abbildet die man da auswählt. Ich habe das mal auf eine tatsächliche Verwendung von match/case umgeschrieben.

Dateien immer mit with öffnen. Textdateien mit der Kodierung die man erwartet/haben will, und bei CSV-Dateien noch newlines="" als Argument, weil sich das csv-Modul da drum kümmert, dass egal welche Plattform, immer die gleichen verwendet werden.

Da steht noch mal hart kodiert ein Test mit ".flac" im Code obwohl man die Dateiendung ja als Kommandozeilenargument übergibt‽

Vom mediainfo-Ergebnis wird nur eins von vier Elementen tatsächlich verwendet‽

Die Hauptfunktion ist IMHO deutlich zu lang und macht zu viel. Und auch Sachen mit kopiertem und leicht angepasstem Code, wo man Funktionen und/oder Schleifen verwenden kann.

Wie immer ungetestet:

  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
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/usr/bin/env python3
import csv
import re
import subprocess
import sys
from pathlib import Path


def file(file_path):
    result = subprocess.check_output(
        ["file", "--brief", str(file_path)], text=True
    )
    return [part.strip() for part in result.split(",")]


def mediainfo(file_path):
    result = subprocess.check_output(["mediainfo", str(file_path)], text=True)
    section_names = ["General", "Video", "Audio", "Image"]
    results = {section_name: {} for section_name in section_names}
    results["Image #1"] = results["Image"]
    mapping = results["General"]
    for line in result.splitlines():
        match [part.strip() for part in line.partition(":")]:
            case "", "", "":
                pass  # Ignore empty lines.

            case section_name, "", "":
                mapping = results.get(section_name)
                if mapping is None:
                    print("mediainfo: unknown section:", section_name)

            case key, ":", value:
                if mapping is not None:
                    mapping[key] = value

            case _:
                assert False, f"should never happen: {line!r}"

    return [results[section_name] for section_name in section_names]


#  ToDo: Use pymediainfo module instead


def detect_volumes(file_path):
    result = subprocess.run(
        [
            "ffmpeg",
            "-hide_banner",
            "-nostdin",
            *("-i", str(file_path)),
            *("-af", "volumedetect"),
            *("-f", "null"),
            "/dev/null",
        ],
        capture_output=True,
        check=True,
    )
    return (
        float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
        float(re.search(rb"max_volume: (\S*)", result.stderr)[1]),
    )


def main():
    if len(sys.argv) <= 3:
        print(
            "Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]"
        )
        sys.exit(1)
    start_path = Path(sys.argv[1])
    extension = sys.argv[2]
    threshold = float(sys.argv[3])
    verbose = "--verbose" in sys.argv[4:]
    album_count = 0
    album_mean_sum = 0.0
    album_mean_min = 0.0
    album_mean_max = -128.0
    album_peak_sum = 0.0
    album_peak_min = 0.0
    album_peak_max = -128.0
    albums_with_low_volume = []
    albums_with_mixed_mode = []
    albums_with_mixed_constant_rate = []
    bitrate_unknown = []
    files_with_errors = []
    with (
        open(
            f"mediainfo_albums{extension}.csv",
            "w",
            encoding="utf-8",
            newline="",
        ) as csv_file,
        open(
            f"mediainfo_files{extension}.csv",
            "w",
            encoding="utf-8",
            newline="",
        ) as verbose_csv_file,
    ):
        csv_writer = csv.writer(csv_file)
        verbose_csv_writer = csv.writer(verbose_csv_file)

        directory_paths = [start_path] + sorted(
            path for path in start_path.rglob("*") if path.is_dir()
        )
        for directory_path in directory_paths:
            file_count = 0
            mean_sum = 0.0
            mean_min = 0.0
            mean_max = -128.0
            peak_sum = 0.0
            peak_min = 0.0
            peak_max = -128.0
            rate_mode = "None"
            rate_sum = 0.0
            rate = 0.0
            old_rate = 0.0
            rate_mixed = False
            file_paths = sorted(
                path
                for path in directory_path.glob(
                    "*" + extension, case_sensitive=False
                )
                #
                # TODO Wozu ist das hier gut?
                #
                if (
                    extension == ".flac"
                    or not path.with_suffix(".flac").exists()
                )
            )
            for file_path in file_paths:
                if verbose:
                    print(file_path)

                try:
                    _general, _video, audio, _image = mediainfo(file_path)
                    if verbose:
                        print(
                            " ",
                            ", ".join(
                                f"{key}={audio.get(key)}"
                                for key in [
                                    "Format",
                                    "Format settings",
                                    "Channel(s)",
                                    "Bit rate mode",
                                    "Bit rate",
                                    "Sampling rate",
                                    "Bit depth",
                                ]
                            ),
                        )
                    is_flac = "FLAC" in audio["Format"]
                    is_mp3 = (
                        "MPEG Audio" in audio["Format"]
                        and "Layer 3" in audio["Format profile"]
                    ) or extension == ".mp3"
                    if is_flac or is_mp3:
                        mean_volume, max_volume = detect_volumes(file_path)
                        mean_sum += mean_volume
                        mean_min = min(mean_min, mean_volume)
                        mean_max = max(mean_max, mean_volume)
                        peak_sum += max_volume
                        peak_min = min(peak_min, max_volume)
                        peak_max = max(peak_max, max_volume)
                        rate_mode = (
                            audio.get("Bit rate mode")
                            if file_count == 0
                            else (
                                rate_mode
                                if audio.get("Bit rate mode") == rate_mode
                                else "Mixed"
                            )
                        )
                        if entry := audio.get("Bit rate"):
                            rate = float(re.sub(r"[^-+\d.]", "", entry))
                            rate_mixed = rate_mixed or (
                                file_count > 0 and rate != old_rate
                            )
                            rate_sum += (old_rate := rate)
                        else:
                            bitrate_unknown.append(file_path)
                        file_count += 1
                        if verbose:
                            tail = (
                                f"      ({audio.get('Bit rate')}),"
                                f" {audio.get('Bit rate mode')})"
                            )
                            print(
                                "  mean volume: %5.1f dB, peak volume: %5.1f dB%s"
                                % (mean_volume, max_volume, tail)
                            )
                        # ToDo: Write values to csv files
                    else:
                        print(file(file_path))

                except subprocess.CalledProcessError as error:
                    files_with_errors.append((file_path, error.returncode))

            if file_paths:
                album_count += 1
                print(
                    len(file_paths),
                    "files,",
                    len(file_paths) - file_count,
                    "errors in:",
                    directory_path,
                )
                if extension not in [".flac", ".mp3"] and verbose:
                    print()
                else:
                    if file_count:
                        mean_average = mean_sum / file_count
                        peak_average = peak_sum / file_count
                        rate_average = rate_sum / file_count
                        print(
                            "Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB      (%1.0f, %s)"
                            % (
                                mean_average,
                                mean_min,
                                mean_max,
                                peak_average,
                                peak_min,
                                peak_max,
                                rate_average,
                                rate_mode,
                            )
                        )
                        album_mean_sum += mean_average
                        album_mean_min = min(album_mean_min, mean_min)
                        album_mean_max = max(album_mean_max, mean_max)
                        album_peak_sum += peak_average
                        album_peak_min = min(album_peak_min, peak_min)
                        album_peak_max = max(album_peak_max, peak_max)

                    if peak_max < threshold:
                        albums_with_low_volume.append(
                            (
                                directory_path,
                                mean_sum / file_count,
                                peak_min,
                                peak_max,
                            )
                        )
                    if rate_mode == "Mixed":
                        albums_with_mixed_mode.append(
                            (directory_path, rate_average)
                        )
                    if rate_mixed and rate_mode == "Constant":
                        albums_with_mixed_constant_rate.append(
                            (directory_path, rate_average)
                        )
                    if verbose:
                        print()

        #            ToDo: Write values to csv files

        print()
        print("Directories total:     ", len(directory_paths))
        print("Albums with", extension, "files:", album_count)
        if album_count:
            album_mean_average = album_mean_sum / album_count
            album_peak_average = album_peak_sum / album_count
            print(
                "All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
                % (
                    album_mean_average,
                    album_mean_min,
                    album_mean_max,
                    album_peak_average,
                    album_peak_min,
                    album_peak_max,
                )
            )

        if albums_with_low_volume:
            print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
            for (
                album,
                average_mean,
                peak_min,
                peak_max,
            ) in albums_with_low_volume:
                print(
                    "%s\n"
                    "    volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
                    % (album, average_mean, peak_min, peak_max)
                )
        if albums_with_mixed_mode:
            print(
                f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):"
            )
            for album, rate_average in albums_with_mixed_mode:
                print(album, "==>", rate_average, "kb/s")

        if albums_with_mixed_constant_rate:
            print(
                f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):"
            )
            for album, rate_average in albums_with_mixed_constant_rate:
                print(album, "==>", rate_average, "kb/s")

        if bitrate_unknown:
            print(
                f"\n{extension} files without bit rate: ({len(bitrate_unknown)}):"
            )
            for file_path in bitrate_unknown:
                print(file_path)

        if files_with_errors:
            print(
                f"\n{extension} files with errors: ({len(files_with_errors)}):"
            )
            for file_path, returncode in files_with_errors:
                print(file_path, "==> return code:", returncode)


if __name__ == "__main__":
    main()

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Ich bin beeindruckt wie gut Du die Sprachmittel kennst und damit jonglierst.
Ich kenne mich halt noch nicht so aus und kenne auch all die zur Verfügung stehenden Bibliotheken nicht.

Marc_BlackJack_Rintsch schrieb:

@UlfZibis: Bei regulären Ausdrücken schaue ich immer ob es nicht auch ohne geht. file() zum Beispiel, da könnte die letzte Zeile auch einfach das hier sein: return [part.strip() for part in result.split(",")].

Gute Idee, auch wenn das dann zu 3-fachem (Teil-)Scannen jeder Zeile führt.

map ist der Name einer eingebauten, recht nützlichen Funktion, den sollte man nicht an andere Werte binden..

Hab' ich mir deshalb auch nur innerhalb der Funktion erlaubt.

Dateien immer mit with öffnen.

Das dachte ich mir, dass das kommt. Da ich es aber unschön fand, dafür eine weitere Einrückung für fast den gesammten Code spendieren zu müssen, dachte ich, für mein Einmal-Programm reicht's auch ohne with.

Textdateien mit der Kodierung die man erwartet/haben will, und bei CSV-Dateien noch newlines="" als Argument, weil sich das csv-Modul da drum kümmert, dass egal welche Plattform, immer die gleichen verwendet werden.

Oh das ist sehr interessant, dass Du mir da eine Vorlage lieferst, denn bisher hab' ich die CSV selbst gebaut.
Spannend ist aber auch, dass man das Datei-Objekt auch außerhalb des with-Blocks verwenden kann. In Java wird eine Datei geschlossen, sobald so ein Block verlassen wird, was ich auch logisch finde, weil damit auch bestimmt ist, wann die Datei geschlossen wird. Hier muss die Datei ja bis zum Ende der Laufzeit des Programm offen bleiben, weil sie später noch benutzt werden könnte. Da Dateien aber wohl sicher auch in Python bei Programm-Ende alle automatisch geschlossen werden, verstehe ich dann den Sinn von with nun nicht ganz.

Da steht noch mal hart kodiert ein Test mit ".flac" im Code obwohl man die Dateiendung ja als Kommandozeilenargument übergibt‽

Ja in wenigen FLAC-Alben hab' ich vom Rumprobieren her schon komprimierte MP3-Duplikate drin. Die will ich beim Untersuchen bzgl. MP3 aussparen. Kann in der Endfassung dann weg.

Vom mediainfo-Ergebnis wird nur eins von vier Elementen tatsächlich verwendet‽

Bisher ja. Meinen selbstgebauten Scanner habe ich aber schon wieder rausgeschmissen, da ich ja das pymediainfo-Modul entdeckt habe.

Die Hauptfunktion ist IMHO deutlich zu lang und macht zu viel. Und auch Sachen mit kopiertem und leicht angepasstem Code, wo man Funktionen und/oder Schleifen verwenden kann.

Ja da ließe sich so manches verschönern.

Wie immer ungetestet:

Die Garantie für Fehlerfreiheit 😉 , danke noch mal sehr.

Dann hier mal mein jetziger Stand (Argumente in die Senkrechte zu verteilen mag ich nicht so. Auf 19 cm Bildschirmhöhe muss ich dann zu viel scrollen. Aber in der Breite ist auf dem 16:9-Sehschlitz (wer so'n Mist durchgesetzt?) genug Platz) (noch nicht alles von Dir eingebaut):

  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
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env python3
import re
import subprocess
import sys
from pathlib import Path
from pprint import pprint
from pymediainfo import MediaInfo


def file(file_path):
    result = subprocess.check_output(
        ["file", "--brief", str(file_path)], text=True)
    return [part.strip() for part in result.split(",")]


def detect_volumes(file_path):
    result = subprocess.run(
        ["ffmpeg", "-hide_banner", "-nostdin",
            *("-i", str(file_path)),
            *("-af", "volumedetect"), *("-f", "null"), "/dev/null"],
        capture_output=True,
        check=True
    )
    return (
        float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
        float(re.search(rb"max_volume: (\S*)", result.stderr)[1])
    )
#    volumes = { # use dictionary (= HashMap)
#        match[1]: float(match[2])
#        for match in re.finditer(rb"m(ean|ax)_volume: (\S*)", result.stderr)
#    }
#    return (volumes[b"ean"], volumes[b"ax"])


def main():
    if len(sys.argv) <= 3:
        print("Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]")
        sys.exit(1)
    start_path = Path(sys.argv[1])
    extension = sys.argv[2]
    threshold = float(sys.argv[3])
    verbose = "--verbose" in sys.argv[4:]
    num_albums = 0
    album_mean_sum = 0.0
    album_mean_min = 0.0
    album_mean_max = -128.0
    album_peak_sum = 0.0
    album_peak_min = 0.0
    album_peak_max = -128.
    albums_with_low_volume = []
    albums_with_mixed_mode = []
    albums_with_mixed_constant_rate = []
    bitrate_unknown = []
    files_with_errors = []
    
    csv = open("mediainfo_albums"+extension+".csv", "w")
    csv_verbose = open("mediainfo_files"+extension+".csv", "w")
    csv.write("No;Path;Size;Mode;Bit rate;\n")
    csv_verbose.write("Album;No;Path;Size;Mode;Bit rate;\n")

    directory_paths = [start_path] + sorted(
        path for path in start_path.rglob("*") if path.is_dir()
    )
    for directory_path in directory_paths :
        num_files = 0
        mean_sum = 0.0
        mean_min = 0.0
        mean_max = -128.0
        peak_sum = 0.0
        peak_min = 0.0
        peak_max = -128.0
        rate_mode = "None"
        rate_sum = 0.0
        rate = 0.0
        rate_old = 0.0
        rate_mixed = False
#        file_paths = sorted(
#            directory_path.glob("*"+extension, case_sensitive=False)
#            # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
#        )
        if not (file_paths := sorted(path for path in
            directory_path.glob("*"+extension, case_sensitive=False)
            # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
                if (extension == ".flac" or not path.with_suffix(".flac").exists())
        )) : continue

        num_albums += 1
        for file_path in file_paths :
            if verbose :
                print(file_path)
            try:
                media_info = MediaInfo.parse(file_path)
                # general = media_info.general_tracks[0]
                audio = media_info.audio_tracks[0]
                # print(f" audio size: {audio.stream_size} bytes, file size: {general.file_size} bytes.")
                # pprint(audio.to_data())
                if verbose :
                    print(f"  Format={audio.format}, Format settings={audio.format_settings},",
                        f"Format profile={audio.format_profile}, Channel(s)={audio.channel_s},",
                        f"Bit rate mode={audio.bit_rate_mode}, Bit rate={audio.bit_rate},",
                        f"Sampling rate={audio.sampling_rate}, Bit depth={audio.bit_depth}")
                FLAC = "FLAC" in audio.format
                MP3 = ("MPEG Audio" in audio.format and "Layer 3" in audio.format_profile) or extension == ".mp3"
                if FLAC or MP3 :
                    mean_volume, max_volume = detect_volumes(file_path)
                    mean_sum += mean_volume
                    mean_min = min(mean_min, mean_volume)
                    mean_max = max(mean_max, mean_volume)
                    peak_sum += max_volume
                    peak_min = min(peak_min, max_volume)
                    peak_max = max(peak_max, max_volume)
                    rate_mode = audio.bit_rate_mode if num_files == 0 else\
                        rate_mode if audio.bit_rate_mode == rate_mode else "Mixed"
                    if entry := audio.bit_rate :
                        rate = float(entry)
                        rate_mixed = rate_mixed or (num_files > 0 and rate != rate_old)
                        rate_sum += (rate_old := rate)
                    else :
                        bitrate_unknown.append(file_path)
                    num_files += 1
                    if verbose :
                        tail = "      ("+str(audio.bit_rate)+", "+str(audio.bit_rate_mode)+")"
                        print("  mean volume: %5.1f dB, peak volume: %5.1f dB%s"
                            %(mean_volume, max_volume, tail))
#                        print(f"  mean volume: {mean_volume: 5.1f} dB,"
#                            f" max volume: {max_volume: 5.1f} dB")

#                    ToDo: Write values to csv files
                    csv_verbose.write(f'{num_albums};{num_files};"{file_path.name}";{audio.stream_size};{audio.bit_rate_mode};{audio.bit_rate};;\n')

                else :
                    print (file(file_path))
            except subprocess.CalledProcessError as error :
                files_with_errors.append((file_path, error.returncode))

        print(len(file_paths), "files,", len(file_paths) - num_files,
            "errors in:", directory_path)
        if not extension in [".flac", ".mp3"] :
            if verbose : print(); continue
        if num_files :
            mean_average = mean_sum / num_files
            peak_average = peak_sum / num_files
            rate_average = rate_sum / num_files
            print("Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB      (%1.0f, %s)"
                %(mean_average, mean_min, mean_max, peak_average, peak_min, peak_max, rate_average, rate_mode))
            album_mean_sum += mean_average
            album_mean_min = min(album_mean_min, mean_min)
            album_mean_max = max(album_mean_max, mean_max)
            album_peak_sum += peak_average
            album_peak_min = min(album_peak_min, peak_min)
            album_peak_max = max(album_peak_max, peak_max)
        if peak_max < threshold :
            albums_with_low_volume.append(
                (directory_path, mean_sum / num_files, peak_min, peak_max))
        if rate_mode == "Mixed" : albums_with_mixed_mode.append((directory_path, rate_average))
        if rate_mixed and rate_mode == "CBR" :
            albums_with_mixed_constant_rate.append((directory_path, rate_average))
        if verbose : print()

        csv.write(f'{num_albums};"{directory_path}";{num_files};{rate_mode};{int(rate_average)};;\n')

#        ToDo: Write values to csv files


    print()
    print("Directories total:     ", len(directory_paths))
    print("Albums with "+extension+" files:", num_albums)
    if num_albums :
        album_mean_average = album_mean_sum / num_albums
        album_peak_average = album_peak_sum / num_albums
        print("All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
            %(album_mean_average, album_mean_min, album_mean_max, album_peak_average, album_peak_min, album_peak_max))
    if albums_with_low_volume :
        print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
        for album, average_mean, peak_min, peak_max in albums_with_low_volume:
            print("%s\n"
                "    volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
                %(album, average_mean, peak_min, peak_max))
    if albums_with_mixed_mode :
        print(f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):")
        for album, rate_average in albums_with_mixed_mode:
            print(f"{album} ==> {rate_average/1000:3.1f} kb/s")
    if albums_with_mixed_constant_rate :
        print(f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):")
        for album, rate_average in albums_with_mixed_constant_rate:
            print(f"{album} ==> {rate_average/1000:3.1f} kb/s")
    if bitrate_unknown :
        print(f"\n{extension} files without bit rate: ({len(bitrate_unknown)}):")
        for file_path in bitrate_unknown:
            print(file_path)
    if files_with_errors :
        print(f"\n{extension} files with errors: ({len(files_with_errors)}):")
        for file_path, returncode in files_with_errors:
            print(file_path, "==> return code:", returncode)

    if csv and not csv.closed : csv.close()
    if csv_verbose and not csv_verbose.closed : csv_verbose.close()


if __name__ == "__main__" :
    main()

Gibt's in Python eigentlich auch for-Schleifen, worin der Index direkt definiert und behandelt wird? Also sowas ähnliches wie in C und Java:

1
2
3
for (int i=0; i<MAX; i++) {
    // i kann hier benutzt werden
}

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4784

Wohnort: Berlin

Die zusätzliche Einrückung ist IMHO hauptsächlich ein ”Problem” weil die Funktion sowieso schon zu viel Code und zu viele Ebenen enthält.

Man kann das Dateiobjekt nicht wirklich ausserhalb des with-Blocks verwenden. Das ist ja der Sinn, dass die Datei geschlossen wird, egal wann und warum der Block verlassen wird.

Dass das Betriebssystem am Ende eines Prozesses auch dessen Dateien schliesst, sollte einen nicht dazu verleiten selber ordentlich mit Ressourcen umzugehen. Da machen auch ”Einmalskripte” keinen Unterschied. Wenn man das immer sauber macht, kann man es nicht vergessen wenn es wichtig(er) ist. Und ein Einmalskript ist das ja schon nicht mehr, *die* stellt man nicht anderen zur Verfügung und löscht die nach Benutzung. Meine Erfahrung: Immer wenn man das nicht löscht, benutzt man das auch irgendwann wieder. Meistens nach einer längeren Zeit, weshalb das so geschrieben werden sollte, wie ein normales Programm, das man nach der Zeit dann auch selber wieder verstehen möchte, und das auch mit anderen Versionen der verwendeten Bibliotheken und externen Programme noch arbeitet und merkt wenn sich da was geändert hat. Weswegen ich persönlich halt gerne parsen von Ausgaben so schreibe, dass das auffällt wenn das Format plötzlich ein bisschen anders ist und die Zahl dann vielleicht nicht mehr kb/s sondern mb/s ist, oder andere Tausendertrenner verwendet werden, usw.

Da habe ich mir jetzt umsonst Gedanken um die mediainfo-Ausgabe gemacht. 😎 Hatte mir das mal angeschaut und da hätte ich statt der Ausgabe für menschliche Leser mit den Tausendertrennern, die ja vielleicht auch noch von den LOCALE-Einstellungen abhängen können, eine XML-Ausgabe angefordert. Beispiel:

 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
35
<?xml version="1.0" encoding="UTF-8"?>
<MediaInfo xmlns="https://mediaarea.net/mediainfo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://mediaarea.net/mediainfo https://mediaarea.net/mediainfo/mediainfo_2_0.xsd" version="2.0">
  <creatingLibrary version="17.12" url="https://mediaarea.net/MediaInfo">MediaInfoLib</creatingLibrary>
  <media ref="test.flac">
    <track type="General">
      <AudioCount>1</AudioCount>
      <FileExtension>flac</FileExtension>
      <Format>FLAC</Format>
      <FileSize>18012074</FileSize>
      <Duration>279.870</Duration>
      <OverallBitRate_Mode>VBR</OverallBitRate_Mode>
      <OverallBitRate>514870</OverallBitRate>
      <StreamSize>0</StreamSize>
      <File_Modified_Date>UTC 2024-05-20 22:52:47</File_Modified_Date>
      <File_Modified_Date_Local>2024-05-21 00:52:47</File_Modified_Date_Local>
    </track>
    <track type="Audio">
      <Format>FLAC</Format>
      <Duration>279.870</Duration>
      <BitRate_Mode>VBR</BitRate_Mode>
      <BitRate>514618</BitRate>
      <Channels>1</Channels>
      <ChannelPositions>Front: C</ChannelPositions>
      <SamplingRate>48000</SamplingRate>
      <SamplingCount>13433760</SamplingCount>
      <BitDepth>16</BitDepth>
      <StreamSize>18003284</StreamSize>
      <StreamSize_Proportion>0.99951</StreamSize_Proportion>
      <Encoded_Library>reference libFLAC 1.3.2 20170101</Encoded_Library>
      <Encoded_Library_Name>libFLAC</Encoded_Library_Name>
      <Encoded_Library_Version>1.3.2</Encoded_Library_Version>
      <Encoded_Library_Date>UTC 2017-01-01</Encoded_Library_Date>
    </track>
  </media>
</MediaInfo>

XML-Parser bringt Python ja mit. So als Ansatz:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import subprocess
from pathlib import Path
from pprint import pprint
from xml.etree import ElementTree as ET


def mediainfo(file_path):
    namespace = "{https://mediaarea.net/mediainfo}"
    return [
        (
            track.get("type"),
            {
                element.tag.removeprefix(namespace): element.text
                for element in track.findall(f"{namespace}*")
            },
        )
        for track in ET.fromstring(
            subprocess.check_output(
                ["mediainfo", "--Output=XML", str(file_path)], text=True
            )
        ).findall(f"{namespace}media/{namespace}track")
    ]

Macht aus dem Beispiel:

 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
[('General',
  {'AudioCount': '1',
   'Duration': '279.870',
   'FileExtension': 'flac',
   'FileSize': '18012074',
   'File_Modified_Date': 'UTC 2024-05-20 22:52:47',
   'File_Modified_Date_Local': '2024-05-21 00:52:47',
   'Format': 'FLAC',
   'OverallBitRate': '514870',
   'OverallBitRate_Mode': 'VBR',
   'StreamSize': '0'}),
 ('Audio',
  {'BitDepth': '16',
   'BitRate': '514618',
   'BitRate_Mode': 'VBR',
   'ChannelPositions': 'Front: C',
   'Channels': '1',
   'Duration': '279.870',
   'Encoded_Library': 'reference libFLAC 1.3.2 20170101',
   'Encoded_Library_Date': 'UTC 2017-01-01',
   'Encoded_Library_Name': 'libFLAC',
   'Encoded_Library_Version': '1.3.2',
   'Format': 'FLAC',
   'SamplingCount': '13433760',
   'SamplingRate': '48000',
   'StreamSize': '18003284',
   'StreamSize_Proportion': '0.99951'})]

So eine for-Schleife mit Initialisierung, Test, und Aktualisierung gibt es in Python nicht. Das Beispiel wäre in Python:

1
2
for i in range(MAX):
    # i kann hier benutzt werden.

Man kann i nicht verändern, also der Iterator den range() liefert bekommt davon nichts mit. Das ist aber sowieso ein bisschen undurchsichtig, und eine while-Schleife würde das klarer machen, dass man im Schleifenkörper mit Änderungen an i rechnen muss, die den Schleifenablauf beeinflussen.

Solche Laufindexschleifen sind aber auch eher selten, denn meisten greift man mit dem Index ja auf irgendwelche Sequenzen zu und diese Indirektion braucht man in Python nicht. Falls man parallel über mehr als eine Sequenz iterieren muss gibt es zip() und falls man zusätzlich eine laufende Zahl braucht, gibt es enumerate().

Grund für das csv-Modul statt das selbst zu basteln wäre übrigens das CSV nicht so einfach ist wie man auf den ersten Blick denkt. Das Format kommt auch mit dem Trennzeichen und sogar Zeilenenden in Zellen klar. Der meiste dafür selbst gebastelte Code allerdings nicht.

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Ich kriege immer mal wieder:

malloc(): invalid size (unsorted)
Speicherabzug geschrieben

beim Lauf des Skripts.

Oft beim Lauf mit noch nicht mal der gleichen Datei bei Wiederholung, meist erst nach längerer Zeit, meist mit MP3-Dateien, weniger mit FLAC.

Manche Quellen, die häufig und regelmäßig auftreten, konnte ich schon aussortieren, siehe auch: 2122243

Aber wie komme ich generell dahinter, wo die Ursache ist?
Passiert das

  • im Python Code ?

  • in der mediainfo Bibliothek ?

Falls es in der mediainfo Bibliothek passiert, wie könnte ich das in Python abfangen, damit das Skript weiterlaufen kann?

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Ich habe den Fehler nun etwas eingegrenzt. Hier erst mal der Code:

  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
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
import csv
import re
import subprocess
import sys
from pathlib import Path
from pprint import pprint
from pymediainfo import MediaInfo


def file(file_path):
    result = subprocess.check_output(
        ["file", "--brief", str(file_path)], text=True)
    return [part.strip() for part in result.split(",")]


def detect_volumes(file_path):
    result = subprocess.run(
        ["ffmpeg", "-hide_banner", "-nostdin",
            *("-i", str(file_path)),
            *("-af", "volumedetect"), *("-f", "null"), "/dev/null"],
        capture_output=True,
        check=True
    )
    return (
        float(re.search(rb"mean_volume: (\S*)", result.stderr)[1]),
        float(re.search(rb"max_volume: (\S*)", result.stderr)[1])
    )
#    volumes = { # use dictionary (= HashMap)
#        match[1]: float(match[2])
#        for match in re.finditer(rb"m(ean|ax)_volume: (\S*)", result.stderr)
#    }
#    return (volumes[b"ean"], volumes[b"ax"])


def main():
    if len(sys.argv) <= 3:
        print("Usage: ./volume-analysis.py START_PATH .mp3|.flac THRESHOLD [--verbose]")
        sys.exit(1)
    start_path = Path(sys.argv[1])
    extension = sys.argv[2]
    threshold = float(sys.argv[3])
    verbose = "--verbose" in sys.argv[4:]
    album_count = 0
    album_mean_sum = 0.0
    album_mean_min = 0.0
    album_mean_max = -128.0
    album_peak_sum = 0.0
    album_peak_min = 0.0
    album_peak_max = -128.0
    albums_with_low_volume = []
    albums_with_mixed_mode = []
    albums_with_mixed_constant_rate = []
    files_without_bitrate = []
    files_with_comment = []
    files_with_notes = []
    files_with_audio_comment = []
    files_with_errors = []

    with (
        open(f"mediainfo_albums{extension}.csv", "w", encoding="utf-8", newline=""
        ) as csv_file,
        open(f"mediainfo_files{extension}.csv", "w", encoding="utf-8", newline=""
        ) as verbose_csv_file
    ) :
        csv_writer = csv.writer(csv_file, delimiter=';')
        verbose_csv_writer = csv.writer(verbose_csv_file, delimiter=';')
        csv_writer.writerow(["No","Path","Size","Mode","Bit rate"])
        verbose_csv_writer.writerow(["Album","No","Path","Size","Settings","Channels",
        "Sampling rate","Bit depth","Mode","Bit rate","Vol mean","Vol max"])

        directory_paths = [start_path] + sorted(
            path for path in start_path.rglob("*") if path.is_dir()
        )
        for directory_path in directory_paths :
            file_count = 0
            mean_sum = 0.0
            mean_min = 0.0
            mean_max = -128.0
            peak_sum = 0.0
            peak_min = 0.0
            peak_max = -128.0
            rate_mode = "None"
            rate_sum = 0.0
            rate = 0.0
            rate_old = 0.0
            rate_mixed = False
#            if not (file_paths := sorted(
#                directory_path.glob("*"+extension, case_sensitive=False)
#                # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
#            )) : continue
            if not (file_paths := sorted(path for path in
                directory_path.glob("*"+extension, case_sensitive=False)
                # directory_path.glob("*.[fF][lL][aA][cC]") # before Python 3.12
                    if (extension == ".flac" or not path.with_suffix(".flac").exists())
            )) : continue

            album_count += 1
            for file_path in file_paths :
                if verbose :
                    print(file_path)
                try:
                    media_info = MediaInfo.parse(file_path)
                    general = media_info.general_tracks[0]
                    audio = media_info.audio_tracks[0]
                    # print(f" general comment: {general.comment}, audio comment: {audio.comment}, abcd: {audio.abcd},")
                    # print(f" audio size: {audio.stream_size} bytes, file size: {general.file_size} bytes.")
                    # pprint(general.to_data())
                    # pprint(audio.to_data())
                    if verbose :
                        print(f"  Format={audio.format}, Format settings={audio.format_settings},",
                            f"Format profile={audio.format_profile}, Channel(s)={audio.channel_s},",
                            f"Bit rate mode={audio.bit_rate_mode}, Bit rate={audio.bit_rate},",
                            f"Sampling rate={audio.sampling_rate}, Bit depth={audio.bit_depth}")
                    FLAC = "FLAC" in audio.format
                    MP3 = ("MPEG Audio" in audio.format and "Layer 3" in audio.format_profile) or extension == ".mp3"
                    if FLAC or MP3 :
                        mean_volume, max_volume = detect_volumes(file_path)
                        mean_sum += mean_volume
                        mean_min = min(mean_min, mean_volume)
                        mean_max = max(mean_max, mean_volume)
                        peak_sum += max_volume
                        peak_min = min(peak_min, max_volume)
                        peak_max = max(peak_max, max_volume)
                        rate_mode = audio.bit_rate_mode if file_count == 0 else\
                            rate_mode if audio.bit_rate_mode == rate_mode else "Mixed"
                        if entry := audio.bit_rate :
                            rate = float(entry)
                            rate_mixed = rate_mixed or (file_count > 0 and rate != rate_old)
                            rate_sum += (rate_old := rate)
                        else :
                            files_without_bitrate.append(file_path)
                        if general.comment :
                            files_with_comment.append(file_path)
                            print(f"  with general comment: {general.comment}")
                        if general.notes :
                            files_with_notes.append(file_path)
                            print(f"  with general notes: {general.notes}")
                        if audio.comment :
                            files_with_audio_comment.append(file_path)
                            print(f"  with audio comment: {audio.comment}")
                        file_count += 1
                        if verbose :
                            tail = "      ("+str(audio.bit_rate)+", "+str(audio.bit_rate_mode)+")"
                            print("  mean volume: %5.1f dB, peak volume: %5.1f dB%s"
                                %(mean_volume, max_volume, tail))
#                            print(f"  mean volume: {mean_volume: 5.1f} dB,"
#                                f" max volume: {max_volume: 5.1f} dB")

                        # Write values to csv file:
                        values = [album_count, file_count, file_path.name, audio.stream_size,
                            audio.format_settings, audio.channel_s, audio.sampling_rate, audio.bit_depth,
                            audio.bit_rate_mode, audio.bit_rate, mean_volume, max_volume]
                        for value in values :
                            if value == None : value = "" # otherwise crash: "malloc(): invalid size (unsorted)"
                        verbose_csv_writer.writerow(values)

                    else :
                        print (file(file_path))
                except subprocess.CalledProcessError as error :
                    files_with_errors.append((file_path, error.returncode))

            print(len(file_paths), "files,", len(file_paths) - file_count,
                "errors in:", directory_path)
            if extension not in [".flac", ".mp3"] :
                if verbose : print(); continue
            if file_count :
                mean_average = mean_sum / file_count
                peak_average = peak_sum / file_count
                rate_average = rate_sum / file_count
                print("Album volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB      (%1.0f, %s)"
                    %(mean_average, mean_min, mean_max, peak_average, peak_min, peak_max, rate_average, rate_mode))
                album_mean_sum += mean_average
                album_mean_min = min(album_mean_min, mean_min)
                album_mean_max = max(album_mean_max, mean_max)
                album_peak_sum += peak_average
                album_peak_min = min(album_peak_min, peak_min)
                album_peak_max = max(album_peak_max, peak_max)
            if peak_max < threshold :
                albums_with_low_volume.append(
                    (directory_path, mean_sum / file_count, peak_min, peak_max))
            if rate_mode == "Mixed" : albums_with_mixed_mode.append((directory_path, rate_average))
            if rate_mixed and rate_mode == "CBR" :
                albums_with_mixed_constant_rate.append((directory_path, rate_average))
            if verbose : print()

            # Write values to csv file:
            values = [album_count, directory_path, file_count, rate_mode, int(rate_average),]
            for value in values :
                if value == None : value = "" # otherwise crash: "malloc(): invalid size (unsorted)"
            csv_writer.writerow(values)


    print()
    print("Directories total:     ", len(directory_paths))
    print("Albums with "+extension+" files:", album_count)
    if album_count :
        album_mean_average = album_mean_sum / album_count
        album_peak_average = album_peak_sum / album_count
        print("All albums volumes: mean: %5.1f | %5.1f | %5.1f dB, peak: %5.1f | %5.1f | %5.1f dB"
            %(album_mean_average, album_mean_min, album_mean_max, album_peak_average, album_peak_min, album_peak_max))
    if albums_with_low_volume :
        print(f"\nAlbums with low volume ({len(albums_with_low_volume)}):")
        for album, average_mean, peak_min, peak_max in albums_with_low_volume:
            print("%s\n"
                "    volumes: average mean | min max | max max : %5.1f | %5.1f | %5.1f dB"
                %(album, average_mean, peak_min, peak_max))
    if albums_with_mixed_mode :
        print(f"\nAlbums with mixed bit rate mode ({len(albums_with_mixed_mode)}):")
        for album, rate_average in albums_with_mixed_mode:
            print(f"{album} ==> {rate_average/1000:3.1f} kb/s")
    if albums_with_mixed_constant_rate :
        print(f"\nAlbums with mixed constant bit rate ({len(albums_with_mixed_constant_rate)}):")
        for album, rate_average in albums_with_mixed_constant_rate:
            print(f"{album} ==> {rate_average/1000:3.1f} kb/s")
    if files_without_bitrate :
        print(f"\n{extension} files without bit rate: ({len(files_without_bitrate)}):")
        for file_path in files_without_bitrate:
            print(file_path)
    if files_with_comment :
        print(f"\n{extension} files with comment: ({len(files_with_comment)}):")
        for file_path in files_with_comment:
            print(file_path)
    if files_with_notes :
        print(f"\n{extension} files with notes: ({len(files_with_notes)}):")
        for file_path in files_with_notes:
            print(file_path)
    if files_with_audio_comment :
        print(f"\n{extension} files with audio comment: ({len(files_with_audio_comment)}):")
        for file_path in files_with_audio_comment:
            print(file_path)
    if files_with_errors :
        print(f"\n{extension} files with errors: ({len(files_with_errors)}):")
        for file_path, returncode in files_with_errors:
            print(file_path, "==> return code:", returncode)


if __name__ == "__main__" :
    main()

Nur in folgendem Ordner taucht dann der Fehler auf (hundert andere Ordner laufen fehlerfrei durch):

$ ./volume-analysis.py "Ablage/Rick Wakeman" .mp3 -1.2 --verbose
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/01 Music Reincarnate, Part I_ The Warning (Introduction).mp3
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.3 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/02 Music Reincarnate, Part I_ The Warning (Conclusion).mp3
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.9 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/03 Music Reincarnate_ The Maker.mp3
malloc(): invalid size (unsorted)
Abgebrochen (Speicherabzug geschrieben)

Wenn ich dann in Zeile 106 den Kommentar wegnehme (um zu checken, wie weit das Programm noch kommt), also:

1
                    print(f" general comment: {general.comment}, audio comment: {audio.comment}, abcd: {audio.abcd},")

dann läuft es merkwürdigerweise fehlerfrei durch:

$ ./volume-analysis.py "Ablage/Rick Wakeman" .mp3 -1.2 --verbose
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/01 Music Reincarnate, Part I_ The Warning (Introduction).mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.3 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/02 Music Reincarnate, Part I_ The Warning (Conclusion).mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.9 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/03 Music Reincarnate_ The Maker.mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -15.6 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/04 Music Reincarnate, Part III_ The Spaceman.mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.6 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/05 Music Reincarnate_ The Realisation.mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -14.9 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/06 Music Reincarnate, Part V_ The Reaper.mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -14.6 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/07 The Prisoner.mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.6 dB, peak volume:   0.0 dB      (320000, CBR)
Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/08 The Lost Cycle.mp3
 general comment: None, audio comment: None, abcd: None,
  Format=MPEG Audio, Format settings=Joint stereo, Format profile=Layer 3, Channel(s)=2, Bit rate mode=CBR, Bit rate=320000, Sampling rate=44100, Bit depth=None
  mean volume: -13.8 dB, peak volume:   0.0 dB      (320000, CBR)
8 files, 0 errors in: Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection
Album volumes: mean: -14.2 | -15.6 | -13.3 dB, peak:   0.0 |   0.0 |   0.0 dB      (320000, CBR)


Directories total:      5
Albums with .mp3 files: 1
All albums volumes: mean: -14.2 | -15.6 | -13.3 dB, peak:   0.0 |   0.0 |   0.0 dB

Der Crash scheint also etwas mit der print-Funktion zu tun zu haben.

Mit mediainfo hat es diesmal wohl nichts zu tun, denn folgendes läuft fehlerfrei durch:

$ mediainfo "Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/"*.mp3
$ mediainfo "Ablage/Rick Wakeman/Rick Wakeman - No Earthly Connection/03 Music Reincarnate_ The Maker.mp3"

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

UlfZibis schrieb:

Ich kriege immer mal wieder:

malloc(): invalid size (unsorted)
Speicherabzug geschrieben

beim Lauf des Skripts.

  • in der mediainfo Bibliothek ?

Der Fehler wurde behoben. Danke an das MediaInfo-Team: MediaArea/MediaInfoLib/issues/2400

Bearbeitet von schwarzheit:

Forensyntax korrigiert.

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Jetzt hätte ich in dem Zusammenhang noch mal eine Frage zur Python-Syntax:

Ich will aus einer Liste von Verzeichnispfaden (directory_paths) diejenigen in eine neue (albums) extrahieren, in denen Dateien mit einer bestimmten Endung vorkommen.

 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
#!/usr/bin/env python3
import sys
from pathlib import Path

def contains(d, suffix):
    for path in d.iterdir() :
        if path.suffix == suffix : return True
    return False

def main():
    if len(sys.argv) <= 3:
        print("Usage: ./compres2mp3.py START_DIR DEST_DIR .mp3|.flac")
        sys.exit(1)
    start_path = Path(start_dir := sys.argv[1])
    dest_dir = sys.argv[2]
    extension = sys.argv[3]

    directory_paths = [start_path] + sorted(
        path for path in start_path.rglob("*") if path.is_dir())
    albums = [d for d in directory_paths if contains(d, extension)]
    print(f"dirs={len(directory_paths)}, albums={len(albums)}")
    albums = [d for d in directory_paths if ((path.suffix == extension) for path in d.iterdir())]
    print(f"dirs={len(directory_paths)}, albums={len(albums)}")
    albums = [d for d in directory_paths if (path for path in d.iterdir() if path.suffix == extension)]
    print(f"dirs={len(directory_paths)}, albums={len(albums)}")

if __name__ == "__main__" :
    main()

liefert:

$ ./test.py "Ablage" "MP3" .flac
dirs=437, albums=138
dirs=437, albums=437
dirs=437, albums=437

Nur der Umweg über die extra Funktion contains(...) liefert das richtige Ergebnis. Aber ich denke, das müsste irgendwie auch direkt nach folgendem Muster funktionieren (vielleicht per lambda-Ausdruck):

1
albums = [d for d in directory_paths if ?????]

Was mache ich da falsch?

snafu1

Avatar von snafu1

Anmeldungsdatum:
5. September 2007

Beiträge: 2149

Wohnort: Gelsenkirchen

Du machst es dir total umständlich und eben auch falsch. Nimm einfach glob() bzw. rglob(), wenn die Anwendung rekursiv erfolgen soll. Letzteres tust du ja auch schon an einer Stelle.

Wie man glob() benutzt, steht in der Dokumentation (https://docs.python.org/3/library/pathlib.html) und zwar ziemlich weit oben, bereits im zweiten Beispiel. Da fragt man sich wirklich, wie du recherchierst (falls du das überhaupt tust), bevor du eine Frage ins Internet setzt. Nichts für ungut...

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

snafu1 schrieb:

Du machst es dir total umständlich und eben auch falsch. Nimm einfach glob() ...

Dass der andere doof ist, wusste ich auch schon vor Deiner Aufklärung. Wer das aus meiner Sicht dann ist, überlasse ich Deiner scharfen Logik.

Dass man hier glob() bemühen könnte, war ich schon selbst drauf gekommen. Das ist hier aber "oversized". Sind in einem Verzeichnis z.B. 100 Dateien, muss glob() sämtliche 100 Dateien auf Passung mit dem Muster prüfen, um dann eine Liste dieser zu liefern, die man erst dann auf leer oder nicht prüfen kann. Es reicht hier aber schon die Suche abzubrechen, wenn die erste passende Datei gefunden ist. Meine Funktion contains() tut genau das. Da diese Funktionalität aber nur einmal gebraucht wird, würde ich es bevorzugen, wenn man hier eine anonyme "inline"-Funktion verwenden könnte. Die "list comprehension" ist ja schon sowas. Wie die syntaktisch definiert werden müsste, ist hier meine Frage. Auch mit Hilfe eines Lambda-Ausdrucks ist mir das bisher nicht gelungen.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4784

Wohnort: Berlin

glob() ist schon richtig, denn das erstellt ja keine Liste sondern einen Iterator, also das was Du Dir da nachbaust:

1
2
3
4
5
6
7
In [131]: it = pathlib.Path().glob("*.py")

In [132]: it
Out[132]: <generator object Path.glob at 0x7f711b246c70>

In [133]: next(it)
Out[133]: PosixPath('forum11.py')

Da kann man sich mit next() und einem Defaultwert das erste Element geben lassen, und wenn es nicht der Defaultwert ist/war, dann gibt es mindestens eine Datei mit der Dateinamenserweiterung.

1
is_album = next(dir_path.glob("*" + extension), None) is not None

UlfZibis

(Themenstarter)

Anmeldungsdatum:
13. Juli 2011

Beiträge: 3440

Wohnort: Köln

Marc_BlackJack_Rintsch schrieb:

Da kann man sich mit next() und einem Defaultwert das erste Element geben lassen, und wenn es nicht der Defaultwert ist/war, dann gibt es mindestens eine Datei mit der Dateinamenserweiterung.

1
is_album = next(dir_path.glob("*" + extension), None) is not None

Genial. wusste ich doch, dass dem Mark da noch was einfällt. 💡 👍

In meinem Fall reicht sogar (also ohne is not None):

1
    albums = [d for d in directory_paths if next(d.glob("*" + extension, case_sensitive=False), None)]

Jetzt verstehe ich auch, warum sorted(d.glob(...)) nötig ist, denn erst dadurch entsteht eine Liste.

Echt spannend, was die Python-Syntax so hergibt. Was mich aber wundert, ist, dass next() hier eine normale Funktion ist, und nicht eine Methode des Iterator-Objekts, also sowas:

1
    d.glob(...).next(None)

Zugegeben, der Performance-Gewinn ist in Relation zum ganzen Programm irrelevant, aber es hat mich doch interessiert, ob es eine Möglichkeit gibt.