Č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ý priestorSystem
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 premennejSystem.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ódyRead
. - 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 Trim
om 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 Object
u: 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
- How To Create Object in PowerShell, Windows PowerShell Blog, 11. marec 2011
- Using Add-Member, Richard Siddaway’s Blog