############################################## # $Id$ # # 98_MediaList.pm # # written by Tobias Faust 2016-12-19 # e-mail: tobias dot faust at gmx dot net # ############################################## # ############################################## # # Log-Levels # 0 - server start/stop # 1 - error messages or unknown packets # 2 - major events/alarms. # 3 - commands sent out will be logged. # 4 - you'll see whats received by the different devices. # 5 - debugging. # ############################################## # ############################################## ## install package libmp3-tag-perl, libjson-xs-perl, libmp3-info-perl ## ## images/cd-empty.png ############################################## use strict; use warnings; use Data::Dumper; use POSIX; use utf8; no utf8; use Encode; use MP3::Tag; use MP3::Info; use JSON::XS; #use open IN => ":encoding(utf8)", OUT => ":utf8"; use IO::File; use Fcntl; use File::Basename; use File::Copy; require 'Blocking.pm'; require 'HttpUtils.pm'; use vars qw($readingFnAttributes); # use vars qw(%attr); use vars qw(%defs); my %sets; ########################################################################### sub MediaList_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "MediaList_Define"; # $hash->{UndefFn} = "MediaList_Undef"; $hash->{SetFn} = "MediaList_Set"; # $hash->{DeleteFn} = "MediaList_Delete"; $hash->{AttrList} = " MediaList_PlayerDevice". " MediaList_PathReplaceFrom". " MediaList_PathReplaceTo". " MediaList_PathReplaceToPic". " MediaList_PlayerStartCommand". " MediaList_CacheFileDir". # TODO: $hash->{.PLAYLISTPATH} muss bei Änderung des CacheFileDir angepasst werden " MediaList_mkTempCopy:none,copy,symlink". # " MediaList_allowedExtensions". " ".$readingFnAttributes; # SetParamName -> Anzahl Paramter %sets = ( "RequestedDirectory" => { "count" => "1" }, "Play" => { "count" => "1", "args" => "currentdir,playlist" }, "Playlist_New" => { "count" => "1"}, #Arg: PlaylistName, optional "Playlist_Name" => { "count" => "1"}, #Arg: Name der Playlist "Playlist_Add" => { "count" => "1"}, #Medien aus CurrentDir werden hinzugefügt "Playlist_Del" => { "count" => "1"}, #Arg: TrackNr "Playlist_Empty" => { "count" => "0", "args" => "noArg"}, #Leeren # "Playlist_Drop" => { "count" => "1"} #Loeschen, erst relevant wenn abgespeicherte Playlist "SortBy" => { "count" => "1", "args" => "File,Title"} ); } ########################################################################### sub MediaList_Define($$) { my ( $hash, $def ) = @_; my $me = $hash->{NAME}; my @a = split( "[ \t][ \t]*", $def ); my $type = $a[1]; return "Wrong syntax: use define MediaList " if ( int(@a) != 3 ); my $MediaList_CacheFileDir = AttrVal($me, "MediaList_CacheFileDir", "cache/"); $hash->{ROOT} = $a[2]; #$hash->{".PLAYLISTPATH"} = $MediaList_CacheFileDir."/playlists_$me"; unless(-e $MediaList_CacheFileDir or mkdir $MediaList_CacheFileDir) { #Verzeichnis anlegen gescheitert Log3 $hash->{NAME}, 2, "MediaList: Angegebenes Verzeichnis $MediaList_CacheFileDir konnte erstmalig nicht angelegt werden."; return undef; } #unless(-e $hash->{".PLAYLISTPATH"} or mkdir $hash->{".PLAYLISTPATH"}) { #Verzeichnis anlegen gescheitert # Log3 $hash->{NAME}, 2, "MediaList: Angegebenes Verzeichnis $hash->{.PLAYLISTPATH} konnte erstmalig nicht angelegt werden."; # return undef; #} return undef; } ########################################################################### sub MediaList_Undef($$) { my ( $hash, $arg ) = @_; BlockingKill( $hash->{helper}{RUNNING_PID} ) if ( defined( $hash->{helper}{RUNNING_PID} ) ); return undef; } ########################################################################### sub MediaList_Delete($$) { my ( $hash, $arg ) = @_; # TODO: alle Files manuell vorher löschen #if (-e $hash->{".PLAYLISTPATH"}) { # Log3 $hash->{NAME}, 1, "Cannot delete ".$hash->{".PLAYLISTPATH"}.". Please clean up by yourself." unless rmdir $hash->{".PLAYLISTPATH"}); #} return undef; } ########################################################################### sub MediaList_Set($@) { my ($hash, @a) = @_; my $me = $hash->{NAME}; return "no set argument specified" if(int(@a) < 2); my $cmd = shift(@a); # Device $cmd = shift(@a); # Command my $par = join(" ", @a); # parameter if(!defined($sets{$cmd})) { my @s; foreach my $key (sort keys(%sets)) { $key = $key .":" . $sets{$key}{"args"} if ($sets{$key}{"args"}); push(@s, $key); } my $r = "Unknown argument $cmd, choose one of ".join(" ",@s); return $r; } if($cmd eq "RequestedDirectory") { return "$cmd needs ".$sets{$cmd}{"count"}." parameter(s)" if(@a-$sets{$cmd}{"count"} < 0); MediaList_Crawl($hash, $par); MediaList_call_playlistinfo($hash, ReadingsVal($me, "CurrentDir", $hash->{ROOT})); } if($cmd eq "Playlist_New") { $par="MyNewPlaylist" if($par eq ""); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "playlistname", $par); readingsBulkUpdate($hash, "playlist", "[]"); readingsBulkUpdate($hash, "playlistduration", ""); readingsEndUpdate($hash, 1); } if($cmd eq "Playlist_Empty") { readingsBeginUpdate($hash); readingsBulkUpdate($hash, "playlist", "[]"); readingsBulkUpdate($hash, "playlistduration", ""); readingsEndUpdate($hash, 1); } if($cmd eq "Playlist_Add") { return "given parameter not an integer value" if($par ne "" && $par !~ m/[0-9]+/); MediaList_PlayListAdd($hash, $par); } if($cmd eq "Playlist_Name") { return "no name specified" if($par eq ""); ReadingsSingleUpdate($hash, "playlist", $par, 1); } if($cmd eq "Playlist_Del") { return "no track number specified" if($par !~ m/[0-9]+/); MediaList_PlayListDel($hash, $par); } if($cmd eq "Playlist_Drop") { # gespeicherte Playlist auf HDD löschen } if($cmd eq "Play") { my $PlayerDevice = AttrVal($me, "MediaList_PlayerDevice", undef); my $PlayerStartCommand = AttrVal($me, "MediaList_PlayerStartCommand", undef); return "no Playerdevice configured, please check Attribute MediaList_PlayerDevice" unless ($PlayerDevice); return "Playerdevice not available: ".$PlayerDevice unless ($defs{$PlayerDevice}); # return "no startcommand for Playerdevice configured, please check attribute MediaList_PlayerStartCommand" unless ($PlayerStartCommand); # der MPD braucht ein paar sekunden (update-db) um die neuen Files zu erkennen und abspielen zu können # beim MPD sollte dieses Attr also nicht gesetzt werden $par = "currentdir" if ($par eq ""); # kein Parameter, spiele currentdir_playlist ab return "Argument not known, keep empty for currentdir or \"playlist\" for your managed playlist" if $par !~ m/(currentdir|playlist)/; MediaList_OnPlayPressed($hash, $par); } # sortiere Playlist nach Kriterien, zb. File, Title, Artist, etc if($cmd eq "SortBy") { return "sort criteria required: ". $sets{$cmd}{"args"} if (!$par); my $json = ReadingsVal($me, "currentdir_playlist", "[]"); return "no currentdir_playlist available, please select one" if($json eq "[]"); $json = MediaList_playlist_sort($json, $par, "asc"); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "currentdir_playlist", $json); readingsBulkUpdate($hash, "sortby", $par); readingsEndUpdate($hash, 1); return undef; } } ########################################################################### ########################################################################### ########################################################################### #################################### # #################################### sub MediaList_OnPlayPressed ($$) { my ($hash, $pltype) = @_; my $me = $hash->{NAME}; my $PlayerDevice = AttrVal($me, "MediaList_PlayerDevice", undef); my $PathReplaceFrom = AttrVal($me, "MediaList_PathReplaceFrom", undef); my $PathReplaceTo = AttrVal($me, "MediaList_PathReplaceTo", undef); my $PlayerStartCommand = AttrVal($me, "MediaList_PlayerStartCommand", undef); my $MediaList_CacheFileDir = AttrVal($me, "MediaList_CacheFileDir", "cache/"); my $MediaList_mkTempCopy = AttrVal($me, "MediaList_mkTempCopy", "none"); my $playlist; my $playlistduration; return "an error occured in MediaList_OnPlayPressed" if (!$PlayerDevice); if ($pltype eq "currentdir") { $playlist = ReadingsVal($me, "currentdir_playlist", ""); $playlistduration = ReadingsVal($me, "currentdir_playlistduration", ""); } elsif ($pltype eq "playlist") { $playlist = ReadingsVal($me, "playlist", ""); $playlistduration = ReadingsVal($me, "playlistduration", ""); } return "Playlist empty" unless($playlist); my $file = $MediaList_CacheFileDir.$PlayerDevice.".m3u"; my @data = @{JSON::XS->new->decode($playlist)}; my $fh; my $hash_target = $defs{$PlayerDevice}; # check, if fhem system hardware supports symlinks my $symlink_check = eval{symlink("","");1}; $MediaList_mkTempCopy = "copy" if($MediaList_mkTempCopy eq "symlink" && $symlink_check != 1); # delete all outdated symbolic links if ($symlink_check == 1) { opendir(my $dh, $MediaList_CacheFileDir) || die "Medialist: $MediaList_CacheFileDir: $!"; while(my $filename = readdir($dh)) { if( -l $MediaList_CacheFileDir.$filename && $filename =~ m/^$me/) { unlink($MediaList_CacheFileDir.$filename); } } closedir($dh); # nicht vergessen } $fh = new IO::File ">$file"; for(my $j=0; $j<=$#data; $j++) { my $utf8file = decode("UTF-8","$data[$j]->{File}"); my $newName = $MediaList_CacheFileDir.$me."_".basename($utf8file); $newName =~ s/ä/ae/g; $newName =~ s/ö/oe/g; $newName =~ s/ü/ue/g; $newName =~ s/Ä/Ae/g; $newName =~ s/Ö/Oe/g; $newName =~ s/Ü/Ue/g; $newName =~ s/ß/ss/g; if ($MediaList_mkTempCopy eq "symlink") { symlink($utf8file, $newName); $data[$j]->{File} = basename($newName); } elsif ($MediaList_mkTempCopy eq "copy") { copy($utf8file, $newName); $data[$j]->{File} = basename($newName); } else { $data[$j]->{File} = $utf8file; } $data[$j]->{File} =~ s/^($PathReplaceFrom)/$PathReplaceTo/ if ($PathReplaceFrom && $PathReplaceTo); $fh->print("". encode("UTF-8", $data[$j]->{File}) ."\n"); Log3 $PlayerDevice, 5, "OnPlayPressed: File prepared for Player $PlayerDevice: ".$data[$j]->{File}; } close($fh); readingsBeginUpdate($hash_target); readingsBulkUpdate($hash_target, "playlist_json", $playlist); readingsBulkUpdate($hash_target, "playlistduration", $playlistduration); readingsEndUpdate($hash_target, 1); if ($PlayerStartCommand) { my($cmd_file, $cmd_dir, $cmd_ext) = fileparse($file, qr"\..[^.]*$"); $PlayerStartCommand =~ s/\/$file/; $PlayerStartCommand =~ s/\/$cmd_file/; $PlayerStartCommand =~ s/\/$cmd_ext/; Log3 $hash->{NAME}, 5, "MediaList: Starte Player mit: set ".$PlayerDevice." ".$PlayerStartCommand; fhem ("set ".$PlayerDevice." ".$PlayerStartCommand); } return undef; } ################################### # PlaylistFunktionen ################################## # PlaylistAdd # Parameter: Tracknummer oder leer (Alle Tracks werden verwendet) ################################## sub MediaList_PlayListAdd($$) { my ($hash, $par) = @_; my $me = $hash->{NAME}; my $curpl = ReadingsVal($me, "currentdir_playlist", ""); my $curpldur = ReadingsVal($me, "currentdir_playlistduration", 0); my $pl = ReadingsVal($me, "playlist", ""); my $pldur = ReadingsVal($me, "playlistduration", 0); return "Playlist empty" unless($curpl); my @curpldata; my @pldata; @curpldata = @{JSON::XS->new->decode($curpl)}; @pldata = @{JSON::XS->new->decode($pl)} if($pl ne ""); if($par eq "") { # alles übergeben push(@pldata, @curpldata); $pldur += $curpldur; } else { return "Argument not an integer" if($par !~ m/[0-9]+/); return "Invalid track number, only ". $#curpldata ." Tracks available" if($par>(scalar @curpldata)); push(@pldata, $curpldata[$par]); $pldur += $curpldata[$par]->{Time}; } $pl = JSON::XS->new->encode(\@pldata); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "playlist", $pl); readingsBulkUpdate($hash, "playlistduration", $pldur); readingsEndUpdate($hash, 1); } ################################## # PlaylistDel # Parameter: Tracknummer die aus der PL geloescht werden soll ################################## sub MediaList_PlayListDel($$) { my ($hash, $par) = @_; my $me = $hash->{NAME}; my $pl = ReadingsVal($me, "playlist", ""); my $pldur = ReadingsVal($me, "playlistduration", 0); return "Playlist empty" unless($pl); return "Argument not an integer" if($par !~ m/[0-9]+/); my @pldata; @pldata = @{JSON::XS->new->decode($pl)}; return "Invalid track number, only ". $#pldata ." Tracks available" if($par>(scalar @pldata)); $pldur -= $pldata[$par]->{Time}; splice(@pldata, $par, 1); $pl = JSON::XS->new->encode(\@pldata); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "playlist", $pl); readingsBulkUpdate($hash, "playlistduration", $pldur); readingsEndUpdate($hash, 1); } #################################### # Startfunktion zur PlaylistInfo #################################### sub MediaList_call_playlistinfo($$) { my ($hash, $object) = @_; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "status", "gathering filelist"); readingsEndUpdate($hash, 1); #Log 3 , "$device: MediaList_call_playlistinfo"; $hash->{helper}{RUNNING_PID} = BlockingCall("MediaList_CollectID3Tags", $hash->{NAME}."|".$object, "MediaList_done_playlistinfo", 120); #, "MediaList_AbortFn", $hash return undef; } #################################### # Abschlussfunktion zur PlaylistInfo #################################### sub MediaList_done_playlistinfo($) { my ($string) = @_; my @t = split(/\|/, $string); my $hash=$defs{$t[0]}; my $playlist= $t[1]; my $playlistduration = 0; delete($hash->{helper}{RUNNING_PID}); $playlist = MediaList_playlist_sort($playlist, "File", "asc"); # sortiere Playlist per default nach Dateinamen my @data = @{JSON::XS->new->decode($playlist)}; for(my $j=0; $j<=$#data; $j++) { $playlistduration += $data[$j]->{Time} } readingsBeginUpdate($hash); readingsBulkUpdate($hash, "currentdir_playlist", $playlist); readingsBulkUpdate($hash, "currentdir_playlistduration", $playlistduration); readingsBulkUpdate($hash, "sortby", "File"); readingsBulkUpdate($hash, "status", "idle"); readingsEndUpdate($hash, 1); return undef; } ##################################### # Timeoutfunktion zur PlaylistInfo #################################### sub MediaList_AbortFn($) { my ($hash) = @_; delete($hash->{helper}{RUNNING_PID}); Log3 $hash->{NAME}, 2, "MediaList: BlockingCall for ".$hash->{NAME}." was aborted"; } #################################### # Hauptfunktion zur Playlist # Rückgabe: JSON Object, für TabletUI Medialist # keine Rekursion! #################################### sub MediaList_CollectID3Tags ($) { my ($string) = @_; my @t = split(/\|/, $string); my $device = $t[0]; my $object = $t[1]; my @data; my $content; my $cover; my %covers; my $fh; my $time = time(); my $MediaList_CacheFileDir = AttrVal($device, "MediaList_CacheFileDir", "cache/"); my $file = $MediaList_CacheFileDir.'covers.txt'; #Format: Artist;Album;Url return "Objekt ($object) exitiert nicht" unless (-e $object); #lade cover in das Hash if (-e $file) { open($fh, "<", $file) or die "Datei nicht gefunden"; my @Zeilen = <$fh>; chomp(@Zeilen); close($fh); foreach(@Zeilen) { my @t = split(/;/,$_); $covers{uri_escape($t[0].$t[1])}=$t[2] if($t[2]); } } if (-f $object) { push(@data, MediaList_GetMP3Tags($device , $object)); } elsif (-d $object) { my $allowedExtensions = AttrVal($device, "MediaList_allowedExtensions", ".*"); opendir(my $dh, $object) || die "$object: $!"; while(my $filename = readdir($dh)) { #undef($cover); $cover darf nicht gelöscht werden, das erste gefundene Cover für diesen Folder soll für den Rest weiterverwendet werden if($filename !~ m/^[\.]+/) { #Log3 $device, 3, "$device -> Datei: ".$filename; $content = MediaList_GetMP3Tags($device, $object."/".$filename); if($content) { Log3 $device, 4, "MP3-Tags für \"".$object."/".$filename."\" gefunden: ".$content->{Artist}. " , " .$content->{Album}; $cover = $covers{uri_escape($content->{Artist}.$content->{Album})} if($covers{uri_escape($content->{Artist}.$content->{Album})}); if (!$cover) { Log3 $device, 4, "Lade Cover: ".$content->{Artist}. " , " .$content->{Album}; $cover = MediaList_GetCover($device, $content->{File}, $content->{Artist}, $content->{Album}); $cover="images/cd-empty.png" if(!$cover); $covers{uri_escape($content->{Artist}.$content->{Album})} = $cover; } $content->{Cover}=$cover; Log3 $device, 5, "CollectID3Tags: ".Dumper($content); push(@data, $content); # informiere Parent, aktualisiere playlist wenn Ausführung > 1sek if(time() - $time >= 1) { # manchmal wird danach die gesamte Liste nicht nochmal erneuert sodass diese unvollständig bleibt :( #BlockingInformParent("MediaList_readingsSingleUpdateByName", [$device, "currentdir_playlist", JSON::XS->new->encode(\@data)], 0); $time = time(); } } } } closedir($dh); # nicht vergessen } return $device ."|" . JSON::XS->new->encode(\@data); } #################################### # Unterfunktion zur PlaylistInfo # Rückgabe -> Array: # {"Artist":"abc", "Title":"def", "Album":"yxz", "Time":"123", "File":"spotify:track:123456", "Track":"1", "Cover":"https://...." } #################################### sub MediaList_GetMP3Tags($$) { my ($device, $file) = @_; my $hash = $defs{$device}; my $mp3; my $res; return undef if ($file !~ m/(\.mp3|\.m4a)$/i); # keine mp3 Endung return undef if (-d $file); # ist Verzeichnis return undef if not (-f $file); # ist keine Datei # $file =~ s/([\(\)\s])/\\$1/g; # alle Zeichen:(,)," " entfernen if ($mp3 = MP3::Tag->new($file)) { my ($title, $track, $artist, $album, $comment, $year, $genre) = $mp3->autoinfo(); my $mp3info = get_mp3info($file); my $duration = round($mp3info->{SECS}, 0); utf8::encode($title); utf8::encode($artist); utf8::encode($album); utf8::encode($comment); $res = {"Artist" => $artist, "Title" => $title, "Album" => $album, "Time" => $duration, "File" => $file, "Cover" => ""}; Log3 $hash, 5, "GetMP3Tags: ".Dumper($res); return $res; } return undef; } ###################### # https://www.allcdcovers.com/api ###################### sub MediaList_GetCover($$$$) { my ($device, $filename, $artist, $album) = @_; my $cover; my $fh; my $MediaList_CacheFileDir = AttrVal($device, "MediaList_CacheFileDir", "cache/"); my $file = $MediaList_CacheFileDir.'covers.txt'; #Format: Artist;Album;Url # Todo persistente Speicherung der Cover $cover = MediaList_CheckCoverAtPath($device, $filename); $cover = MediaList_DownloadCover($device, $artist, $album) if(!$cover); if ($cover && (length($artist) > 0 || length($album) > 0 )) { open($fh, ">>", $file) or die "Datei nicht gefunden"; #Format: Artist;Album;Url print $fh $artist .";". $album .";". $cover ."\n"; close($fh); } return $cover; } #################################### # Download von Covern # https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html # http://www.myuuzik.de/index.php?SearchIndex=Music&Keywords=4+strings+Turn+It+Around # https://www.google.de/search?q=cover+strumbellas+spirit&tbm=isch # https://duckduckgo.com/?q=rhianna+unfaithful&iax=1&ia=images # # TODO # https://www.allcdcovers.com/api # https://musicbrainz.org/doc/Cover_Art_Archive/API # # über den Schlüssel von %params wird die Priorität der Datenabfrage gesteuert # zb. 2_itunes -> Prio 2, erste wenn myuuzik nichts gefunden hat # # 3. Spotify: http://jsfiddle.net/JMPerez/0u0v7e1b/ # https://developer.spotify.com/web-api/ # # 4. LastFM # http://www.last.fm/api #################################### sub MediaList_DownloadCover($$$) { my ($device, $artist, $album) = @_; my $hash = $defs{$device}; my $HttpResponse; my $HttpResponseErr; my @matches; my $search = ""; $artist = undef if (lc($artist) =~ m/various/); $search .= $artist ." " if($artist); $search .= $album; $search =~ s/\W/ /g; # alle sonderzeichen entfernen my %params = ("1_myuuzik" => {"baseurl"=> "http://www.myuuzik.de/index.php?SearchIndex=Music", "term" => "&Keywords=" . uri_escape($search), "pattern" => "img src=\"(http:\/\/ecx\.images-amazon\.com\/images[^\"]+)\"",}, "3_itunes" => {"baseurl"=> "https://itunes.apple.com/search?", "term" => "term=" . uri_escape($search), "pattern" => "\"artworkUrl100\"\:\"(http[^\"]+)\"",}, "2_spotify" => {"baseurl"=> "https://api.spotify.com/v1/search?type=album&", "term" => "q=" . uri_escape($search), "pattern" => "\"url\"\ : \"(http[^\"]+)\"",}, "3_lastfm_1"=> {"baseurl"=> "http://ws.audioscrobbler.com/2.0/?method=album.search&api_key=f3a26c7c8b4c4306bc382557d5c04ad5&", "term" => "album=" . uri_escape($album), "pattern" => "\(.+)<\/image>",}, ); foreach my $engine (sort keys(%params)) { my $url = "$params{$engine}{baseurl}" . "$params{$engine}{term}"; Log3 $device, 4, "DownloadCover: Hole URL: ". $url; my $param = { url => $url, timeout => 5, hash => $hash, # Muss gesetzt werden, damit die Callback funktion wieder $hash hat method => "GET", # Lesen von Inhalten header => "User-Agent: Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:10.0) Gecko/20100101 Firefox/10.0" }; ($HttpResponseErr, $HttpResponse) = HttpUtils_BlockingGet($param); if(length($HttpResponseErr) > 0) { Log3 $device, 3, "GetCover: Fehler beim abrufen der Daten"; Log3 $device, 3, "GetCover: " . $HttpResponseErr; } #return Dumper($HttpResponse); @matches = ( $HttpResponse =~ /$params{$engine}{pattern}/igm ); #return Dumper(@matches); last if($matches[0]); } if($matches[0]) { return $matches[0]; } else { Log3 $device, 4, "GetCover: Cover nicht auffindbar: ".$search; return undef; } } #################################### # Funktion checkt, ob im angegebenen Pfad # eine CoverDatei liegt #################################### sub MediaList_CheckCoverAtPath($$) { my ($device, $fullfile) = @_; my $cover; #my $PathReplaceFrom = "/media/music/"; #my $PathReplaceTo = "https://192.168.10.30/music/"; my $PathReplaceFrom = AttrVal($device, "MediaList_PathReplaceFrom", undef); my $PathReplaceTo = AttrVal($device, "MediaList_PathReplaceToPic", undef); my($file, $dir, $ext) = fileparse($fullfile, qr"\..[^.]*$"); opendir(my $dh, $dir) || die "$dir: $!"; while(my $filename = readdir($dh)) { Log3 $device, 5, "Checke Cover in $dir: $filename"; if(lc($filename) =~ m/front.*\.jpg/ || lc($filename) =~ m/cover.*\.jpg/) { $cover = $dir.$filename; $cover =~ s/^($PathReplaceFrom)/$PathReplaceTo/ if ($PathReplaceFrom && $PathReplaceTo); return $cover; } } closedir($dh); return undef; } #################################### # Aus dem BlockingCall Readings aktualisieren #################################### sub MediaList_readingsSingleUpdateByName($$$) { my ($devName, $readingName, $readingVal) = @_; my $hash = $defs{$devName}; #Log3 $hash, 4, "MediaList_readingsSingleUpdateByName: Dev:$devName Reading:$readingName Val:$readingVal"; readingsSingleUpdate($defs{$devName}, $readingName, $readingVal, 1); } #################################### # die PlayList sortieren, Parameter # 1. Hash # 2. SortiTem: Filename, Title # 3. order: asc, desc #################################### sub MediaList_playlist_sort { my ($json, $SortItem, $order) = @_; my @t; my @sortdata; my @data = @{JSON::XS->new->decode($json)}; # Log3 undef, 1, "JSON: ".Dumper($json); # Log3 undef, 1, "DATA: ".Dumper(@data); for(my $j=0; $j<=$#data; $j++) { push(@t, $data[$j]->{$SortItem}); } @t = sort(@t); @t = reverse @t if($order eq "desc"); for(my $i=0; $i<=$#t; $i++) { for(my $j=0; $j<=$#data; $j++) { if($t[$i] eq $data[$j]->{$SortItem}) { push(@sortdata, $data[$j]); } } } return JSON::XS->new->encode(\@sortdata); } #################################### # CrawlerRoutine zur Navigation im # Verzeichnis #################################### sub MediaList_Crawl($$) { my ($hash, $startdir) = @_; #my @e = split(/:\ /, $event); #my $startdir = $e[1]; my @list; my $SelItem; my $cmdBack = "Back"; my $FolderIdent = "*"; my $me = $hash->{NAME}; $startdir = "/" if ($startdir eq ""); my $CurDir = ReadingsVal($me, "CurrentDir", "/"); $startdir =~ s/^\*(.*)/$1/g; # FolderIdent wieder entfernen if ($startdir eq $cmdBack) { my @dir = split("/", $CurDir); pop(@dir); $startdir = join("/", @dir) if ($#dir > 0); $startdir = "/" if ($#dir == 0); $SelItem= $startdir; } elsif (!(-d $startdir || -d $CurDir."/".$startdir)) { # Datei anstatt Verzeichnis ausgewählt $SelItem= $CurDir."/".$startdir; $startdir = $CurDir; } elsif ($startdir =~ m/^\//) { # absoluter Pfad angegeben $startdir = $startdir; $SelItem= $startdir; } else { # relativer Pfad $startdir = $CurDir."/".$startdir; $startdir =~ s/^\/\//\//g; $SelItem= $startdir; } $startdir = $hash->{ROOT} unless ($startdir =~ m/^($hash->{ROOT})/); if (-d $startdir) { my $allowedExtensions = AttrVal($me, "MediaList_allowedExtensions", ".*"); opendir(my $dh, $startdir) || die "$startdir: $!"; while(my $filename = readdir($dh)) { if($filename !~ m/^\..*/) { #Log3 undef, 3, "Datei: ".$filename; $filename = $FolderIdent . $filename if(-d $startdir."/".$filename); push(@list, $filename); } } closedir($dh); # nicht vergessen } @list=sort(@list); unshift(@list, $cmdBack) unless($startdir eq $hash->{ROOT}); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "FolderContent", join(":", @list)); readingsBulkUpdate($hash, "CurrentDir", $startdir); readingsBulkUpdate($hash, "SelectedItem", $SelItem); readingsEndUpdate($hash, 1); return undef; } ##################################### 1; =pod =item helper =item summary creates an MediaList based on an local Mediashare for submission to any devices =item summary_DE Erstellt eine Playlist aus lokaler Musik zur Übergabe an ein beliebiges Device =begin html

