Ein MP3-Kopierer für den Quintessential Player

Wie es dazu kam

So muß der Player zum Mitschneiden eingestellt werden.Seit ich endlich DSL benutzen kann, kann ich z.B. auch Internetradio hören. Ich habe verschieden Programme getestet und war dabei mit dem "Quintessential Player" bei MP3-Strömen besonders zufrieden. Insbesondere weil der Player die empfangene Sendung auch mitschneiden kann. Die Einstellung muß aber aber unter Umständen lange suchen (bei "Voreinstellungen -> Encoding" steht sie nicht).

Das kann z.B. auch "Audiojack 2", das ich als Vollversion einer CHIP-Heft-DVD installiert habe. Es kann die MP3-Dateien nach Interpreten in eigenen Verzeichnissen speichern und entfernt sogar den Interpretennamen. Leider scheint mir bei diesem Programm die Verbindung häufiger zusammenzubrechen. Aber diese Funktionalität hätte ich schon gerne. Der Quintessential Player kann für jeden Sender einen eigenen Ordner anlegen und nutzen, nicht für InterpretInnen. Ich mußte mir deshalb selbst etwas programmieren.

Das mag mit vielen Programmiersprachen zu lösen sein, sicher mit Java, vielleicht auch mit PHP oder Javascript. Ich habe mich für VBA entschieden und mein Programm in eine Word-Vorlage eingebaut.

Warum VBA? Ich habe schon viel mit VBA gearbeitet und konnte sofort Teile früherer Programme übernehmen. Ich war sicher, für alle Anforderungen eine Lösung zu finden. Hätte ich eine mir weniger bekannte Sprache bevorzugt, wäre ich vielleicht an eine Stelle gekommen, an der ich lange nach dem richtigen Weg hätte suchen müssen, vielleicht sogar erfolglos.

Mit welchem MS-Office-Programm ich das Problem löse, ist im Prinzip egal, ich habe Word genommen, weil die Textverarbeitung bei mir mit Works schon beim Computerkauf installiert war, ansonsten verfüge ich nur über ein älteres Officepaket (mit dem es auch ginge), und zum Verbreiten ist es auch besser, wenn ich Word nehme, weil das fast jeder hat. Noch besser wäre Open Office, weil das kostenlos ist. Vielleicht stelle ich mein Programm noch auf Open Office um, wenn ich mich mehr mit der dort benutzten Basic-Version beschäftigt habe.

Die Oberfläche

Für die Ein- und Ausgabe benutze ich Tabellen. Weitere Einstellungen werden mit Kontrollkästchen vorgenommen. Schaltflächen starten das Programm oder eine Ereignisprozedur zum Auswählen der Verzeichnisse.

Auswahl der Pfade

Hier wird keine Datei angegeben, sondern ein Verzeichnis. Wer nicht so einfache Pfade hat, die man einfach in die Tabelle schreiben kann, kann die Schaltflächen anklicken und Quell- und Zielverzeichnis mit einem Dialog auswählen. Dazu verwende ich eine Funktion der Windows-Bibliothek shell32.dll.

Ordner für InterpretInnen anlegen

Das ist der eigentliche Grund meiner Arbeit. In den ersten Tagen habe ich das manuell gemacht und fand dann, daß auch ein Arbeitsloser seine freie Zeit besser verwenden kann. Der Name des anzulegenden Ordners steht nämlich im Dateinamen vor dem ersten Auftreten von " - ". Das Programm überprüft das vorhandensein des aus diesem Grunde leicht zu ermittelnden Zielverzeichnisses und legt es ggf. an. Das wäre hier "Gene Autry". Später stellte sich heraus, daß manche MP3-Dateien ein führendes Leerzeichen besaßen, was Verzeichnisnamen mit führendem Leerzeichen verursachte. Das kann man mit TRIM() ausschließen. Die hier ausgelieferte Programmversion hat das noch nicht, baut es selber ein!

Nummerierung entfernen

Der Player nummeriert Titel, die er mehrmals aufnimmt, mit " (01)", " (02)", " (03)" usw.. Damit ist es leicht für 99 Dateien das gleiche Verfahren zu verwenden.

Nur für diese Nummerierung habe ich die Möglichkeit vorgesehen, diese Bestandteile zu entfernen. Wer mein Programm für ähnliche Aufgaben umbauen möchte, sollte dies berücksichtigen.

