MP3 a ID3 tagy: to všetko v 100% Powershelli

Čo takto si v PowerShelli vytvoriť vlastnú funkciu na zisťovanie ID3 tagu v MP3kách?

Takto by sme vedeli prehľadávať súbory a vidieť v nich viac.

Pre jednoduchosť sa venujme klasike — ID3 tagom vo verzii 1 — o ktorých vieme, že zaberajú posledných 128 bajtov MP3 súboru. A ak chceme byť ešte jednoduchší, obmedzíme sa len na položky autora a názov skladby, lebo stačí vedieť, že názov v tagu začína od štvrtého znaku (index 3) a vyžaduje najviac 30 znakov, a autor nasleduje hneď za ním (index 33) a potrebuje rovnaký počet znakov.

Ako na to v Bodka Nete?

Výhoda je, že vieme využiť všetky základné triedy a metódy v .NETe:

  • System.IO.FileStream bude načítavať súbory
  • jeho metódou Seek sa posunieme na pozíciu 128 bajtov od konca
  • celý tag vieme načítať do 128-bajtového poľa

Ako na to v Silnej mušli?

Prvá časť kódu bude vyzerať:

$fileStream = New-Object IO.FileStream ($mp3, [IO.FileMode]::Open)
$fileStream.Seek(-128, [IO.SeekOrigin]::End)
$buf = New-Object byte[] 128
$fileStream.Read($buf, 0, 128)

Aha, koľko syntaktických podivností!

  • Na vytvorenie novej inštancie využijeme cmdlet New-Object. Uvedieme názov triedy (pričom menný priestor System môžeme vynechať, bude zavedený automaticky) a parametre konštruktora. Keďže v tomto prípade využijeme konštruktor s dvoma argumentami (reťazec s cestou k súboru a režim otvárania), oba parametre dodáme v jednom dvojprvkovom poli. Všimnime si tiež prístup k statickej inštančnej premennej System.IO.FileMode.Open — v PowerShelli musíme použiť hranaté zátvorky a dve dvojbodky — s využitím [IO.FileMode]::Open.
  • Klasické metódy na objektoch voláme zaužívaným spôsobom: vidieť to na volaní metódy Seek i metódy Read.
  • Stodvadsaťosembajtové pole bajtov vytvoríme opäť pomocou New-Object: ale v tomto prípade dĺžku poľa odovzdáme ako klasický parameter, bez zátvoriek. (Máme totiž len jeden parameter pre “konštruktor” poľa.)

Bajty do reťazcov

Máme teda v premennej $buf celý tag. Ako ho prevedieme na jednotlivé položky? Trieda System.Text.Encoding poskytuje statické premenné pre jednotlivé kódovania (napr. pre ASCII, či UTF-8), čo sú objekty, ktoré dokážu previesť polia bajtov na reťazce.

$title = [System.Text.Encoding]::ASCII.GetString($buf[3..32]).Trim()
$artist = [System.Text.Encoding]::ASCII.GetString($buf[33..62]).Trim()

Zároveň tam máme ďalší syntaktický trik: podpole z poľa získame pomocou rezov (slices), kde vieme veľmi ľahko zobrať príslušné časť tagu a previesť ju na reťazec. Finálne orezávanie Trimom slúži na vyhodenie prebytočných medzier z konca položiek.

Položky von z funkcie

Ak sme nazbierali položky, ako ich dostaneme z funkcie von? Vieme, že v PowerShelli funkcie všetko pchajú do rúry, a keďže v tomto prípade chceme vrátiť dve položky (názov a umelca), je najlepšie ich zoskupiť.

Napríklad do mapy.

Ukážme si to na celom kóde:

$mp3 = "d:\MP3\Happy Melon\(01) Walk With Me.mp3"

$fileStream = New-Object IO.FileStream ($mp3, [IO.FileMode]::Open)
$fileStream.Seek(-128, [IO.SeekOrigin]::End)
$buf = New-Object byte[] 128
$fileStream.Read($buf, 0, 128)