MediaList

    This module can support you to navigate trough you local connected music library. It can compile complex playlists and als an quick playing of an selected whole path.
    Note: this module needs the following additional modules:
    • libmp3-tag-perl
    • libjson-xs-perl
    • libmp3-info-perl


    Define
      define <name> MediaList <RootPath>

      Defines a new instanze of MediaList. The Rootpath defines your start directory.
      Examples:
        define MyMediaList MediaList /media/music

    Readings
    • CurrentDir:your navigated current directory
    • FolderContent:the folder content of CurrentDir
    • SelectedItem:the last selected Item from FolderContent
    • currentdir_playlist:playlist of CurrentDir
    • currentdir_playlistduration:duration of currentdir_playlist
    • playlist:your playlist ;)
    • playlistduration:duration of your playlist

    Set
    • RequestedDirectory
      Moving to given relative directory. An record of Reading FolderContent must be used.
      Example:
        set <MyMediaList> RequestedDirectory AbbaMusic
    • Play
      Submit the Playlist to your defined Targetdevice. You has to select which playlist you want to submit
    • currentdir: Submit the playlist of current directory, see Reading currentdir_playlist
    • playlist: Submit the real playlist, see Reading playlist

    • Example:
        set <MyMediaList> Play currentdir
        set <MyMediaList> Play playlist
    • Playlist_New
      Creates an new playlist.
      Example:
        set <MyMediaList> Playlist_New MyNewPlaylist
    • Playlist_Add
      Add an Track or complete currentdir to your playlist

      Example:
        set <MyMediaList> Playlist_Add 0
        Add first Track from Reading currentdir_playlist to your playlist
        set <MyMediaList> Playlist_Add
        Add all Tracks from Reading currentdir_playlist to your playlist
    • Playlist_Del
      Deletes an Track from your playlist.
      Example:
        set <MyMediaList> Playlist_Del 0
        Drops first Track from your Playlist
    • Playlist_Empty
      Makes your playlist empty.
      Example:
        set <MyMediaList> Playlist_Empty

    Get
      N/A

    Attributes
    • MediaList_PlayerDevice
      Definition of your Traget Player Device
      Example:
        attr <MyMediaList> MediaList_PlayerDevice Sonos_LivingRoom

    • MediaList_PathReplaceFrom
      Rewrite the local mediapath to an accessible path by Targetdevice. This Attribut define the FROM pattern.
      Example:
        attr <MyMediaList> MediaList_PathReplaceFrom /media/music/

    • MediaList_PathReplaceTo
      Rewrite the local mediapath to an accessible path by Targetdevice. This Attribut define the TO pattern.
      Example:
        attr <MyMediaList> MediaList_PathReplaceTo \\NAS/music/

    • MediaList_PathReplaceToPic
      Rewrites the local Cover path to an accessible path your Webbrowser, TabletUI. This Attribut define the TO pattern. The FROM pattern are defined by MediaList_PathReplaceFrom
      Example:
        attr <MyMediaList> MediaList_PathReplaceToPic https://192.168.1.30/music/

        For this example you has to share your music directory via Webserver
    • MediaList_PlayerStartCommand
      Definition of the Startcommand for your Targetdevice.
      Example:
        attr <MyMediaList> MediaList_PlayerStartCommand StartPlaylist file:<fullfile>

        Command to insert the playlist into your Targetdevice ans starts playing. The definition of fullfile defines a internal dummy to rewrite it by a real playlistname
    • MediaList_CacheFileDir
      Definition of your cachefiledir. In this directory the playlist.m3u will be created. In cases of symlinks or music-copies, this directory will be used
      Example:
        attr <MyMediaList> MediaList_CacheFileDir /var/lib/mpd/playlists/
        attr <MyMediaList> MediaList_CacheFileDir cache/
    • MediaList_mkTempCopy
      Definition if you want a playlist with remote files or local accessible files.
      In case of using an sonos device, an remote file based playlist is sufficient.
      In case of using an MPD, local files in MPD music directory must be used
      Example:
        attr <MyMediaList> MediaList_mkTempCopy none
        In case of an Sonos Device
        attr <MyMediaList> MediaList_mkTempCopy symlink
        In case of an MPD Device
=end html =begin html_DE

MediaList


Eine deutsche Beschreibung ist aktuell nur im WIKI verfügbar.
Wiki MediaList =end html_DE =cut