größere Datei ersetzt kleinere

Oft sind mehrmal gespeicherte Musikstücke genau gleich groß, im obigen Beispiel "Ride, Tenderfoot, Ride". Dann sind sie wahrscheinlich komplett gespielt worden. Es ist mir egal, ob ich die erste Aufnahme behalte oder die zweite. Manchmal wird die Verbindung gestört, dann legt der Player 2 Dateien an, die zusammen knapp so groß sind, wie die gesendete MP3-Datei. Hier wäre es bei "wertvollen" Aufnahmen nötig, manuell daraus eine MP3-Datei zu machen, sonst zerstört mein Programm den kleineren Teil. Das habe ich eher für Aufnahmen vorgesehen, bei denen vielleicht ausgeblendet wurde, im Beispiel "Don`t Fence Me In" und "South Of The Border" (0:2:48 oder 0:3:30 lang).

Früher mußte ich das jedesmal neu entscheiden.

neuere Datei ersetzt ältere

Mir ist völlig egal, ob ich eine neuere oder ältere Aufnahme behalte. Das habe ich aber nicht beim Entwurf der Oberfläche, sondern erst danach bei der Programmierung erkannt. Da gab es Konflikte mit "größere Aufnahme ersetzt kleinere". Was sollte passieren, wenn die größere auch die ältere wäre? Wer diese Funktionalität haben möchte, muß sie noch einbauen.

Umgang mit nicht kopierten Dateien

Ich will nicht kopierte Dateien nicht behalten. Deshalb geht mein Programm entweder so vor, wie für löschen eingebaut, oder es übergeht die Auswahl und macht nichts mit den nicht kopierten Dateien. Wer diese Funktionalität haben möchte, muß sie noch einbauen.

Informationen

Mich interessierte, wieviele Dateien das Programm bearbeitet, kopiert, löscht und in neue Verzeichnisse verschiebt. Deshalb lasse ich diese Informationen zählen und in die untere Tabelle eintragen. Man kann es auch abschalten. Dann geht aber ein weitere Nutzen der Information verloren. Da mein Programm nur bis zu 1000 Dateien bearbeitet, erkennt man bei einer Gesamtzahl unter 1000, daß es fertig ist, bei der Gesamtzahl 1000 kann man mit einem erneuten Anklicken der Start-Schaltfläche das Programm nochmal starten.

weitere Links

Wer andere Lösungen kennt oder sogar selbst geschrieben hat, möge mir dies mitteilen, damit ich hier darauf verlinken kann.

Download

Die Programmierung

Die Auswahl eines Verzeichnisses durch einen in Windows eingebauten Dialog habe ich bereits früher von einer im Netz veröffentlichten Lösung übernommen und nun von meinem früheren Programm. Sie muß in einem Modul untergebracht werden. An der anderen Form, Kommentare hervorzuheben, erkennt man, daß dieser Teil nicht von mir ist.

Die Auswertung der Benutzereingaben und -Aktionen erfolgt dagegen bei "ThisDocument"

Private Sub btn_Start_Click()

'----------------------------------------------------------------------+
' Dateien kopieren und je nach Einstellung überschreiben, umbenennen   |
' oder löschen und einen Bericht erstellen.                            |
'----------------------------------------------------------------------+
' Schritt 1: Dateinamen bestimmen                                      |
'            Aus der Angabe des Verzeichnisses ergibt sich mit der     |
'            DIR-Funktion jede Datei, die eventuell kopiert oder ge-   |
'            löscht wird. Aber die Zieldatei kann anders heißen, wenn  |
'            eine Numerierung entfernt werden muß. Eventuell muß aus   |
'            der Quelldatei noch der Name des Interpreten ermittelt    |
'            werden, um ihn als Unterordner in den Pfad einzubauen.    |
'----------------------------------------------------------------------+

Dim Startpfad   As String   ' Pfad der Startdatei
Dim Startname   As String   ' Einzeldatei
Dim Zielpfad    As String   ' Pfad der Zieldatei
Dim Interpret   As String   ' Interpretenname wie im Dateinamen
Dim Zielname    As String   ' Einzeldatei nach umbenennen
Dim erlaubt     As Boolean
Dim Feld(1000)  As String   ' Array für die Dateinamen. 1000 maximal bearbeiten
Dim Feldindex   As Integer  ' Zähler
' --- diverse Zähler ---