$tag["title"] = [System.Text.Encoding]::ASCII.GetString($buf[3..32]).Trim()
$tag["artist"] = [System.Text.Encoding]::ASCII.GetString($buf[33..62]).Trim()

$fileStream.Close()

$tag

Ak si tento kus spustíme v PowerShell ISE, uvidíme:

4810567
128

Name                           Value                                                                                                                          
----                           -----                                                                                                                          
artist                         Happy Melon                                                                                                                    
title                          Walk With Me                                                                                                                   

Celkom dobré, nie?

Ale čo znamenajú tie čísla?

Upratovanie vo funkciách

To je ďalší zádrheľ PowerShellu: ak voláme funkciu, a jej návratovú hodnotu nezachytíme do premennej, znamená to, že ju pošleme do rúry.

V našom prípade voláme dve také funkcie: Seek (vracia int, ktorý nezachytávame) a Read (vracia byte).

Ak chceme odignorovať návratovú hodnotu, môžeme ju pretypovať na dátový typ void:

$mp3 = "d:\MP3\Happy Melon\(01) Walk With Me.mp3"

$fileStream = New-Object IO.FileStream ($mp3, [IO.FileMode]::Open)
[void] $fileStream.Seek(-128, [IO.SeekOrigin]::End)
$buf = New-Object byte[] 128
[void] $fileStream.Read($buf, 0, 128)

$tag["title"] = [Text.Encoding]::ASCII.GetString($buf[3..32]).Trim()
$tag["artist"] = [Text.Encoding]::ASCII.GetString($buf[33..62]).Trim()

$fileStream.Close()

$tag

Spravme si filter

Poďme vylepšovať ďalej: skúsme z tohto kódu vyrobiť funkciu, ktorá dokáže zobrať súbor zo vstupu a získať preňho ID3 tag.

Z nášho kódu môžeme rovno vyrobiť filter, ktorý bude spracovávať jednotlivé položky z rúry (dostupné v premennej $_):

filter Get-ID3Tag {
    $mp3 = $_.FullName

    $fileStream = New-Object IO.FileStream ($mp3, [IO.FileMode]::Open)
    [void] $fileStream.Seek(-128, [IO.SeekOrigin]::End)

    $buf = New-Object byte[] 128

    [void] $fileStream.Read($buf, 0, 128)
    $fileStream.Close()

    $tag["title"] = [Text.Encoding]::ASCII.GetString($buf[3..32]).Trim()
    $tag["artist"] = [Text.Encoding]::ASCII.GetString($buf[33..62]).Trim()

    $tag
}

Filter môžeme využiť aj na viacero súborov:

dir "d:\MP3\Happy Melon" | Get-ID3Tag

Pre poriadok: predpokladáme, že filter berie objekty typu FileInfo, z ktorých vytiahne plnú cestu (vlastnosť FullName).

Čo keď dostaneme nielen hudbu?

Všimnime si, ako to bude fungovať pre pestré súbory:

