mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-02-01 13:29:26 +00:00
44972e52d2
git-svn-id: https://svn.fhem.de/fhem/trunk@20005 2b470e98-0d58-463d-a4d8-8e2adae1ed80
1044 lines
34 KiB
Perl
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 <name> MediaList <RootPath> </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 <MyMediaList> 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 <MyMediaList> Play currentdir</code><br>
|
|
<code>set <MyMediaList> Play playlist</code><br>
|
|
</ul>
|
|
</li>
|
|
|
|
<li><b>Playlist_New</b><br>
|
|
Creates an new playlist.
|
|
<br>Example:<br>
|
|
<ul>
|
|
<code>set <MyMediaList> 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 <MyMediaList> Playlist_Add 0</code><br>
|
|
Add first Track from Reading <i>currentdir_playlist</i> to your playlist<br>
|
|
<code>set <MyMediaList> 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 <MyMediaList> 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 <MyMediaList> 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 <MyMediaList> 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 <MyMediaList> 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 <MyMediaList> 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 <MyMediaList> 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 <MyMediaList> MediaList_PlayerStartCommand StartPlaylist file:<fullfile></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 <MyMediaList> MediaList_CacheFileDir /var/lib/mpd/playlists/</code><br>
|
|
<code>attr <MyMediaList> 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 <MyMediaList> MediaList_mkTempCopy none</code><br>
|
|
In case of an Sonos Device<br>
|
|
<code>attr <MyMediaList> 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
|