fhem-mirror/FHEM/98_MediaList.pm
Tobias.Faust fbc65e8f96 98_Medialist.pm: bugfixing "round" problem
git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@20005 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2019-08-16 09:46:20 +00:00

1044 lines
34 KiB
Perl

##############################################
# $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 <name> MediaList <RootFolder>" 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/\<fullfile\>/$file/;
$PlayerStartCommand =~ s/\<filename\>/$cmd_file/;
$PlayerStartCommand =~ s/\<fileext\>/$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\ size=\"extralarge\">(.+)<\/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
<a name="MediaList"></a>
<h3>MediaList</h3>
<ul>
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.
<br>
Note: this module needs the following additional modules:<br>
<ul>
<li>libmp3-tag-perl</li>
<li>libjson-xs-perl</li>
<li>libmp3-info-perl</li>
</ul>
<br>
<br>
<a name="MediaList define"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; MediaList &lt;RootPath&gt; </code>
<br><br>
Defines a new instanze of MediaList. The Rootpath defines your start directory.
<br>
Examples:
<ul>
<code>define MyMediaList MediaList /media/music</code><br>
</ul>
</ul>
<br>
<a name="MediaListReadings"></a>
<b>Readings</b><br>
<ul>
<li><b>CurrentDir</b>:your navigated current directory</li>
<li><b>FolderContent</b>:the folder content of CurrentDir</li>
<li><b>SelectedItem</b>:the last selected Item from FolderContent</li>
<li><b>currentdir_playlist</b>:playlist of CurrentDir</li>
<li><b>currentdir_playlistduration</b>:duration of currentdir_playlist</li>
<li><b>playlist</b>:your playlist ;)</li>
<li><b>playlistduration</b>:duration of your playlist</li>
</ul>
<br>
<a name="MediaListset"></a>
<b>Set</b>
<ul>
<li><b>RequestedDirectory</b><br>
Moving to given relative directory. An record of Reading <i>FolderContent</i> must be used.
<br>Example:<br>
<ul>
<code>set &lt;MyMediaList&gt; RequestedDirectory AbbaMusic</code><br>
</ul>
</li>
<li><b>Play</b><br>
Submit the Playlist to your defined Targetdevice. You has to select which playlist you want to submit<br>
<li><b>currentdir</b>: Submit the playlist of current directory, see Reading <i>currentdir_playlist</i></li>
<li><b>playlist</b>: Submit the real playlist, see Reading <i>playlist</i></li>
<br>Example:<br>
<ul>
<code>set &lt;MyMediaList&gt; Play currentdir</code><br>
<code>set &lt;MyMediaList&gt; Play playlist</code><br>
</ul>
</li>
<li><b>Playlist_New</b><br>
Creates an new playlist.
<br>Example:<br>
<ul>
<code>set &lt;MyMediaList&gt; Playlist_New MyNewPlaylist</code><br>
</ul>
</li>
<li><b>Playlist_Add</b><br>
Add an Track or complete currentdir to your playlist<br>
<br>Example:
<ul>
<code>set &lt;MyMediaList&gt; Playlist_Add 0</code><br>
Add first Track from Reading <i>currentdir_playlist</i> to your playlist<br>
<code>set &lt;MyMediaList&gt; Playlist_Add</code><br>
Add all Tracks from Reading <i>currentdir_playlist</i> to your playlist<br>
</ul>
</li>
<li><b>Playlist_Del</b><br>
Deletes an Track from your playlist.
<br>Example:<br>
<ul>
<code>set &lt;MyMediaList&gt; Playlist_Del 0</code><br>
Drops first Track from your Playlist
</ul>
</li>
<li><b>Playlist_Empty</b><br>
Makes your playlist empty.
<br>Example:<br>
<ul>
<code>set &lt;MyMediaList&gt; Playlist_Empty</code><br>
</ul>
</li>
</ul>
<br>
<a name="MediaListget"></a>
<b>Get</b>
<ul>N/A</ul><br>
<a name="MediaListattr"></a>
<b>Attributes</b>
<ul>
<li><b>MediaList_PlayerDevice</b><br>
Definition of your Traget Player Device
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_PlayerDevice Sonos_LivingRoom</code><br>
<br>
</ul>
</li>
<li><b>MediaList_PathReplaceFrom</b><br>
Rewrite the local mediapath to an accessible path by Targetdevice. This Attribut define the FROM pattern.
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_PathReplaceFrom /media/music/</code><br>
<br>
</ul>
</li>
<li><b>MediaList_PathReplaceTo</b><br>
Rewrite the local mediapath to an accessible path by Targetdevice. This Attribut define the TO pattern.
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_PathReplaceTo \\NAS/music/</code><br>
<br>
</ul>
</li>
<li><b>MediaList_PathReplaceToPic</b><br>
Rewrites the local Cover path to an accessible path your Webbrowser, TabletUI. This Attribut define the TO pattern.
The FROM pattern are defined by <i>MediaList_PathReplaceFrom</i>
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_PathReplaceToPic https://192.168.1.30/music/</code><br>
<br>For this example you has to share your music directory via Webserver
</ul>
</li>
<li><b>MediaList_PlayerStartCommand</b><br>
Definition of the Startcommand for your Targetdevice.
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_PlayerStartCommand StartPlaylist file:&lt;fullfile&gt;</code><br>
<br> Command to insert the playlist into your Targetdevice ans starts playing. The definition of <i>fullfile</i>
defines a internal dummy to rewrite it by a real playlistname
</ul>
</li>
<li><b>MediaList_CacheFileDir</b><br>
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
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_CacheFileDir /var/lib/mpd/playlists/</code><br>
<code>attr &lt;MyMediaList&gt; MediaList_CacheFileDir cache/</code><br>
</ul>
</li>
<li><b>MediaList_mkTempCopy</b><br>
Definition if you want a playlist with remote files or local accessible files.<br>
In case of using an sonos device, an remote file based playlist is sufficient.<br>
In case of using an MPD, local files in MPD music directory must be used
<br>Example:
<ul>
<code>attr &lt;MyMediaList&gt; MediaList_mkTempCopy none</code><br>
In case of an Sonos Device<br>
<code>attr &lt;MyMediaList&gt; MediaList_mkTempCopy symlink</code><br>
In case of an MPD Device
</ul>
</li>
</ul>
</ul>
=end html
=begin html_DE
<a name="MediaList"></a>
<h3>MediaList</h3>
<br>
Eine deutsche Beschreibung ist aktuell nur im WIKI verfügbar.<br>
<a href="https://wiki.fhem.de/wiki/MediaList">Wiki MediaList</a>
=end html_DE
=cut