Dim i_gesamt    As Integer
Dim i_kopiert   As Integer
Dim i_besser    As Integer
Dim i_vorhanden As Integer
Dim i_Ordnerneu As Integer

' keine Eingabe der Datenmaske -> *.*
' Möglich wäre auch "*.mp3"

If Zellentext(ActiveDocument.Tables(1).Cell(3, 1).Range.Text) = "" Then _
ActiveDocument.Tables(1).Cell(3, 1).Range.Text = "*.*"

' Naheliegend war wie in Btn_Woher_Click()
' Dateien = ActiveDocument.Tables(1).Cell(1, 1).Range.Text & "\" & _
' ActiveDocument.Tables(1).Cell(3, 1).Range.Text
' Dann werden aber Steuerzeichen mit eingebaut.

i_gesamt = 0
i_kopiert = 0
i_besser = 0
i_vorhanden = 0
i_Ordnerneu = 0

erlaubt = False ' erst mal nicht draufloskopieren
Interpret = ""  ' und keinen Unterordner anlegen
Startpfad = Zellentext(ActiveDocument.Tables(1).Cell(1, 1).Range.Text) & "\"
Zielpfad = Zellentext(ActiveDocument.Tables(1).Cell(2, 1).Range.Text) & "\"

For Feldindex = 0 To 999
    Feld(Feldindex) = ""
Next
Feld(0) = Dir(Startpfad & Zellentext(ActiveDocument.Tables(1).Cell(3, 1).Range.Text))
For Feldindex = 1 To 999
    Feld(Feldindex) = Dir()
    If Feld(Feldindex) = "" Then Exit For
    ' Das ist nötig, weil Dir() nur beim ersten nicht Finden "" zurückgibt.
    ' Ohne neues Argument ergibt der erneute Aufruf von Dir() einen Fehler

Next

'----------------------------------------------------------------------+
' Schritt 2: Schleife                                                  |
'            Dir gibt den Dateinamen zurück, nicht den Pfad            |
'            Manipulation von Dateinamen hintangestellt, ist der Kern  |
'            der Schleife der Befehl                                   |
'               FileCopy Startpfad & Startname, Zielpfad & Zielname    |
'----------------------------------------------------------------------+

For Feldindex = 0 To 999
    Startname = Feld(Feldindex)       ' nächsten Quelldateinamen holen
    If Startname = "" Then Exit For

'----------------------------------------------------------------------+
' Schritt 3: Zieldatei ermitteln. Dazu muß eventuell eine Nummerierung |
'            entfernt werden und ein Interpretenname als Zielordner-   |
'            Unterordner ermittelt werden.                             |
'----------------------------------------------------------------------+

    i_gesamt = i_gesamt + 1
    Zielname = Startname

    If Me.KK_entnummerieren Then    ' Nummerierung enfernen
        If Right(Startname, 4) = ".mp3" Then
            If Left(Right(Startname, 9), 2) = " (" And _
            IsNumeric(Left(Right(Startname, 7), 2)) And _
            Left(Right(Startname, 5), 1) = ")" Then
            ' Wenn ein Titel mehrmals gespeichert wurde, enthält der Datei-
            ' name vor der Erweiterung eine typische 5-Zeichen-Kombination:
            ' Leerzeichen
            ' Klammer auf - prüfe mit Left(Right(Startname, 9), 2) = " ("
            ' zweistelliger Zähler - prüfe mit IsNumeric(Left(Right(Startname, 7), 2))
            ' Klammer zu - prüfe mit Left(Right(Startname, 5), 1) = ")"

            Zielname = Left(Startname, Len(Startname) - 9) & ".mp3"
            End If
        End If
    End If

'----------------------------------------------------------------------+
' Schritt 4: Kopieren vorbereiten. Dazu muß eventuell ein Ordner ange- |
'            legt werden (für Interpreten) und geprüft werden, ob am   |
'            Ziel schon eine entsprechende Datei existiert.            |
'----------------------------------------------------------------------+

    If Me.KK_Ordner Then ' Ordner für Interpreten
        If InStr(1, Startname, " - ") <> 0 Then
            Interpret = Trim(Left(Startname, InStr(1, Startname, " - ") - 1))
            ' Trim(), weil auch " - " vorkam
            ' Instr() ergibt die Position des ersten Leerzeichens, daher -1

            If Dir(Zielpfad & "\" & Interpret, vbDirectory) = "" Then
                ' Verzeichnis existiert noch nicht, anlegen!
                MkDir Zielpfad & "\" & Interpret
                i_Ordnerneu = i_Ordnerneu + 1
            End If
            ' Um später nicht prüfen zu müssen, ob die Variable leer ist
            ' (siehe ELSE), wird schon hier "\" angehängt

            Interpret = Interpret & "\"
            ' Das ging nicht vor MkDir()
            ' Für die Prüfung, ob ein Verzeichnis existiert, ist es egal
            ' Beispiel: Suche C:\Daten\Test
            ' 1. C:\Daten\Test existiert
            '    dir ("C:\Daten\Test", vbdirectory) = "Test"
            '    dir ("C:\Daten\Test\", vbdirectory) = "."
            ' 2. C:\Daten\Test existiert nicht
            '    dir ("C:\Daten\Test", vbdirectory) = ""
            '    dir ("C:\Daten\Test\", vbdirectory) = ""

        Else    ' alles in den gleichen Ordner kopieren
            Interpret = ""
        End If
    End If

'----------------------------------------------------------------------+
' Schritt 5: Kopieren                                                  |
'            Hindernisse können sein, daß eine bereits vorhandene Datei|
'            Datei nicht überschrieben werden darf,                    |
'            wenn sie jünger oder größer ist                           |
'----------------------------------------------------------------------+

    If Dir(Zielpfad & "\" & Interpret & Zielname) = "" Then
    ' Datei nur an der Quelle, nicht am Ziel vorhanden
        erlaubt = True
    Else
    ' wenn die Datei schon am Ziel exisiert
        erlaubt = False
        i_vorhanden = i_vorhanden + 1

        ' Angefangen, dann aber abgebrochen
        ' If Me.KK_erneuern Then ' neuere Datei ersetzt ältere
        '    If (FileDateTime(Zielpfad & Interpret & Zielname) < filedatetime(startpfad & startname)) then
            '    erlaubt = true
        '    end if
        ' else
        '    erlaubt = true
        ' end if

        if me.kk_vergrößern then    ' größere datei ersetzt kleinere
            erlaubt = (dateigroesse(zielpfad & interpret & zielname) < dateigroesse(startpfad & startname))
            if me.o_löschen and _
            (dateigroesse(zielpfad & interpret & zielname) >= _
            Dateigroesse(Startpfad & Startname)) Then
                Kill Startpfad & Startname
                ' da kleiner als bereits vorhandene Datei oder die gleiche im Ziel
            End If
        End If
        If erlaubt Then
            Kill Zielpfad & Interpret & Zielname
            i_besser = i_besser + 1
        End If
        ' Objekt.DeleteFile Dateispez[, erzwingen] ist mir zu kompliziert
    End If

    If erlaubt Then
        FileCopy Startpfad & Startname, Zielpfad & Interpret & Zielname
        i_kopiert = i_kopiert + 1
        If Me.O_löschen Then Kill Startpfad & Startname
    End If
    ' Problem: Dir() wurde z.B. benutzt, um die Existenz eines Verzeichnisses zu prüfen.
    ' Startname = Dir()       

Next

If Me.KK_Info Then
    ActiveDocument.Tables(2).Cell(1, 1).Range.Text = "Ergebnis"
    ActiveDocument.Tables(2).Cell(2, 1).Range.Text = _
    "Insgesamt wurden " & Trim(Str(i_gesamt)) & " Dateien bearbeitet." & Chr(13) & Chr(10) & _
    Trim(Str(i_vorhanden)) & " Dateien waren schon vorhanden, " & _
    Trim(Str(i_besser)) & " wurden durch größere ersetzt." & Chr(13) & Chr(10) & _
    Trim(Str(i_Ordnerneu)) & " neue Verzeichnisse sind angelegt worden."
Else
    ActiveDocument.Tables(2).Cell(1, 1).Range.Text = ""
    ActiveDocument.Tables(2).Cell(2, 1).Range.Text = ""
End If

End Sub

Private Sub Btn_woher_Click()

'----------------------------------------------------------------------+
' Ein Quell-Vezeichnis auswählen                                       |
' und eintragen, wenn wirklich eine Auswahl getroffen wurde.           |
'----------------------------------------------------------------------+

Dim Inhalt As String
Inhalt = GetDirectory("Quellverzeichnis auswählen")
If Inhalt <> "" Then ActiveDocument.Tables(1).Cell(1, 1).Range.Text = Inhalt

End Sub

Private Sub Btn_wohin_Click()

'----------------------------------------------------------------------+
' Ein Ziel-Vezeichnis auswählen                                        |
' und eintragen, wenn wirklich eine Auswahl getroffen wurde.           |
'----------------------------------------------------------------------+

Dim Inhalt As String
Inhalt = GetDirectory("Zielverzeichnis auswählen")
If Inhalt <> "" Then ActiveDocument.Tables(1).Cell(2, 1).Range.Text = Inhalt

End Sub

Function Zellentext(Eingabe As String) As String

'----------------------------------------------------------------------+
' Bei der Rückgabe des Cell-Ranges ist zu beachten, dass der Text in   |
' diesem Range immer mit zwei Steuerzeichen endet (ASCII-Steuerzeichen |
' dezimal 13 und 7). Diese Steuerzeichen sollte man abtrennen, damit   |
' der Text in sauberer Form vorliegt.                                  |
'----------------------------------------------------------------------+

Zellentext = IIf(Len(Eingabe) > 2, Left(Eingabe, Len(Eingabe) - 2), "")

End Function

Function Dateigroesse(Datei As String) As Long

'----------------------------------------------------------------------+
' Für die Dateigröße gibt es nicht so eine bequeme eingebaute Funktion |
' wie FileDateTime() für das Dateidatum. Das Konzept ist aus der Hilfe.|
'----------------------------------------------------------------------+

Dim f   As Object
Dim fs  As Object
Set fs = CreateObject("Scripting.FileSystemObject")
Set f = fs.GetFile(Datei)

Dateigroesse = f.Size

End Function

'**** Anfang des anpassbaren Teils *****
'Private Const Modus = 0 '0 = Ordner wählen; 1: = Datei wählen
'Private Const Modal = 1 '0 = ungebunden; 1 = gebunden (modal)
'Private Const Hinweis = "Bitte wählen Sie aus:" 'Erscheint wörtlich im Dialog
'**** Ende des anpassbaren Teils *****

' Typdeklaration für API-Dialog 'Verzeichnis auswählen'

Public Type BROWSEINFO
            hOwner As Long
            pidlRoot As Long
            pszDisplayName As String
            lpszTitle As String
            ulFlags As Long
            lpfn As Long
            lParam As Long
            iImage As Long
End Type

Declare Function GetActiveWindow Lib "user32" () As Long

Declare Function SHGetPathFromIDList Lib "shell32.dll" Alias "SHGetPathFromIDListA" _   (ByVal pidl As Long, ByVal pszPath As String) As Long

Declare Function SHBrowseForFolder Lib "shell32.dll" Alias "SHBrowseForFolderA" _   (lpBrowseInfo As BROWSEINFO) As Long

Function GetDirectory(Optional Msg) As String

Dim bInfo As BROWSEINFO
Dim path As String
Dim r As Long, x As Long, pos As Integer

'If Modal = 0 Then
'    bInfo.hOwner = 0               'Ungebunden
'Else

bInfo.hOwner = GetActiveWindow      'Gebunden (modal)
'End If
bInfo.lpszTitle = Msg               'Titel des Dialoges
'If Modus = 0 Then
    bInfo.ulFlags = &H1             'Ordner auswählen
'Else
'    bInfo.ulFlags = &H4000         'Datei auswählen
'End If
bInfo.pidlRoot = 0&                 'Root
x = SHBrowseForFolder(bInfo)
path = VBA.Space$(512)
r = SHGetPathFromIDList(ByVal x, ByVal path)
If r Then
    pos = InStr(path, VBA.Chr$(0))
    GetDirectory = VBA.Left(path, pos - 1)
Else
    GetDirectory = ""
End If

End Function