From 9b64c9dfbbfde91df75754cd439dd969cb175e9e Mon Sep 17 00:00:00 2001 From: "Tobias.Faust" <> Date: Wed, 8 Mar 2017 13:48:29 +0000 Subject: [PATCH] 98_MediaList.pm: adds support to navigate through local attached media library, pls see https://wiki.fhem.de/wiki/MediaList git-svn-id: https://svn.fhem.de/fhem/trunk@13639 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 2 + fhem/FHEM/98_MediaList.pm | 983 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 985 insertions(+) create mode 100644 fhem/FHEM/98_MediaList.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 438f6f935..4e935f852 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 98_MediaList: adds support to navigate through local attached + media library, pls see https://wiki.fhem.de/wiki/MediaList - feature: 98_Text2Speech: new server mode to serve audiofile for requestors - feature: at+notify: extend the FHEMWEB Wizard with a simple command modifier - feature: 42_SYSMON: support ssh login with public key diff --git a/fhem/FHEM/98_MediaList.pm b/fhem/FHEM/98_MediaList.pm new file mode 100644 index 000000000..cc33b0965 --- /dev/null +++ b/fhem/FHEM/98_MediaList.pm @@ -0,0 +1,983 @@ + +############################################## +# $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, libmath-round-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; +use Math::Round qw/round/; +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 + ); + +} +########################################################################### + +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); + } +} + +########################################################################### +########################################################################### +########################################################################### + +#################################### +# +#################################### +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) = @_; + #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}); + + 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); + 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) { + 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); + + utf8::encode($title); + utf8::encode($artist); + utf8::encode($album); + utf8::encode($comment); + + $res = {"Artist" => $artist, "Title" => $title, "Album" => $album, "Time" => round($mp3info->{SECS}), "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); +} + + +#################################### +# 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
    • +
    • libmath-round-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