Name                           Value                                                                                                                          
----                           -----                                                                                                                          
artist                         Happy Melon                                                                                                                    
title                          Walk With Me                                                                                                                   
artist                         Happy Melon                                                                                                                    
title                          Ride'N Roll                                                                                                                    
artist                         Happy Melon                                                                                                                    
title                          Touchmehall                                                                                                                    
artist                         ??????1#X(    ???z??(???S?YH?                                                                                              
title                          ??Q?j??E#??????21CF?p?T?}                                                                                                 
New-Object : Exception calling ".ctor" with "2" argument(s): "Access to the path 'D:\MP3\Happy Melon\HAPPY MELON - Happy Melon (2000).txt' is denied."
At line:4 char:19
+     $fileStream = New-Object System.IO.FileStream ($mp3, [System.IO.FileMode]::O ...
+                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [New-Object], MethodInvocationException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

Všetkých umelcov a tituly naseká do jednej spoločnej mapy. To je síce nemilé, ale zase nie úplne tragické. Horšie je, keď do funkcie príde nie-empétrojka: buď dostaneme smetie, alebo podivné chyby o nedostatočnej dĺžke súboru.

Skúsme teda pri filtri rovno odignorovať aspoň nehudobné súbory.

filter Get-ID3Tag {
    $mp3 = $_.FullName
    if(! $mp3.EndsWith(".mp3")) {
        return
    }

    ...
}

Výpis sa zlepšil, vyhodili sme smetie a neporiadok. Ako však sprehľadniť výpis?

Skúsme miesto hašu normálne objekty

Zatiaľ vraciame hešovaciu tabuľku, ale omnoho lepšie by bolo vracať objekty, pretože s tými sa dá pracovať oveľa lepšie, a dajú sa triediť, zoskupovať, či filtrovať.

V PowerShelli je najjednoduchšie deklarovať triedu objektov na základe odvodenia z iného objektu (nie triedy!) Vieme vyrobiť inštanciu objektu a dynamicky za behu k nej dodať nové vlastnosti a na základe tohto upraveného objektu vytvoriť nové inštancie.

Predstavme si to ako hlúpy objekt, plastelínu, ktorá nevie nič, ale vieme z nej vymodelovať niečo konkrétnejšie. Tou plastelínou je PSObject, akýsi praotec všetkých objektov v PowerShelli (analogický triede Object, pramatke všetkých tried). Dodať nové vlastnosti môžeme tak, že ich uvedieme v mape (hashi, haštabuľke): kľúčom bude názov vlastnosti a hodnotou jej … ehm… hodnota.

$tag = New-Object PSObject -Property @{
    Title = $title
    Artist = $artist
}

Deklarácia @{ ... } slúži presne na uvedenie mapy.

filter Get-ID3Tag {
    $mp3 = $_.FullName
    if(! $mp3.EndsWith(".mp3")) {
        return
    }

    $fileStream = New-Object IO.FileStream ($mp3, [IO.FileMode]::Open)
    [void] $fileStream.Seek(-128, [IO.SeekOrigin]::End)

    $buf = New-Object byte[] 128

    [void] $fileStream.Read($buf, 0, 128)
    $fileStream.Close()

    $tag = New-Object PSObject -Property @{
        Title = [Text.Encoding]::ASCII.GetString($buf[3..32])
        Artist = [Text.Encoding]::ASCII.GetString($buf[33..62])
    }

    $tag
}

dir "d:\MP3\Happy Melon" | Get-ID3Tag

A to je všetko, máme nádherný cmdlet, môžeme sa z neho tešiť!

Alternatívne vytváranie objektov

Alternatívny spôsob spočíva v postupnom dodávaní vlastností do Objectu: zoberieme hlúpu inštanciu, prerúrujeme ju cez viacero cmdletov, ktoré ju obvešajú vlastnosťami a na záver vygenerujú upravený a bohatší a múdrejší objekt:

$title = [Text.Encoding]::ASCII.GetString($buf[3..32])
$artist = [Text.Encoding]::ASCII.GetString($buf[33..62])

$tag = New-Object Object | 
   Add-Member NoteProperty Title $title -PassThru | 
   Add-Member NoteProperty Artist $artist -PassThru

Pomocou cmdletu Add-Member dodávame nové vlastnosti: v tomto prípade chceme pridať vlastnosť s pevnou hodnotou, na čo využijeme typ NoteProperty. Nezabudnime vždy dodať parameter -PassThru, čím zabezpečíme, že Add-Member pošle upravený objekt ďalej do rúry na budúce spracovanie.

Ešte alternatívne vytváranie objektov

S plastelínou PSObject sa dá hrať aj inak. Vieme vytvoriť nový objekt a pomocou Select-Object mu dotvoriť nové vlastnosti, ktoré neskôr naplníme:

$tag = New-Object PSObject | Select Title, Artist
$tag.Title = [Text.Encoding]::ASCII.GetString($buf[3..32])
$tag.Artist = [Text.Encoding]::ASCII.GetString($buf[33..62])

Zdroje

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *